输入、文件与出站请求:按解释器实施防护
同一个字符串进入不同组件,会被赋予不同含义:
| 输入去向 | 组件怎样解释它 | 主要处理方式 |
|---|---|---|
| SQL 查询值 | 数据库表达式中的值 | 参数绑定 |
| HTML 文本或属性 | 浏览器标记语法 | 对应上下文的输出编码 |
| Shell 命令 | 命令、重定向、参数和展开语法 | 避免 Shell 拼接,固定程序和参数结构 |
| 文件名或归档条目 | 文件系统路径 | 服务端命名、私有目录和路径归属检查 |
| XML、模板、对象序列化 | 文档、表达式或对象构造规则 | 收紧解析功能和允许类型 |
| HTTP 请求目标 | 协议、主机、端口、路径 | 固定目标映射或完整的出站访问策略 |
长度与类型校验可以拒绝不符合业务要求的数据。数据进入 SQL、HTML 或文件系统时,还需要按那个组件的语法处理。
字符串进入解释器后会发生什么
SQL 值与查询结构
PreparedStatement 把 SQL 结构和参数值分开传递:
try (PreparedStatement query = connection.prepareStatement(
"SELECT id FROM account WHERE name = ?")) {
query.setString(1, suppliedName);
try (ResultSet rows = query.executeQuery()) {
// 读取查询结果
}
}参数中的引号和运算符按字符串值处理。把不可信输入拼进 SQL,再尝试删除引号、关键字或注释符,会同时受到数据库语法、编码和业务字符需求影响。
表名、列名和排序方向属于 SQL 结构,通常不能放进参数占位符。让调用方传入 name 或 created 这样的业务选项,服务端映射到固定列名;未知选项明确拒绝。ORM、存储过程和查询构造器内部若继续拼接原始 SQL,仍可能产生相同问题。OWASP SQL 注入防护区分了参数化与结构白名单。
数据库账号仍应只具有业务需要的权限。参数化防止输入改写查询语法,对象授权决定哪些租户或用户的数据可以访问,两个控制需要同时存在。
HTML 输出位置决定编码方式
HTML 文本节点、带引号的属性、URL、JavaScript 和 CSS 具有不同语法。模板引擎的自动转义只覆盖它明确支持的上下文;调用 raw HTML 输出、字符串拼接脚本或设置 innerHTML 时,会重新进入标记或脚本解释。
只显示文字时,前端使用 textContent 等文本接口;后端按 HTML 文本上下文编码。属性中应使用带引号的固定属性名,并使用相应属性编码。URL 属性还要校验允许的协议和目标用途,HTML 编码不会把 javascript: 之类的危险协议变成可接受地址。
富文本需要允许部分标记,适合采用持续维护的 HTML 清洗器并限制元素、属性和 URL;不能把普通文本编码与富文本清洗混为一谈。CSP 可以限制脚本来源与执行方式,作为附加保护,但不应替代正确的输出处理。OWASP XSS 防护提供了各上下文的处理规则。
命令、模板与对象构造
能够用 Java API 完成的文件、压缩和网络操作,通常无需启动 Shell。必须调用外部程序时,固定可执行文件,并用参数列表传递数据;不要把输入拼成 sh -c 后的完整命令。
参数列表避免了 Shell 的管道与展开语法,但目标程序仍可能把以短横线开头的值解释成选项。需要按程序支持的方式使用 --、固定选项或限制值的形式,同时约束工作目录、环境变量、执行时限和进程权限。OWASP 命令注入防护说明了这种参数注入风险。
模板名称与模板内容应来自受控位置;用户提交的文字应作为模板数据,而不是交给模板引擎编译。反序列化同样会执行类型解析和对象构造。JSON 接口应使用明确 DTO、字段白名单和解析大小/深度限制,谨慎启用多态类型;不让输入中的类名任意决定实例化哪个 Java 类型。Java 原生序列化需要类型和资源过滤,接口说明见 Serialization Filtering。
XML 解析功能需要逐个限制
XML 外部实体可以把文档内容连接到文件读取或网络请求。通用配置应禁用不需要的 DOCTYPE、外部实体和外部 DTD/Schema,并限制文档大小、深度与展开成本。
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");完整工程还关闭外部一般实体、外部参数实体、XInclude 和实体引用展开。不同 XML API、XSLT、Schema 验证器可能拥有各自配置;给 DocumentBuilderFactory 设置一次,无法覆盖后来创建的另一个解析器。业务必须使用 DTD 或外部 Schema 时,采用受控目录和解析器,不重新开放任意网络地址。OWASP XXE 防护按 Java 解析器列出了设置方式。
运行查询、编码和解析实验
构建环境与第一组结果
下载 input-defense-lab.zip。Linux 宿主需要 Docker、unzip 和可用浏览器;当前账号具有 Docker daemon 权限。工程使用 Maven 3.9.12、Java 17 编译目标、H2 2.4.240 和 OWASP Java Encoder 1.3.1。Boot 4.1.1 只用于依赖管理与可执行 JAR 打包,程序没有启动 Spring Web 应用。
unzip input-defense-lab.zip
cd input-defense-lab
mkdir -p .m2
LAB_UID=$(id -u)
LAB_GID=$(id -g)
docker run --rm --user "$LAB_UID:$LAB_GID" -e MAVEN_CONFIG=/tmp/.m2 \
-v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
mvn -B -ntp -Dmaven.repo.local=/m2 -Duser.home=/tmp clean verify
docker run --rm --user "$LAB_UID:$LAB_GID" -v "$PWD:/work" -w /work \
eclipse-temurin:17.0.20_8-jdk \
java -jar target/input-defense-lab-1.0.0.jar构建和运行都使用宿主 UID/GID,target 与 Maven 缓存可写。Java 程序查询实际 H2 表后输出:
literal account matches: 1
injection-shaped value matches: 0
HTML text example: target/encoded-input.html测试中的隔离反例把同一输入拼接进 SQL,会匹配两行;正常 PreparedStatement 查询把它作为完整账号名,只得到零行。H2 数据库随连接关闭而回收。
镜像或依赖下载失败时检查仓库访问、代理与证书配置;离线环境先同步固定版本镜像和依赖。不生成 JAR 时先读 Maven 的实际编译或测试错误,不能跳过失败测试后继续使用旧 target 文件。
在浏览器检查输出的含义
打开 target/encoded-input.html,可以看到输入作为文本显示:

