输出编码与转义
输出编码防止XSS攻击,根据输出上下文选择正确的编码方式。
编码原理
输出编码核心原则:根据输出位置选择对应编码方式。
| 输出上下文 | 编码方式 | 示例 |
|---|---|---|
| HTML内容 | HTML实体编码 | < → < |
| HTML属性 | HTML属性编码 | " → " |
| JavaScript | JavaScript转义 | " → \" |
| URL | URL编码 | 空格 → %20 |
| CSS | CSS转义 | 特殊字符转义 |
HTML编码
HTML内容编码
JavaScript
function escapeHtml(str) {
const htmlEntities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return String(str).replace(/[&<>"'/]/g, c => htmlEntities[c]);
}
// 使用示例
const userInput = '<script>alert("xss")</script>';
const safe = escapeHtml(userInput);
// 输出: <script>alert("xss")</script>
HTML属性编码
JavaScript
function escapeHtmlAttribute(str) {
// 属性值必须用引号包裹
return escapeHtml(str);
}
// 安全使用
// <div data-value="${escapeHtmlAttribute(value)}">
重要:HTML属性值必须用引号包裹,否则编码无效。
HTML
<!-- 危险:无引号属性 -->
<div data-value=${userInput}>
<!-- 安全:引号包裹 -->
<div data-value="${escapeHtmlAttribute(userInput)}">
安全HTML生成
JavaScript
// 使用DOM API自动编码
function safeSetText(element, text) {
element.textContent = text; // 自动编码
}
function safeSetAttribute(element, attr, value) {
element.setAttribute(attr, value); // 浏览器自动编码属性
}
// 示例
const div = document.getElementById('output');
safeSetText(div, userInput); // 安全
JavaScript编码
字符串字面量转义
JavaScript
function escapeJsString(str) {
const jsEscapes = {
'\\': '\\\\',
'"': '\\"',
"'": "\\'",
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'<': '\\x3C', // 防止</script>攻击
'>': '\\x3E',
'&': '\\x26'
};
return String(str).replace(/[\\'" \n\r\t<>]/g, c => jsEscapes[c] || c);
}
// 使用示例
// <script>var msg = "${escapeJsString(userInput)}";</script>
JSON编码
JavaScript
// 安全方式:使用JSON.stringify
function safeJsonEncode(value) {
return JSON.stringify(value);
}
// 使用示例
// <script>
// var config = ${JSON.stringify(userConfig)};
// </script>
DOM中的JavaScript
JavaScript
// 危险:直接拼接
// <button onclick="handleClick('${userInput}')">
// 安全:使用data属性
// <button data-action="${escapeHtmlAttribute(action)}"
// data-param="${escapeHtmlAttribute(param)}"
// onclick="handleClick(this.dataset)">
function handleClick(dataset) {
const { action, param } = dataset;
// 安全使用
}
URL编码
URL组件编码
JavaScript
// 完整编码(用于URL参数值)
function encodeUrlParam(value) {
return encodeURIComponent(value);
}
// 解码
function decodeUrlParam(value) {
return decodeURIComponent(value);
}
// 使用示例
const url = `/search?q=${encodeURIComponent(query)}`;
const redirect = `https://example.com/callback?next=${encodeURIComponent(path)}`;
URL路径编码
JavaScript
// 路径编码(保留/符号)
function encodeUrlPath(path) {
return encodeURI(path);
}
// 示例
const path = '/users/' + encodeURI(username);
安全URL验证
JavaScript
function safeUrl(url, allowedOrigins = []) {
try {
const parsed = new URL(url, window.location.origin);
// 只允许特定协议
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('仅允许HTTP/HTTPS协议');
}
// 白名单域名检查(防止开放重定向)
if (allowedOrigins.length > 0) {
if (!allowedOrigins.includes(parsed.origin)) {
throw new Error('不允许的域名');
}
}
return parsed.href;
} catch (error) {
throw new Error('无效URL');
}
}
// 使用示例
const allowedOrigins = ['https://example.com', 'https://api.example.com'];
const safe = safeUrl(userInput, allowedOrigins);
CSS编码
CSS字符串转义
JavaScript
function escapeCssString(str) {
return String(str).replace(/[<>"'\\]/g, c => {
return '\\' + c.charCodeAt(0).toString(16) + ' ';
});
}
// 使用示例
// <style>
// .user-class::after { content: "${escapeCssString(userContent)}"; }
// </style>
CSS选择器转义
JavaScript
function escapeCssSelector(str) {
// CSS.escape是标准API
if (typeof CSS !== 'undefined' && CSS.escape) {
return CSS.escape(str);
}
// Polyfill
return String(str).replace(/([^\w-])/g, (match, c) => {
return '\\' + c;
});
}
// 使用示例
const userClass = escapeCssSelector(userInput);
document.querySelector(`.${userClass}`);
各上下文编码对照
JavaScript
const encoders = {
// HTML上下文
htmlContent: escapeHtml,
htmlAttribute: escapeHtml,
// JavaScript上下文
jsString: escapeJsString,
jsData: (v) => JSON.stringify(v),
// URL上下文
urlParam: encodeURIComponent,
urlPath: encodeURI,
// CSS上下文
cssString: escapeCssString,
cssSelector: (v) => CSS ? CSS.escape(v) : v
};
// 根据上下文选择编码器
function encode(value, context) {
const encoder = encoders[context];
if (!encoder) {
throw new Error(`未知上下文: ${context}`);
}
return encoder(value);
}
模板引擎集成
安全模板函数
JavaScript
function safeHtml(strings, ...values) {
let result = strings[0];
for (let i = 0; i < values.length; i++) {
// 自动编码
result += escapeHtml(values[i]) + strings[i + 1];
}
return result;
}
// 使用示例
const name = '<script>alert(1)</script>';
const output = safeHtml`<div>Hello, ${name}!</div>`;
// <div>Hello, <script>alert(1)</script>!</div>
React自动转义
jsx
// React默认转义
function UserGreeting({ name }) {
return <div>Hello, {name}</div>; // 自动HTML编码
}
// 危险:dangerouslySetInnerHTML绕过保护
function UserContent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />; // 不安全
}
// 安全:先净化再渲染
function SafeHtml({ html }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
编码时机
text
输入 → 验证 → 存储 → 读取 → 输出编码 → 显示
核心原则:存储原始数据,输出时编码。不要在存储前编码,会导致二次编码问题。
要点总结
| 上下文 | 编码函数 | 关键字符 |
|---|---|---|
| HTML内容 | escapeHtml() | < > & " ' |
| HTML属性 | escapeHtml() | 同上(必须引号包裹) |
| JS字符串 | escapeJsString() | \ " ' < > & |
| JS数据 | JSON.stringify() | 自动处理 |
| URL参数 | encodeURIComponent() | 特殊字符 |
| URL路径 | encodeURI() | 保留/ |
| CSS | CSS.escape() | 选择器特殊字符 |
最佳实践:
- 根据输出位置选择编码方式
- 使用框架内置转义功能
- 存储原始数据,输出时编码
- 复杂场景使用DOMPurify等库
- 避免内联事件处理器
📝 发现内容有误?点击此处直接编辑