前端安全边界

一、导读

1 怎么读这篇笔记

定义: 先讲「威胁从哪里来、浏览器默认帮你挡了什么」,再讲你该怎么写代码才不拆墙
代码块 // 语法要点//正确示例//错误示例;安全话题也要能落地到代码,而不是口号集合。
啥时候用 每个小节后列「联调、上线、接入第三方脚本」常见触点。
与前文的关系 你在《前端网络层》里写过 Cookie 与 CORS;在《浏览器存储》里写过 HttpOnly、SameSite;本篇把它们放进统一威胁模型里读,而不是三条孤立知识点。

目标:能说清 XSS / CSRF / 点击劫持 / 敏感信息落点 四类主线的原理边界;能在评审里指出 「这句 innerHTML 为什么危险、这个登录态为什么不该进 localStorage、这段 CORS 配置到底方便了谁」
免责声明:安全是系统级问题;前端守住的是输入输出与最小暴露面任何「靠前端加密密钥」的方案都不成立——因为浏览器里的字符串用户都能看见。


2 威胁建模速记:STRIDE(知道一张表就够)

定义:STRIDE 是微软经典威胁分类法,用以穷举思考攻击面(不是前端独舞,但产品评审时很好用):

字母 含义 前端常触点
S Spoofing 伪装 冒充用户/站点 会话窃取、钓鱼页 UI
T Tampering 篡改 改数据、改包 无完整性校验的静态资源、被劫持 CDN
R Repudiation 抵赖 否认操作 审计日志应在服务端
I Information Disclosure 泄露 不该看的被看了 XSS、Referer、错误栈上传
D Denial of Service 拒绝服务 拖垮可用性 主线程炸弹、无限弹窗(体验/可用性)
E Elevation of Privilege 提权 低权限变高权限 越权接口、IDOR

本篇展开每一条的攻防全书,但你在 PR 描述里写一句「此项改动影响 STRIDE 的 I/T」——评审档次立刻不一样


二、先搭「威胁模型」:我们在防谁

1 资产、威胁、暴露面

定义:

  • 资产 — 用户数据、会话、钱、声誉。
  • 威胁 — 窃取会话、伪造请求、钓鱼、植入恶意脚本、拖库(多在后端)。
  • 暴露面 — 浏览器能读写的所有入口:HTML 拼接、postMessage、URL 参数、第三方脚本、存储 API。
    前端的目标不是「绝对不被黑」,而是提高攻击成本、避免低级失误把大门敞开。
// 语法要点:凡是「把不可信字符串塞进能执行的环境」都是高危

//错误示例
element.innerHTML = userInput;

//正确示例——先问:这是纯文本还是富文本?纯文本就用 textContent
element.textContent = userInput;

啥时候用

  • 设计阶段:先列数据从哪来、到哪去——再选渲染方式。

2 「同源策略」与「不是万能的」

定义:同源策略限制不同源的页面读对方 DOM / 默认不读响应体——所以才有 CORS
但它挡不住

  • 同源下的 XSS(脚本已经“成为页面的一部分”)。
  • CSRF(浏览器会自动带 Cookie)。
  • 点击劫持(视觉欺骗)。
  • 供应链投毒(你主动 import 了坏包)。
// 语法要点:CORS 是放松读权限,不是「鉴权」

//错误示例——以为「开 CORS」就等于安全接口
// Access-Control-Allow-Origin: * 只会让浏览器允许页面读响应;攻击者自有办法从用户浏览器发请求

//正确示例——身份校验仍靠 Cookie + CSRF 防御 / Token + 正确存储策略,或后端会话体系

啥时候用

  • 评审后端 CORS — 问:我们到底允许哪些读?有没有 credentials

三、XSS:跨站脚本与「可执行上下文」

1 反射型 / 存储型 / DOM 型(记语义)

