跨境电商系统三大 Web 漏洞的防护:SQL 注入、XSS、CSRF

一、本质是权限边界问题

跨境电商系统的敏感数据分布在三层:数据库里(证件、地址、支付流水)、浏览器里(会话 Cookie、令牌)、资金链路里(收款账户、退款指令)。这三类漏洞恰好各针对其中一层:

漏洞 攻击面所在层 攻击者最终想拿到的东西
SQL 注入 应用层到数据库层 库里的数据、库的写权限
XSS 服务端渲染到用户浏览器 用户的会话、用户在页面上的操作权
CSRF 用户浏览器到服务端 借用户的身份发起操作

SQL 注入属于信任边界丢失 :用户输入被当作代码执行;XSS 是上下文边界丢失 :数据被当作代码渲染;CSRF 是主体边界丢失:浏览器自动附带的凭证被第三方站点借用。理解这一点,后面所有措施都只是同一件事的具体实现:设定边界,并在边界处做类型和上下文检查。


二、SQL 注入

先看跨境场景里最容易出错的地方

注入不一定发生在登录框。跨境电商系统里有几类输入是"看起来不像参数"的:

  • 排序与分页 :ORDER BY ${sortField} ${sortOrder} 是参数化查询无法绑定的部分(列名不能用占位符),这是最常见的注入点。
  • 多语言字段与表名 :部分系统按语言分表(product_en / product_de),表名由前端传语言码拼接。
  • 报表导出与对账查询 :为了支持任意维度筛选,后端用字符串拼 WHERE 子句。
  • 后台搜索:运营后台按"模糊匹配 + 多条件组合",拼接风险最高。

正确的做法

1)参数化查询,但要知道它无法处理什么

ORDER BY、GROUP BY、表名、列名不能用占位符绑定。这类只能用白名单映射处理后拼进语句:

复制代码
// 错误:直接拼接
String sql = "SELECT * FROM product ORDER BY " + sortField + " " + sortOrder;

// 正确:白名单映射
private static final Map<String, String> SORT_MAP = Map.of(
    "price", "price_amount",
    "sales", "sale_count",
    "created", "create_time"
);

String column = SORT_MAP.get(sortField);
if (column == null) {
    throw new BizException("非法的排序字段");
}
String direction = "asc".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC";
String sql = "SELECT * FROM product ORDER BY " + column + " " + direction;

MyBatis 的 ${} 就是字符串拼接,#{} 才是预编译。可以用 Sonar 或 Checkstyle 自定义规则,禁止 ${} 出现在 mapper 的 SQL 语句里,只允许在动态表名等明确场景加注释豁免。

2)不要用黑名单拦关键字

原文说拦截 union、select、and、or,这在跨境电商里会直接出故障:商品标题里有 "Union Jack 卫衣"、品牌名里有 "SELECTED",全被拦掉;而攻击者用 sel/**/ect、URL 编码、十六进制、大小写混合即可绕过。黑名单只能拦没有经验的攻击者,同时会误伤业务。

正确的做法是做白名单式的类型校验:

复制代码
// 商品 ID、订单号、用户 ID 这类参数,只允许数字与下划线
private static final Pattern ID_PATTERN = Pattern.compile("^[A-Za-z0-9_]{1,32}$");

public void checkId(String id) {
    if (id == null || !ID_PATTERN.matcher(id).matches()) {
        throw new BizException("参数格式异常");
    }
}

业务参数则交给参数化查询,输入是否含有 SQL 关键字无关紧要,因为它并不会变成 SQL 语法的一部分。

3)ORM 的进阶设置

启用 MyBatis safeResultHandlerEnabled、关闭 allowMultiQueries(JDBC URL 参数,防止堆叠查询);JPA 场景避免使用 @Query 的 nativeQuery = true 拼接参数,改用 EntityManager.createNativeQuery(sql).setParameter(...)。

权限与运维层

数据库账号按业务拆分,这是防止注入影响扩大的最后一道保障:

账号 权限 使用方
app_ro 只读核心表 前台商品、搜索查询
app_order 订单、库存、支付流程表的 CRUD 订单服务
app_report 只读从库、仅限指定视图 报表、对账
dba_admin 全部权限,但禁止应用使用 运维人工操作

