本文目录导读:

- 输出到 HTML 标签内容(最常见)
- 输出到 HTML 标签属性(非常危险)
- 输出到 JavaScript 上下文(高风险)
- 输出到 CSS 上下文
- 输出到 URL 上下文(
href,src,action) - 输出转义的最佳实践
输出转义(Output Escaping)是防御XSS(跨站脚本攻击)最核心、最基础的手段,其核心思想是:将用户输入的、不可信的数据,在输出到HTML、JavaScript、CSS等不同上下文时,转换成该上下文无法被“解释”为代码的格式。
简单说,就是把特殊字符(如 <, >, , , &)替换成它们的HTML实体编码(如 <, >, ", ', &),这样浏览器就会把它们当作普通文本显示,而不是执行。
输出转义必须根据输出位置(上下文)进行,否则会无效甚至产生新的漏洞,下面分场景详细说明如何正确规避XSS:
输出到 HTML 标签内容(最常见)
这是最经典的情况,比如用户输入显示在 <div>、<p>、<span> 等标签的文本内容中。
错误的做法: 直接输出用户输入,不进行任何转义。
正确的做法: 对 &、<、>、、 这五个字符进行转义。
&→&<→<>→>- →
" - →
'(或')
示例(PHP):
// 用户输入
$user_input = '<script>alert("XSS")</script>';
// 正确输出(使用htmlspecialchars函数)
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
// 输出:<script>alert("XSS")</script>
// 浏览器显示:<script>alert("XSS")</script> (作为文本,不会执行)
不同语言的常见函数:
| 语言 | 函数/方法 | 说明 |
|---|---|---|
| PHP | htmlspecialchars() |
默认转义 4 个字符,可配置转义单引号(推荐使用 ENT_QUOTES) |
| Java (JSP) | StringEscapeUtils.escapeHtml4() |
Apache Commons Lang 库 |
| JavaScript (浏览器端) | textContent 或 innerText |
直接设置文本内容,无需手动转义 |
| Python (Django) | django.utils.html.escape() |
自动转义 |
| Python (Flask) | 模板引擎 (Jinja2) 自动转义 | 默认开启自动HTML转义 |
| Ruby on Rails | h() 或 html_escape() |
模板中的 h(user_input) |
| .NET | System.Web.HttpUtility.HtmlEncode() |
或 Razor 视图中的 符号(默认编码) |
输出到 HTML 标签属性(非常危险)
属性值中的特殊字符会被浏览器截断,导致注入。<img src=" 后跟用户输入,用户输入为 " onerror="alert(1)",结果变为 <img src="x" onerror="alert(1)">。
错误的做法: 直接拼接用户输入到属性值中,且不转义引号。
正确的做法:
- 一定要使用引号包围属性值(单引号或双引号)。
- 转义属性值中的引号和 & 符号。
- 对于双引号属性:转义 ,
&,<,>。 - 对于单引号属性:转义 ,
&,<,>。
- 对于双引号属性:转义 ,
示例(Java):
// 用户输入
String userInput = "onerror='alert(1)'";
// 正确做法:使用双引号包围,并转义双引号
String safeOutput = "<img src=\"placeholder.png\" alt=\"" +
StringEscapeUtils.escapeHtml4(userInput) + "\" />";
// 输出:<img src="placeholder.png" alt="onerror='alert(1)'" />
// 其中单引号没有被转义? 等等,这里有个坑!
关键点: 上面例子中,escapeHtml4() 默认转义 但不转义 ,如果属性值是用单引号包围的,就会有问题,所以最佳实践是:
- 统一使用双引号包围属性值。
- 使用专门针对属性的转义函数,它们会转义 , ,
<,>,&。 - 或者将用户输入限制为只允许安全的URL/值,而不是直接放在alt、title等属性中。
更安全的做法: 对于 src、href、on* 等事件属性,严禁直接插入用户输入,需要使用白名单机制(如只允许 http:// 或 https:// 开头的URL),并进一步使用 encodeURI() 或 urlencode()。
输出到 JavaScript 上下文(高风险)
将用户输入直接嵌入到 <script> 标签或事件处理函数(如 onclick)的字符串中。
错误的做法: 使用字符串拼接。
// 错误!
var name = "<%= userInput %>"; // 如果userInput包含 </script> 或 ",会导致语法错误或注入
document.write("Hello " + name);
正确的做法:
- 永远不要直接在 JavaScript 中嵌入 HTML 数据,最好的方法是只传递数据,不在服务器端拼接 JS 代码。
- 如果必须传递,需要对数据进行 JavaScript 转义:
- 转义 (反斜杠)
- 转义 和 (引号)
- 转义 (斜杠)
- 转义
\n,\r(换行符) - 最重要:转义
\x00到\x1F的所有控制字符(特别是换行符,防止注入) - 转义
<和>(防止直接注入HTML)
- 推荐使用 JSON 编码,然后在JS端解析,几乎所有语言都有
json_encode函数,它会自动处理好所有转义。
示例(PHP + JavaScript):
$userInput = "hello'; alert('xss'); //";
$jsSafeData = json_encode($userInput);
// 结果为: "hello'; alert('xss'); //" (注意外层的双引号)
?>
<script>
var name = <?php echo $jsSafeData; ?>; // 现在name是一个字符串变量
console.log(name);
</script>
输出到 CSS 上下文
用户输入出现在 CSS 中(style 属性或 <style> 标签内)。
核心原则: 几乎不可能安全地转义用户输入放入 CSS 中,因为 CSS 可以引入URL(如 background: url(...))或执行表达式(旧版IE)。
唯一安全做法: 使用白名单,只允许用户选择预设的颜色(如 'red', 'blue'),或者使用 class 名称,让用户选择预定义的 class,而不是直接输入颜色值或尺寸。
如果必须动态生成颜色值(如十六进制颜色):
- 使用正则严格验证:只允许
#[0-9a-fA-F]{3,6}这样的格式。 - 绝对不要允许用户输入URL或任何函数调用。
输出到 URL 上下文(href, src, action)
错误的做法: 直接拼接用户输入到 href="user_input"。
正确的做法:
- URL 编码:对用户输入进行
encodeURIComponent()或urlencode()。- 这会将特殊字符(如 ,
&, ,空格)转换为 开头的编码。
- 这会将特殊字符(如 ,
- 协议白名单:强制使用
http://或https://,否则用户输入javascript:alert(1)或data:text/html,...可以执行代码。- 检查 URL 是否以
http://或https://开头。 - 或者使用
parse_url()分解后,只允许特定协议。
- 检查 URL 是否以
示例(Python):
import urllib.parse
user_input = "javascript:alert(1)"
# 1. 协议检查
if user_input.startswith(('http://', 'https://')):
# 2. 对URL参数进行编码(实际是对整个URL编码,但需保留分隔符)
safe_url = urllib.parse.quote(user_input, safe=':/?&=')
print(f'<a href="{safe_url}">Link</a>')
else:
# 拒绝或使用默认链接
print('<a href="/default">Link</a>')
输出转义的最佳实践
- 理解上下文:输出到不同的地方(HTML内容、属性、JS、CSS、URL),转义规则完全不同。没有一种万能的转义函数。
- 使用成熟的库:不要自己写转义函数,使用语言或框架内置的、经过安全审查的函数(如
htmlspecialchars,json_encode,StringEscapeUtils等)。 - 原则优先:
- 输出到 HTML 内容:对
<,>,&, , 进行 HTML 实体编码。 - 输出到 HTML 属性:确保属性值被引号包围,并使用针对属性的 HTML 编码。
- 输出到 JavaScript:使用
JSON.stringify(或json_encode) 转义,并避免直接字符串拼接。 - 输出到 URL:使用 URL 编码,并强制协议白名单。
- 输出到 CSS:不要直接输出用户输入,使用白名单。
- 输出到 HTML 内容:对
- 数据流审查:在整个应用的数据流中,确保输入验证和输出转义是分层的,不依赖单一防御。
- 开启自动转义:大多数现代模板引擎(如 Django Templates, Jinja2, Razor, React (JSX))默认开启 HTML 内容自动转义。不要轻易关闭它,但也要注意,自动转义只针对 HTML 内容,如果数据被放到
<script>标签、属性、CSS 中,自动转义可能不够,需要手动处理。
最终检查清单:
- [ ] 我是否知道这个数据最终会出现在哪个上下文中?
- [ ] 我是否使用了针对该上下文的正确转义函数?
- [ ] 我是否验证了数据格式(如URL、数字、颜色值)?
- [ ] 我是否避免了将用户输入直接放入
<script>、<style>或事件处理属性中? - [ ] 我是否考虑了编码的层级(比如数据库、页面、JS之间的编码一致)?