定义(简化教学版):

  • 反射型 — 恶意输入立刻从 URL 等弹回页面(常用于钓鱼链接)。
  • 存储型 — 恶意内容进了数据库,每次打开页面都会执行。
  • DOM 型 — 纯前端路由把不可信数据写进 DOM / eval不经由后端存储也能出事。
    共同点:不信任的数据进了可执行或可被解析为 HTML 的通道。
// 语法要点:危险的「sink」= innerHTML、outerHTML、insertAdjacentHTML、document.write、eval、new Function、setTimeout(字符串)、URL 的 javascript: 协议 href

//错误示例
div.innerHTML = `<p>${name}</p>`; // name 含 <img src=x onerror=...> 即炸

//正确示例——模板引擎默认转义 + CSP

啥时候用

  • 评论、昵称、搜索词回显 — 预设它就是坏的。

2 输出编码与「上下文相关」

定义:HTML 转义不等价于 JavaScript 字符串转义、也不等价于 URL 编码。在 HTML 文本节点里要 & < > ";在 属性里要注意引号闭合;在 javascript: URL里几乎一切都是毒。

输出上下文 典型坑 原则性做法
HTML 文本节点 < 触发标签 textContent 或模板引擎 HTML-escape
HTML 属性值(双引号) "... onload=..." 断引号 属性实体转义 + 能用布尔/严格枚举就别字符串裸插
URLhref/src javascript:、data:text/html http(s) 白名单 + URL API 解析;禁止 javascript:
CSSstyle/style 属性) expression()-moz-binding(历史) 尽量不动态拼完整样式字符串;用类名切换
JSON<script type="application/json"> </script> 断出有脚本 按 JSON 规则转义并由后端生成;切勿字符串拼接

一条总原则:任何「把不可信数据」写进能产生执行语义的位置,都是高危 sink先定上下文,再谈转义表——不要幻想「统一 escapeHtml 一把梭」。

//正确示例——把用户输入当作**文本节点**
const p = document.createElement('p');
p.textContent = userName;
root.append(p);

//错误示例——模板字符串拼 href
a.href = `javascript:alert(1)`; // 若 user 可控,灾难

//正确示例——http(s) 白名单校验后再赋值
function safeHref(url) {
  try {
    const u = new URL(url, location.href);
    if (u.protocol === 'https:' || u.protocol === 'http:') return u.href;
  } catch {
    /* ignore */
  }
  return 'about:blank';
}

啥时候用

  • 富文本编辑器 — 需要 HTML 消毒(服务器侧为主,客户端可第二层),而不是「禁止用户输 <」这种鸵鸟策略。

3 CSP:内容安全策略(从「补洞」到「限权」)

