JSP 前端 XSS 防范:企业最容易漏掉的六个编码细节
日期 : 2026-11-10 12:39:32
大多数 JSP 项目的 XSS 漏洞,不是因为没做防护,而是因为防护做在了错误的位置、用了错误的编码方式。页面能正常显示,功能验收通过,问题就一直躺在那里,直到某天被人提交到漏洞平台。
这篇文章不重复"要过滤特殊字符"这类结论,只讲六个具体到代码行的编码细节。
01 漏洞的起点,往往只有一行
JSP 最方便的地方,也是它最危险的地方:Java 代码和 HTML 处在同一个文件里。
<%= request.getParameter("q") %> 这样一行,就把 URL 里的内容原封不动写进了页面。访问 ?q=<img src=x onerror=alert(1)>,浏览器会老实执行它——因为浏览器分不清哪一段是开发者写的代码、哪一段是用户提供的数据。这就是反射型 XSS 的最小复现。
更能说明问题的是存储型。工单标题、商品评价、留言、后台备注,这些内容会被保存、被列表页输出、被管理员打开。一条带脚本的工单,等于给所有看到它的人埋了一次执行机会,包括权限更高的运营和客服。企业常觉得"系统在内网、用户都是员工",但真正被打穿的往往就是这类后台。
02 转义函数再完善,也管不到"上下文"
很多团队有自研的网站设计 escapeHtml,把尖括号换成实体就认为安全了。问题在于,同一个值出现在页面的不同位置,需要的编码方式并不相同。
属性值是第一个破口。写成 <input value=<%= name %>> 而不加引号时,攻击者只要输入一个空格加 onmouseover=,就能在标签里凭空加一个属性;加了引号,编码则必须覆盖单双引号,只换尖括号没有用。所以属性的正确姿势是:始终加引号,并且用属性专用的编码方法。
第二个破口是链接。href 里塞进 javascript:alert(1),整串没有一个字符需要转义,任何基于特殊字符的过滤都会放行。这个位置真正该做的是协议白名单,只允许 http 和 https,而不是再加一层转义。
第三个破口是脚本块。<script> 内部的 HTML 实体不会被浏览器解码,所以把单引号转成实体再拼进 var a = '<%= x %>';,既改坏了变量值——用户看到的是一串实体编码,也没有按 JS 语法边界形成保护。这段内容需要的是 JavaScript 字符串编码,或者干脆整体走 JSON 序列化。
这三点指向同一件事:没有"通用的转义函数",只有"针对某个位置的编码方法"。OWASP 把这件事叫作上下文输出编码,其 Java Encoder 项目就直接按上下文拆成了 forHtml、forHtmlAttribute、forJavaScript、forUriComponent、forCssString 等不同方法。