app_* 账号一律禁止 DDL 权限和 FILE 权限(FILE 可读写服务器文件,是提权跳板)。开启 skip_name_resolve,只允许应用服务器内网段连接;云数据库用安全组而非 IP 白名单兜底,因为海外节点 IP 会变动。

WAF 的注入规则可以开,但一定要开启观察模式运行一段时间再启用拦截,否则商品搜索这类正常请求会被误拦,导致订单量下降。日志里按命中规则统计误杀率,高于 0.5% 的规则要调整。


三、XSS

三种类型在跨境场景中应该怎么区分

  • 存储型:商品评论、店铺公告、客服消息、买家秀。危害最大,一次植入长期有效,而且跨境场景下的审核链路更长(多语言内容机审准确率低),攻击者常利用小语种绕过审核。
  • 反射型 :搜索结果页回显关键字、语言切换参数、支付完成后的跳转提示页(?msg=支付成功)。
  • DOM 型 :单页应用里用 innerHTML、document.write、location.hash 渲染的路由与富文本预览。

关键纠正:输出转义只发生在渲染时,不在输入端

原文说"对所有用户提交内容过滤 <script>",这条单独用是错的:数据库里存的是原始内容,同一份数据要渲染到 HTML、JS、URL、属性四种上下文,转义规则完全不同。在输入时转义一次,等于假设只有一个输出位置,而且会把用户内容改坏(比如代码分享类内容被转义后还原不了)。

正确做法是"输入只校验长度与类型,输出按上下文转义":

复制代码
// 按上下文选择转义器(以 OWASP Java Encoder 为例)
// HTML 正文
out.print(Encode.forHtml(userContent));
// HTML 属性值
out.print("<input value=\"" + Encode.forHtmlAttribute(value) + "\">");
// JS 字符串
out.print("var msg = '" + Encode.forJavaScript(msg) + "';");
// URL 参数
out.print("<a href=\"/search?q=" + Encode.forUriComponent(keyword) + "\">");

模板引擎默认都开转义(Thymeleaf 的 th:text、Vue 的 {``{ }}、React 的 JSX),出问题的都是主动关掉转义的地方:th:utext、v-html、dangerouslySetInnerHTML。把这些 API 加进代码审查的强制检查项,每次使用必须在 PR 里说明数据来源与过滤方式。

富文本必须经过服务端 HTML 净化(推荐 OWASP Java HTML Sanitizer 或 DOMPurify 的服务端版本),使用标签和属性白名单而不是黑名单:

复制代码
// 只允许这几种标签和属性,其余一律剥离
PolicyFactory policy = new HtmlPolicyBuilder()
    .allowElements("p", "br", "strong", "em", "ul", "ol", "li", "img", "a")
    .allowAttributes("src", "alt").onElements("img")
    .allowAttributes("href").onElements("a")
    .allowUrlProtocols("https")            // 只允许 https,挡掉 javascript: 与 data:
    .requireRelNofollowOnLinks()
    .toFactory();
String safe = policy.sanitize(rawComment);

为什么同时需要 HttpOnly 和 CSP,说清楚这两条各自的适用范围:

HttpOnly 只能防"偷 Cookie",挡不住 XSS 本身。攻击者拿不到 Cookie,但可以在页面里直接发起已登录状态的请求;下单、改地址都不需要读 Cookie。所以 HttpOnly 只是降低影响,并不是完整防护。

CSP 才是从根源阻断的手段,而且原文说的"禁止内联脚本"只是第一步,真正能挡住 XSS 的是 nonce 或 hash 机制:

复制代码
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{每次请求随机生成}' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

每次响应生成一个随机 nonce 注入到合法的 <script nonce="..."> 上,攻击者注入的脚本因为没有 nonce 而被拒绝执行。frame-ancestors 'self' 顺便把点击劫持也一起挡了。CSP 上线一定用 Content-Security-Policy-Report-Only 先跑两周,收集违规报告,把所有第三方脚本(海外统计、客服 SDK、支付 SDK)都纳入白名单后再切到强制模式,否则结账页会因为支付 SDK 被拦而无法付款。

业务侧补充