定义:Content-Security-Policy 响应头告诉浏览器:哪些源可以执行脚本、加载图片、连接接口。典型:

  • default-src 'self'
  • script-src 'self'(拒绝内联脚本,除非 nonce / hash
  • object-src 'none'
  • base-uri 'self'
  • frame-ancestors 'none' 或具体列表(防嵌套点击劫持,与 X-Frame-Options 协同)
// 语法要点(响应头示意,不是 JS)
Content-Security-Policy: default-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self';
//正确示例——静态站点先上「报告模式」观察误拦
// Content-Security-Policy-Report-Only: ...

//错误示例——script-src 写 unsafe-inline 还自以为「上了 CSP」
// 内联脚本仍可执行,XSS 仍快乐

啥时候用

  • 上线前 — 与后端协同下发 CSP;先从 Report-Only 收集误杀

4 upgrade-insecure-requestsblock-all-mixed-content

定义:在「暂时还有个别 http 子资源」的迁移期,可用 CSP:

  • upgrade-insecure-requests:自动把 http:// 资源请求升级https://(若服务端不存在会 404,但避免混内容被动攻击面)。
  • block-all-mixed-content(或等价策略):直接拦混合内容(更硬)。

精确注意:这不替代你在资源 URL 上修正确链接;只是给遗漏一条安全网。

Content-Security-Policy: upgrade-insecure-requests; default-src https: 'unsafe-inline'

啥时候用

  • HTTPS 改造中期;稳定后应以资源链接全 HTTPS 为目标,而不是长期依赖升级指令。

5 Trusted Types(信任类型):从 API 上消灭 innerHTML 泥沼

定义:Trusted Types 配合 CSP require-trusted-types-for 'script' 等指令,要求某些 DOM XSS sink 只接受「已审核」的 TrustedHTML / TrustedScript 对象,而不是裸字符串。
生产采用需要构建链与模板层支持(如封装 policy.createHTML)。中小团队可以先把「禁止手写 innerHTML」写进 eslint 规则,再评估 Trusted Types。

// 语法要点(概念演示,实际需 policy 注册)
// const policy = trustedTypes.createPolicy('default', { createHTML: (s) => DOMPurify.sanitize(s) });
// el.innerHTML = policy.createHTML(userHtml);

//错误示例——以为 CSP 一条 default-src 就万事大吉,却对 sink 毫无约束

啥时候用

  • 大型应用、强合规行业;与 DOMPurify + 服务端消毒 组合。

实操建议

  • 先用 lint 规则禁止直接调用 innerHTML/outerHTML/insertAdjacentHTML 等高危 API,把风险点在代码审查阶段暴露出来。
  • 在渲染链路中引入 policy.createHTML(...) 的封装层,默认走服务端清洗 + 客户端 DOMPurify 双重消毒;逐步评估启用 Trusted Types 的成本与兼容性。
  • 把典型 XSS payload 写成集成测试(回归测试),在 CI 中运行以防止未来改动意外打开 sink。

小结:把策略变成 CI/编码规则与测试用例,比单纯靠文档/会议更能长期管控风险。


四、CSRF:跨站请求伪造与「浏览器的「好心」」

1 它到底利用了啥

定义:已登录用户打开攻击站点;攻击站点让浏览器自动你的域发请求——Cookie 通常自动带上(视 SameSite)。若接口仅依赖 Cookie、且无不可伪造的一次性令牌,就可能被「替用户做事」。

// 语法要点:CSRF 攻击的是「**身份绑定在 Cookie** 且接口**不校验来源**」的组合

//错误示例(后端契约层面)
// POST /transfer 只检查 Cookie,不检查 CSRF token

//正确示例(思路)
// 1) SameSite=Lax/Strict(见下文局限) 2) CSRF token(表单隐藏域 / 双 Cookie) 3) 关键操作要求二次认证

啥时候用

  • 所有改状态的接口:转账、改邮箱、删数据。

2 SameSite Cookie:有用但不是银弹

定义:你在《浏览器存储与缓存策略》读过:SameSite=Lax 为现代浏览器默认——跨站子资源请求常常不再携带 Cookie,能挡不少旧式 CSRF。
局限同站(scheme + registrable domain)仍可能携带;GET 仍可能被顶级导航利用(Lax 对若干 GET 放行);若站点需要第三方 cookie(广告、嵌入),世界会复杂得多。

// 语法要点(Set-Cookie 示意)
Set-Cookie: sid=...; Path=/; Secure; HttpOnly; SameSite=Lax

啥时候用

  • 新站默认 SameSite=Lax;评估 Strict 的兼容与体验。

3 CSRF Token 与「双提交」思路

定义:CSRF Token — 服务端下发与用户会话绑定的随机数,表单或 fetch 头带上;服务器校验。双提交 Cookie — Cookie 里一份、Header/Form 里一份,服务器比对是否一致(注意 XSS 会连 Cookie 与 Header 一起读——所以 XSS 与 CSRF 防线要叠罗汉,不是二选一)。

//正确示例——SPA 从登录响应拿 token,存在内存或 meta;每次 mutation 请求带头
await fetch('/api/settings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfFromCookieOrMeta,
  },
  credentials: 'include',
  body: JSON.stringify(payload),
});

//错误示例——把 CSRF token 塞 localStorage 然后又怕 XSS —— XSS 照样读

4 CSRF 与「GET 改状态」的反模式