03 三种输出方式,默认行为并不一致
JSP 页面里能输出值的写法有三种,默认行为完全不同,而团队往往以为它们一样安全。
- <%= %> 是脚本表达式,原样输出,不做任何转义,安全性完全取决于写代码的人。
- ${} 形式的 EL 表达式在页面上同样是原样输出。这是最容易被误判的一处:不少人以为换掉脚本表达式就自动安全了,实际上它一样需要手工编码。
- <c:out> 才默认开启转义。按 Jakarta Tags 规范的定义,它的 escapeXml 默认为 true,会把 <、>、&、单引号、双引号转成对应的字符实体。
反过来说,全站最该被 review 的一行代码,是 escapeXml="false"。它通常出现在"这里要显示富文本"的需求里,一旦加上,这个输出点就退回零防护,而从页面上看不出任何异常。
04 编码做在入口,会留下三笔账
第二类问题来自时机。不少项目的做法是在 Controller 或 Filter 里统一"清理"参数,然后入库。这个顺序会留下三笔很难还的账。
- 双重编码。已经被转成实体的内容存进了数据库,导出 Excel、对接第三方接口、给小程序提供 API 时读出来还是实体串,前端再转一次,页面上就出现 < 这样的乱码。业务方为了"修显示",往往再加一次反转义——等于把当初的防护原样退了回去。
- 数据污染。原始内容一旦被改写,检索、统计、对外同步全部跟着错,而且很难回溯到底是哪一层改的。
- 绕过。入口的黑名单总能被变形绕过:大小写、编码、非常规标签、浏览器容错解析。而输出点的编码是按上下文做的确定性处理,不存在"猜攻击者手法"这回事。
正确的顺序是:入口只做校验和规范化——长度、格式、类型、白名单;编码一律放在最终输出点,并且同一个值只编码一次。
还有一个几乎没人注意的实现细节:自研转义函数的替换顺序。必须先替换 &,再替换尖括号和引号。顺序反了,后面替换产生的 & 不会再被处理,攻击者可以用一段精心构造的实体串穿过去。这类 bug 在代码里看不出来,只会表现为"某些特殊输入下页面渲染异常"。
05 URL、脚本、样式各有一套入口
同一个用户输入,可能同时出现在 URL 参数、脚本字符串、样式值、JSONP 回调名里,每个位置的"正确编码"都不一样。下面四种是实际项目中最常漏的。
- URL 参数。用 forUriComponent 做百分号编码。要注意它只编码"组件",不负责校验整条地址是否合法,所以跳转类参数还必须额外做协议白名单。
- JavaScript。用 forJavaScript,或者把数据整体序列化成 JSON 再在页面里解析,避免任何形式的字符串拼接。
- CSS。style 属性里能被塞进 expression() 或 url(),两者都能执行代码。最省事的做法是不让用户控制样式;确实需要时,用颜色值之类的强白名单加 forCssString,而不是相信"CSS 不会被利用"。
- JSONP 回调名。只允许字母、数字、下划线和点,其余一律拒绝。它出现在函数名位置,一旦放开字符集,就等于开了一个现成的脚本注入点。
06 富文本要净化,不是转义
需要保留用户提交的 HTML 时——比如富文本编辑器、公告正文、商品详情——转义和净化是两条完全不同的路。转义会把标签也变成可见文字,页面直接坏掉;净化则是按白名单保留允许的标签和属性,删掉其余部分。
这一步必须用专门的 HTML Sanitizer,例如 OWASP Java HTML Sanitizer,不要用正则拼。白名单要盯住三类东西:所有 on 开头的事件属性、危险协议(javascript:、data:、vbscript:)、以及能承载脚本的标签(script、iframe、object)。
另外提醒一点:服务端净化之后,如果前端用 innerHTML 或 document.write 再把用户数据拼一次,这条防线会当场失效。净化必须和前端的使用方式一起约定,否则只是把洞挪了个位置。
07 正文之外的三处,长期无人检查
XSS 不总发生在页面正文里。下面三处,代码审查时几乎不会被翻到。
- 响应头。缺少完整的 Content-Type 与字符集声明、又没带 X-Content-Type-Options: nosniff 时,浏览器可能按内容嗅探决定类型,一个被上传成"图片"的 HTML 文件就能被当成页面执行。
- 下载接口。附件下载要固定返回 Content-Disposition: attachment,上传的文件名和扩展名做白名单,别让附件落在可直接访问的 Web 目录里。
- 错误页。把异常信息、堆栈或请求参数回显到错误页,等于把注入面开在最没人看的地方;Tomcat 的默认错误页本身就会带出异常消息。
CSP 可以作为纵深防御,但前提是不写 unsafe-inline,否则它只是让配置看起来存在。
08 把它变成一次代码审查

上面六条落到执行层面,可以压缩成一份能在半天内跑完的清单:
- 全局搜 <%= 和 ${},逐个确认对应输出点做了上下文编码。
- 搜 escapeXml="false",每一个都要有明确理由和责任人。
- 检查属性值是否都带引号,编码方法是否覆盖单双引号。
- 检查 href、src、跳转参数是否有协议白名单。
- 确认编码只在输出点做一次,且 & 的替换排在尖括号之前。
- 确认富文本走的是 sanitizer 而非转义,并检查前端有没有二次拼接。
这些细节之所以容易被忽视,是因为它们不在功能验收清单里。页面显示正常、流程走得通,就没人会回头去看那个引号加没加。XSS 的修复成本很低,发现成本却很高——通常是被报告之后,才第一次被认真看一眼。
上一篇:5 步打造爆款网站|别再只做一个 “线上展板”
下一篇:没有了
相关文章



精彩导读




热门资讯