客服聊天窗口是跨境电商的特殊高风险区:买家和卖家可以互发消息和图片链接,且往往是独立的 iframe 或 WebSocket 渲染。这里要做三件事:消息渲染一律走文本节点(textContent)而不是 innerHTML;外链强制 rel="noopener noreferrer",并经过跳转确认页;图片 URL 做协议与域名校验,禁止 data: 与 javascript: 协议。


四、CSRF

先说一个容易被忽略的前提

CSRF 能成立需要三个条件同时满足:浏览器会自动带上凭证(Cookie)、参数可被攻击者预测、服务端不校验请求是否来自本站。因此防护也要从这三条分别入手,任何单一措施都有覆盖不到的情况。

1)Token 的正确实现方式

原文说 Token 存"会话与前端页面",这里有个细节:Token 如果存在 Session 里,跨域/多实例部署时要保证 Session 一致性;更稳妥的方式是双提交 Cookie 或基于签名的无状态 Token。

前后端分离架构下推荐这样:

复制代码
// 登录成功后签发一个与用户绑定、有时效的 CSRF Token
public String issueCsrfToken(String userId, String sessionId) {
    String raw = userId + "|" + sessionId + "|" + System.currentTimeMillis();
    String sign = hmacSha256(raw, csrfSecret);   // 服务端密钥签名
    return Base64.getUrlEncoder().encodeToString((raw + "|" + sign).getBytes());
}

// 校验:拆出原始部分,重算签名比对,再校验时效
public boolean verify(String token, String userId, String sessionId) {
    String decoded = new String(Base64.getUrlDecoder().decode(token));
    String[] parts = decoded.split("\\|");
    if (parts.length != 4) return false;
    if (!parts[0].equals(userId) || !parts[1].equals(sessionId)) return false;
    long ts = Long.parseLong(parts[2]);
    if (System.currentTimeMillis() - ts > 2 * 3600_000L) return false;  // 2 小时过期
    String expect = hmacSha256(parts[0] + "|" + parts[1] + "|" + parts[2], csrfSecret);
    return MessageDigest.isEqual(expect.getBytes(), parts[3].getBytes());  // 常量时间比较
}

用 MessageDigest.isEqual 而不是 equals 比较签名,避免时序攻击。Token 通过自定义请求头(X-CSRF-Token)传递,天然不受跨域表单提交影响,因为普通 HTML 表单无法设置自定义头。

2)SameSite 是被低估的一道防护措施

Cookie 的 SameSite 属性在主流浏览器(含 Safari 与 Chrome)上已经默认 Lax,这本身就挡掉了大部分跨站 POST。建议显式声明:

复制代码
Set-Cookie: SESSION=xxx; HttpOnly; Secure; SameSite=Lax; Path=/

注意两个例外:支付回调 和跨站单点登录 需要 SameSite=None; Secure。支付渠道的回调来自第三方域名,如果 Cookie 是 Lax,回调里的会话会丢失。这类接口不要依靠 Cookie 鉴权,而应该用渠道签名验签(微信、PayPal、Stripe 都有自己的签名机制),这既是安全要求也是合规要求。

3)Referer / Origin 校验的真实局限

原文已提到隐私浏览器会清空 Referer,补充一点:Origin 头在现代浏览器里对跨域 POST 一定会有,比 Referer 可靠,但服务端到服务端的调用、旧版浏览器、部分海外代理可能不带。所以正确的策略是"有 Origin 就严格校验,没有则回落到 Token 校验",把两者作为组合条件,而不是二选一。

4)敏感操作二次校验,要说清校验什么

改收款账户、退款、改支付密码这类操作,仅短信验证码不够:如果攻击者已经通过 XSS 获得会话,他可以在受害者不知情时触发验证码下发,再通过社工套取。更稳的是:

操作 二次校验方式
修改收款账户 / 提现 原支付密码 + 短信验证码 + 冷却期(24 小时内不可再次修改)
大额退款 审批流(双人复核),不做单点自助
修改登录密码 / 绑定邮箱 原密码 + 邮箱验证码,改密后强制全端下线
批量发布商品 频率限制 + 滑块验证

冷却期这一条常被忽略但很有效:资金类配置变更后 24 小时内不允许再次变更,给风控留出人工介入的时间窗。


五、四层防御的落地与验证

架构分层