定义:REST 本意里 GET 应是安全、幂等的;若你的 删除、转账、改邮箱 用 GET 或「GET 也能触发副作用」,则 CSRF + 钓鱼链接成本极低(一页 <img src> 在部分 SameSite 策略下仍可能触发顶级导航中的 GET —— 具体结合浏览器默认与 Set-Cookie 细读)。
精确规则凡是改状态,用 POST/PUT/PATCH/DELETE;配合 CSRF tokenSameSite

<!-- 错误示例:用 GET 注销 —— 太容易被第三方页面引用 -->
<a href="https://bank.example/logout">不要这样设计</a>

啥时候用

  • 评审旧接口 — 业务说「历史原因」时,把这张表拍桌上。

5 Origin / Referer 校验:补 CSRF 时的注意点

定义:服务端可校验 OriginReferer 是否来自可信域——但 Referer 可被用户隐私设置 / Referrer-Policy 去掉;Origin 在多数浏览器发起的 CORS 简单/预检请求里更可靠,但不是万能鉴权,仍应配合 token。
精确不要:把「只验 Header」当成「已经防了 CSRF」——攻击页面仍可发起 同源策略允许的请求形态(视方法、Content-Type、Cookie 携带而定)。

啥时候用

  • 无 CSRF token 的老接口应急 — 与后端一起定 退出条件 切到正式 token 方案。

五、点击劫持与 UI 欺骗

1 X-Frame-Optionsframe-ancestors

定义:诱导用户以为在点 A,其实在点被 iframe 覆盖的 B
防御:

  • X-Frame-Options: DENY | SAMEORIGIN(老而稳)。
  • CSP frame-ancestors(更灵活,可列多个祖先)。
// 正确示例
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

啥时候用

  • 后台管理、支付页 — 默认拒绝被嵌;若业务需要嵌入,白名单具体父域。

2 X-Content-Type-Options: nosniff

定义:阻止浏览器嗅探非脚本类型为可执行内容,降低部分上传攻击面。

X-Content-Type-Options: nosniff

啥时候用

  • 所有静态资源响应 — 尤其用户上传文件下载接口。

3 减少泄漏与权限收缩:Referrer-Policy、Permissions-Policy

1 Referrer-Policy:别把下一跳的 URL 细节送给全世界

定义:控制是否在请求里附带 Referer 以及附带多少(完整路径、是否含源)。收紧可减轻路径/查询串泄露;太严可能影响合法的分析与安全反爬策略——要和业务一起定。

// 语法要点(响应头示意)
Referrer-Policy: strict-origin-when-cross-origin
<!-- 兜底(页面级)——别替代全站策略 -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

啥时候用

  • URL 带敏感 id —— 与日志脱敏、权限校验一起看。

2 Permissions-Policy:哪些强大 API 在整站「默认关掉」

定义:以前常叫 Feature Policy;告知浏览器本页及子 iframe 能否使用摄像头、麦克风、地理位置、全屏、PaymentRequest、USB 等能力。被禁止时调用会失败或抛错(依 API 而定)。

Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://pay.example.com")

啥时候用

  • 嵌入未知第三方 iframe 的内容站 — 先禁再按需开,比出事下架便宜。

3 Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy(进阶:跨源隔离)

定义:这对响应头把文档放入更严格的跨源隔离环境,是启用 SharedArrayBuffer 等能力的常见前置;也会改变 window.opener、弹窗集成行为。乱上会导致 OAuth 弹窗、统计脚本异常——必须与全链路联调后再开。

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

啥时候用

  • WASM 多线程 + 共享内存 等高性能场景;普通后台 CRUD 别跟风

六、敏感信息与「浏览器永远不可信」

1 不要把「机密」写进前端

定义:API 私钥、数据库密码、Stripe sk_live —— 永远不能出现在浏览器。前端最多只有可公开或可被限制的 key(配合域名白名单、后端签名)。

//错误示例
const AWS_SECRET = 'xxxx';

