← 中文资源

简体中文版本

输出编码

输出编码在插值点把不可信数据转换成该输出上下文的安全表示——HTML 体、属性、JS 字符串、URL——使其无法被当作标记或代码解析。

阅读英文原文

概念定义

注入的代码能执行,是因为输出解析器分不清数据与语法。编码把数据字节改写成解析后仍读回原字符的形式——尖括号变实体、引号变转义——让数据在每个上下文里都保持数据身份。

工作机制

上下文决定要转义的字符集: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, "&lt;script&gt;") {
		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));
}