复制代码
接入层  WAF(注入/XSS 规则,观察期 2 周后转拦截)+ CDN 回源校验
网关层  统一鉴权、CSRF Token 校验、请求频率限制、Origin 校验
应用层  参数化查询、输出转义、富文本净化、权限最小化
数据层  分账号权限、禁 DDL、内网访问、审计日志
业务层  内容审核、二次验证、异常交易监控、资金变更冷却期

验证方式,重点不是"有没有做",而是"做了是否有效"

安全措施上线后必须验证,否则容易认为已经开启、实际没有生效:

  • SQL 注入 :用 sqlmap 对订单查询、后台搜索、报表导出入参跑一遍,确认全部返回"参数异常"而非数据库报错。检查线上响应体里是否泄漏堆栈(搜索 SQLException、ORA-、You have an error in your SQL)。
  • XSS :在评论、店铺公告、客服消息各投一条 <img src=x onerror=alert(document.domain)>,确认存储后在所有端(PC、H5、APP 内嵌 WebView、海外小程序)都被转义。检查 CSP 报告端点上是否出现 blocked-uri 记录。
  • CSRF:写一个第三方域名的 HTML 页面,用表单 POST 到你们的退款接口,确认返回 403。这个测试要在删除 Token 的情况下做,验证 Token 确实是拦截原因。

常态化节奏

频率 动作
每次提交 SAST 扫描(注入、${} 使用、innerHTML、v-html)+ 依赖漏洞扫描
每周 DAST 扫描 + WAF 日志分析,统计拦截量 Top 规则与误杀率
每月 人工渗透测试(重点:资金链路、后台管理、第三方回调)
每季度 权限复核(数据库账号、云平台 IAM)、CSP 白名单清理
大促前 全量回归渗透 + 限流熔断验证

跨境电商特有的三条

  1. 不能只靠 IP 白名单。海外用户走代理、VPN 是常态,IP 黑名单会误伤大量正常用户;风控应以设备指纹、行为特征、Token 校验为主,IP 只作为辅助信号。
  2. 欧盟 GDPR、加州 CCPA 与《个人信息保护法》的处罚是实打实的。泄露事件要在 72 小时内向监管报告(GDPR 第 33 条),所以日志留存、入侵检测、事件响应预案要在平时就准备好,否则出事后连"泄露了什么"都说不清。
  3. 第三方 SDK 是供应链风险点 。客服、统计、支付、物流追踪 SDK 都在页面里执行脚本,任何一个被投毒都等于给了攻击者 XSS 能力。要求引入前做代码审计,用 SRI(integrity 属性)锁定外部脚本版本,并纳入 CSP 白名单单独管控。

六、结语

三类漏洞的共同点是:修复成本极低(参数化查询、输出转义、Token 校验都是几行代码),但一旦漏掉,代价极高。它们反复出现的真实原因不是技术难,而是默认安全没有成为开发习惯 :新接口赶工期直接用字符串拼 SQL,新页面为了快速上线用 v-html,新微服务忘了加 Token 校验。

所以真正的加固重点在工程侧:把这些检查写进代码规约、CI 流水线、代码审查清单和上线卡点,让"不安全"的写法在提交阶段就失败。技术方案只是第一步,让它变成不能绕过的流程,才是长期有效的防护。

相关推荐
冒险的豆子1 小时前
Proxy & Vue 响应式(合并精简版,适合面试)
前端·javascript·vue.js
Java后端的Ai之路2 小时前
前端技术Vue-18-视图中的渲染
前端·javascript·vue.js
程序员海军8 小时前
用了两年多 GPT,我最近开始换模型了,还折腾了个求职避坑 Agent
前端·人工智能·后端
zttbee9 小时前
echarts图表tooltip色块失效
前端·javascript·echarts
excel10 小时前
IndexNow:让搜索引擎更快发现网站更新的主动通知协议
前端·后端·架构
子兮曰10 小时前
CSS 复现 23 种设计风格:从拟物到液态玻璃的成本阶梯与兜底写法【实战】
前端·css·视觉设计
虚无境11 小时前
如何用AI帮我写出实用的插件
前端·javascript·ai编程
柳杉11 小时前
从数字化到数智化:3D 可视化大屏的下一个五年
前端·ai编程·数据可视化
风骏时光牛马12 小时前
从零到实战:AI全栈学习进阶路线
前端