前端安全边界
前端安全边界
一、导读
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=..." 断引号 |
属性实体转义 + 能用布尔/严格枚举就别字符串裸插 |
URL(href/src) |
javascript:、data:text/html |
http(s) 白名单 + URL API 解析;禁止 javascript: |
CSS(style/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-requests 与 block-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 token 与 SameSite。
<!-- 错误示例:用 GET 注销 —— 太容易被第三方页面引用 -->
<a href="https://bank.example/logout">不要这样设计</a>
啥时候用
- 评审旧接口 — 业务说「历史原因」时,把这张表拍桌上。
5 Origin / Referer 校验:补 CSRF 时的注意点
定义:服务端可校验 Origin 或 Referer 是否来自可信域——但 Referer 可被用户隐私设置 / Referrer-Policy 去掉;Origin 在多数浏览器发起的 CORS 简单/预检请求里更可靠,但不是万能鉴权,仍应配合 token。
精确不要:把「只验 Header」当成「已经防了 CSRF」——攻击页面仍可发起 同源策略允许的请求形态(视方法、Content-Type、Cookie 携带而定)。
啥时候用
- 无 CSRF token 的老接口应急 — 与后端一起定 退出条件 切到正式 token 方案。
五、点击劫持与 UI 欺骗
1 X-Frame-Options 与 frame-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
啥时候用
- 代码评审第一反应:搜
secret、BEGIN PRIVATE KEY。
2 Token 放哪:localStorage vs HttpOnly Cookie
定义:
localStorage/sessionStorage— 任何页面脚本可读(遇 XSS 即送)。HttpOnlyCookie — 脚本读不到,能抵抗单纯读存储的 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-ancestors、nosniff、HSTS、Referrer-Policy、Permissions-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。
行动清单(上线前至少完成):
- 在 CI 中启用锁文件校验与 SBOM 生成;2. 部署 CSP 报告模式并收集误报;3. 建立第三方脚本接入审批与 SRI 检查;4. 在代码审查中强制检查高危 sink(innerHTML、eval 等)。
这些措施成本可控、见效明确,把日常开发从「偶然犯错」转成「可管理的风险」。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)