//正确示例——需要签名的上传,走后端签发 STS / presigned URL

啥时候用

  • 代码评审第一反应:搜 secretBEGIN PRIVATE KEY

2 Token 放哪:localStorage vs HttpOnly Cookie

定义:

  • localStorage / sessionStorage任何页面脚本可读(遇 XSS 即送)。
  • HttpOnly Cookie脚本读不到,能抵抗单纯读存储的 XSS 偷 token,但不能单靠它解决 CSRF
    没有 XSS 银弹CSP + 严格输出编码 + 依赖治理 是主线。
//错误示例
localStorage.setItem('access_token', token); // XSS:一键复制

//更安全的方向(概括)
// 短生命周期 access + 后端 HttpOnly refresh 轮替;或全站 Cookie + CSRF 防护,视架构而定

啥时候用

  • 做技术选型评审 — 先把威胁模型写在白板上再选 storage。

3 URL 与日志:别把秘密放查询串

定义:GET ?token= 会出现在浏览器历史、Referer、服务器访问日志
正确Authorization 头或 POST body(仍要 HTTPS)。

//错误示例
location.href = 'https://api.example.com/me?token=' + token;

//正确示例
fetch('https://api.example.com/me', { headers: { Authorization: `Bearer ${token}` } });

啥时候用

  • 第三方 OAuth 回调设计 —— 当心 URL fragment 与 code 交换流程。

七、HTTPS、混合内容、HSTS

1 为什么「全站 HTTPS」不是可以随便省略的套话

定义:不加密的传输可被窃听与篡改(包括 Cookie、个性化内容)。混合内容(HTTPS 页中的 HTTP 子资源)会被浏览器升级拦截或阻断。

// 语法要点:Secure Cookie 只在 HTTPS 发送

//错误示例——生产环境仍 http 提供登录页

啥时候用

  • 所有登录态页面 — 与《网络层》「Mixed Content」一节呼应。

2 HSTS(强制 HTTPS 记忆)

定义:Strict-Transport-Security 让浏览器记住应用只走 HTTPS,减少 SSL 剥离攻击。

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

啥时候用

  • 域名稳定后由后端开启;小心子域还没 ready 就开 includeSubDomains 导致大面积故障。

八、依赖供应链与第三方脚本

1 npm 包不是「免安检乘客」

定义:锁文件package-lock / pnpm-lock)、SRI(子资源完整性)最小权限的第三方域名 —— 减少「构建即被改包」与「CDN 被投毒」的面积。

<!-- 正确示例:SRI -->
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-..."
  crossorigin="anonymous"
></script>

啥时候用

  • 接入统计、客服浮窗、地图 SDK — 评估可否延迟加载、子资源完整性。

可落地实践(依赖与第三方)

  • 在 CI 中强制使用锁文件(npm ci / pnpm install --frozen-lockfile),并把生成的 SBOM 附到构建记录。
  • 对接入的新第三方脚本做最小化域名隔离、延迟加载与 SRI;重要页面优先使用子域隔离或 sandbox iframe。
  • 建立「新包审批」流程:查看 weekly downloads、repository 链接与贡献历史;对可疑包使用私有 registry + 白名单。
  • npm audit 作为周期性风险评估的一环,建立工单/跟踪规则;对于高危 CVE 指定 RTO 并评估修复成本。
  • 使用自动化依赖更新(Dependabot/renovate)但保留人工审查,CI 中跑集成测试与 bundle 比对以捕捉行为差异。

落地理由:把偶发风险变成可追溯、可回滚的事件流,便于审计与快速响应。


2 lockfile、CI 与「可重现构建」

定义:没有锁文件 = 同一条 npm install 在不同天可能得到不同依赖树——供应链攻击里这是放大器
精确做法:版本库里提交 锁文件;CI 用 npm ci(或 pnpm/yarn 等价命令)做确定性安装;重大升级走 PR + diff 依赖树,而不是「随手 upgrade 大版本」。

