概念定义
注入的代码能执行,是因为输出解析器分不清数据与语法。编码把数据字节改写成解析后仍读回原字符的形式——尖括号变实体、引号变转义——让数据在每个上下文里都保持数据身份。
工作机制
上下文决定要转义的字符集:HTML 体的小于号、属性的引号、JS 字符串的反斜杠与闭合标签、URL 属性的冒号斜杠。模板引擎提供按上下文的自动转义;raw 插值逃生舱是缺陷聚集地。script 标签里的 JSON 除字符串转义外还要防闭合标签序列。
使用场景
一切不可信或半可信数据的插值。优先用带上下文感知自动转义的引擎,尽量少用 raw 模式并在评审中当类来审查。textContent 与属性赋值这类 DOM API 天然按构造编码。
常见误区
输入端一次性编码不够——同一份数据可能先后渲染进体、属性与脚本,各上下文的逃逸集不同。净化与编码不可互换:净化是从 HTML 输入里删危险标记,编码是让任何数据在输出时失活。统一转义函数到处用会在属性与脚本上下文失效或破坏内容;双重编码还会腐蚀显示。富 HTML 输入要另行用白名单库净化。
代码示例
package main
import (
"bytes"
"fmt"
"html/template"
"strings"
)
type Comment struct {
Author string
Body string
Link string
}
func main() {
page := template.Must(template.New("comment").Parse(
`<article><h2>{{.Author}}</h2><p>{{.Body}}</p><a href="{{.Link}}">profile</a></article>`,
))
comment := Comment{
Author: "<b>Mallory</b>",
Body: `<script>alert("x")</script>`,
Link: "javascript:alert(1)",
}
var output bytes.Buffer
if err := page.Execute(&output, comment); err != nil {
panic(err)
}
rendered := output.String()
fmt.Println(rendered)
if strings.Contains(rendered, "<script>") {
panic("script tag was not escaped")
}
if !strings.Contains(rendered, "<script>") {
panic("expected escaped body text")
}
if !strings.Contains(rendered, "#ZgotmplZ") {
panic("expected dangerous URL to be blocked")
}
}
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
export function base32Encode(input) {
if (typeof input !== "string") throw new TypeError("input must be a string");
const bytes = new TextEncoder().encode(input);
let bits = 0;
let value = 0;
let output = "";
for (const byte of bytes) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 5) {
output += ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) output += ALPHABET[(value << (5 - bits)) & 31];
while (output.length % 8 !== 0) output += "=";
return output;
}
export function base32Decode(input) {
if (typeof input !== "string") throw new TypeError("input must be a string");
const normalized = input.toUpperCase().replace(/=+$/, "");
if (!/^[A-Z2-7]*$/.test(normalized)) {
throw new RangeError("invalid base32 characters");
}
let bits = 0;
let value = 0;
const bytes = [];
for (const char of normalized) {
const index = ALPHABET.indexOf(char);
value = (value << 5) | index;
bits += 5;
if (bits >= 8) {
bytes.push((value >>> (bits - 8)) & 255);
bits -= 8;
}
}
return new TextDecoder().decode(new Uint8Array(bytes));
}