文件由程序调用 Encode.forHtmlContent 生成,所用 API 来自 OWASP Java Encoder。输入中的 img 没有成为 DOM 元素,其 onerror 也不会作为事件处理器执行。
在 F12 的 Elements 中查看 id=target 的节点,应看到文本节点。模板拼接和前端二次写入 DOM 都可能改变浏览器的解释结果,因此还要检查最终形成的节点。排查真实页面时,定位实际写入的 DOM API,并检查最后形成的节点。
测试覆盖的拒绝位置
DefenseTest 使用真实 H2 查询、XML 解析器、ZIP 流和本地 HTTP 服务器。XML 负例引用本轮临时文件,但解析器在 DOCTYPE 处拒绝;普通 XML 文本可以正常解析。
ZIP 负例包括 ../escape、Windows 形式路径、单文件解压超限、累计解压超限和条目过多。拒绝后检查私有目录没有残留新展开内容,防止异常返回时留下半成品供其他进程读取。
出站测试启动两个回环服务器。第一个返回 302,Location 指向第二个;客户端拒绝这个响应,第二个服务器的访问计数保持零。另有超大响应和先返回响应头、再延迟发送正文的路径,分别检查字节上限与完整交换时限。
文件和出站请求还需要哪些约束
上传文件从接收走到公开
文件处理通常经历接收、隔离存储、内容识别、解析或扫描、发布、下载和删除。原始文件名用于展示时也要处理,存储键由服务端生成;避免让调用方直接决定目录或覆盖已有文件。
上传输入
↓ 身份、对象权限、压缩/上传字节上限
私有隔离区
↓ 类型识别、解析限制、必要的扫描或转换
可发布对象
↓ 下载授权、内容类型、响应头、有效期
下载或预览
解析或扫描失败 → 保持隔离 / 删除本轮半成品扩展名和 Content-Type 都可以伪造,需要结合允许的格式、内容识别与安全解析。对图像、PDF、Office 文件等复杂格式,限制解析资源并更新处理库;恶意内容扫描也不能覆盖所有主动内容或解析器漏洞。
提供下载时设置符合实际内容的 Content-Type 和必要的 Content-Disposition,限制内容嗅探;允许用户上传 HTML/SVG 等主动内容时,考虑独立内容域、沙箱和严格访问策略。上传成功的文件仍要在下载入口检查对象权限。OWASP 文件上传指南讨论了存储、名称、类型和后续处理。
ZIP 路径与展开预算
归档中的 entry name 不是已经安全的相对路径。实验为每轮上传创建独立私有目录,把条目 resolve 后 normalize,再确认结果仍位于该目录内;绝对路径、反斜杠形式和重复目标被拒绝,文件使用 CREATE_NEW,避免覆盖。
路径字符串检查还依赖目录本身可信。攻击者能修改目标目录或插入符号链接时,校验与实际打开文件之间可能发生变化。实验只接受本进程新建、没有其他写入者的私有目录;生产共享目录需要更严格的权限与文件描述符级处理,不能把 startsWith 检查当作所有文件系统场景的完整解决方案。
实验上限刻意设置得很小:压缩输入 8192 字节、最多 20 个条目、单文件展开 2048 字节、累计展开 4096 字节。这些是便于观察的教学参数;生产应根据格式、内存、磁盘和业务预算重新确定,并限制嵌套归档与处理时间。
检查应发生在读取和展开过程中,而非全部解压完成之后。归档头部声称的尺寸未必可靠,实际读取的字节才会消耗资源。目录条目也拒绝携带数据,避免切换条目时隐式排空正文而绕过展开计数。失败时删除本轮私有目录,不删除上级目录或其他上传对象。
固定合作方请求与任意 URL 抓取
固定合作方业务可以只接受 partnerId,由服务端配置映射到目标 URI。调用方无法把输入替换成云元数据地址、内部管理接口或任意端口。实验 PartnerClient 就采用这一方式,并关闭自动重定向与代理。
重定向是一次新的目标选择。若业务需要允许跳转,每一跳都应重新判断协议、地址、端口和权限,并限制跳转次数;不要只校验最初 URL。代理配置也会改变实际连接路径、解析位置和可达网络,因此需要纳入出站策略。
任意 URL 抓取器更复杂。至少要使用严格 URL 解析,限制协议和端口,处理 userinfo、IPv4/IPv6 与地址表示差异,检查全部 DNS 结果,阻止环回、链路本地和不允许的内部网段,并在连接阶段保证使用的地址仍符合刚才的判断。
只在校验时解析一次 DNS,再把原始主机名交给会重新解析的 HTTP 客户端,可能受到 DNS rebinding 或校验/使用时地址变化的影响。需要能够控制地址解析和实际连接的网络组件,或者把请求送入专门的受限抓取服务;HTTPS 还应保留正确的 SNI、Host 与证书主机名校验。
网络出口策略是另一道限制:即使应用解析逻辑出现遗漏,抓取服务也不应访问数据库管理端、实例元数据或控制平面。固定域名的合作方配置同样需要受控 DNS 和网络权限。完整讨论见 OWASP SSRF 防护。
响应头到达后还要限制正文
连接超时只限制建立连接。HTTP 响应头先到达时,正文仍可能无限缓慢或持续增长;使用 InputStream 接收后再同步读取,不能未经核对就认为原来的请求超时覆盖后续全部读取。
PartnerClient 使用有界 BodySubscriber,累计超过 4096 字节时取消订阅;异步交换最多等待 3 秒,超时或线程中断时取消请求,重定向响应按非 200 拒绝。连接上限为 2 秒,且 ProxySelector 显式返回 NO_PROXY。API 行为见 Java 17 HttpClient。
这些限制保护单次交换。生产还需要总并发、每目标并发、队列长度和连接池预算;请求取消也不能保证远端已经停止它开始执行的业务写入,所以可重试的出站写请求需要幂等设计。
按解释器和副作用定位问题
正常输入被拒绝
先确定拒绝来自业务校验、编码前处理、解析器限制还是下游响应。姓名中的引号、合法 Unicode 和普通文件名字符不应通过删除字符来“修复”。为合法输入添加明确用例,确认数据库值、页面文字与下载名称保持业务含义。
HTML 出现 < 或页面显示实体编码文本时,检查是否重复编码。数据通常按原始业务值保存,在最终输出上下文处理;已经清洗过的富文本需要明确类型和专用输出路径,不与普通文本字段混用。
返回失败却产生了文件或请求
文件处理检查临时目录、目标对象和清理顺序,出站请求检查真正被访问的目标端。只读取一条“被拒绝”日志,会漏掉校验之前已发出的请求或已写入的文件。
本地重定向实验使用目的服务器计数,可以直接判断有没有跟随;生产中可结合受控代理访问记录和流量日志,但应避免记录完整敏感 URL、请求体或凭证。修复后复测原始允许路径与被禁止目标:受信合作方仍可访问,越界目标继续被拒绝。
参数很小,资源仍然耗尽
查看解析深度、解压后大小、查询扫描量、响应正文速度和并发队列。给输入设置长度上限,无法限制一个小压缩包展开后的体积;给连接设置超时,也无法替代对完整处理过程的限制。
把限制放到实际消耗资源的环节,再验证超过限制时的取消、清理和错误响应。对于 XML、图片转换或归档处理等高风险解析任务,可进一步使用独立进程、较小权限和操作系统资源约束。
实验容器都使用 --rm,测试会停止回环服务器并清理临时目录。target/encoded-input.html 是可重建输出,确认不再使用后可删除;不涉及数据库卷或长期运行服务。
权威资料与规范地址
OWASP SQL 注入防护:https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
OWASP XSS 防护:https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
OWASP 命令注入防护:https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
Java 17 Serialization Filtering:https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html
OWASP XXE 防护:https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
OWASP Java Encoder:https://owasp.org/www-project-java-encoder/
OWASP 文件上传:https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
OWASP SSRF 防护:https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
Java 17 HttpClient:https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpClient.html