# 语法要点:CI 里用 ci 而不是 install(依包管理器择一)
npm ci

#错误示例——生产镜像构建脚本写 npm install --no-package-lock

啥时候用

  • 被审计的客户 — 第一条就查你有没有 lockfile 与 SBOM。

3 npm audit 不是万能,但也不是摆设

定义:audit 报告的是已知漏洞数据库与依赖匹配,会有误报/漏报——但它能迫使团队周期性回答「我们是否仍在用有 CVE 的传递依赖」。
精确态度不把 audit 当门禁的唯一真理;也不永远 npm audit fix --force 大版本蹦级——先看 breaking change。

啥时候用

  • 发版前 — 至少扫一遍;高危项要有工单跟踪号。

4 拼写近似包名(typosquatting)

定义:攻击者发布 lodashs / react-domm 之类名字蹭安装失误;或 postinstall 脚本挖矿。
精确预防:安装新包前 看 weekly downloads、repository 链接、Readme 是否正常;组织内用 私有 registry + 审批名单


5 「放一个 <script> 就把键盘交出去」

定义:第三方脚本与你的页面同源视角下权限极大——能读非 HttpOnly 的存储、能改写 DOM、能劫持 fetch治理 > 盲目接入

//正确示例——沙箱 iframe 承载极端第三方(有代价,未必可行,仅思路)
// 业务上更多用「合同 + SLA + 子资源审计」

//错误示例——为了统计把主站密钥放 window 全局

啥时候用

  • 增长团队要加七个小工具 — 安全评审合成一条 PR。

九、postMessage:跨窗口别「来者不拒」

定义:父子窗口 / iframe / window.open 通信用 postMessage
必须event.origin 白名单校验;敏感数据别明文广播。

//正确示例
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.example.com') return;
  // 再处理 event.data
});

//错误示例
window.addEventListener('message', (event) => {
  eval(event.data); // 任意来源 + 任意代码
});

啥时候用

  • OAuth popup 回调嵌入支付页

十、实战清单(上线前快速扫一遍)

做什么
输出 默认 textContent;HTML 必过可信模板或消毒
CSP(先 Report-Only)、frame-ancestorsnosniff、HSTS、Referrer-PolicyPermissions-Policy(按需)
会话 HttpOnly + Secure + SameSite;CSRF 策略与后端对齐
存储 禁明文长期密钥;最小化 localStorage 中的高价值
依赖 锁版本 + 审计;CDN 用 SRI
第三方 延迟加载、域名最小化、合同 SLA
日志 URL 去敏;前端报错上报脱敏

自勉:安全条款读起来像「行政通知」,但每一句背后都有真实血汗账单。写漂亮 UI 是本事;不让用户流血是本分。


十一、结语

前端在安全防御链路中的核心职责:减少攻击面、严控能执行的输入、以及把风险可观测化与可回滚化。关键带走点:

  • 输出永远优先文本(textContent、模板引擎转义);对富文本使用服务端消毒 + 客户端二次消毒。
  • 用 CSP(先 Report-Only)和 Trusted Types 对「执行语义」进行权限收缩;不要把 CSP 当万能钥匙。
  • 对会话与敏感数据采用 HttpOnly + Secure + 合理的 SameSite 策略,并把 CSRF token / 二次确认作为重要操作的必备防线。
  • 依赖管理要把随机性降到最低:锁文件、CI 的确定性安装、私有 registry 与审查流程;把 npm audit 与自动化更新变成可追溯的过程。
  • 第三方脚本需最小权限、延迟加载、SRI 或子域隔离;把风险纳入合同与 SLA。

行动清单(上线前至少完成):

  1. 在 CI 中启用锁文件校验与 SBOM 生成;2. 部署 CSP 报告模式并收集误报;3. 建立第三方脚本接入审批与 SRI 检查;4. 在代码审查中强制检查高危 sink(innerHTML、eval 等)。

这些措施成本可控、见效明确,把日常开发从「偶然犯错」转成「可管理的风险」。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