前端安全边界
前端安全边界
一、导读
1 怎么读这篇笔记
| 定义: | 先讲「威胁从哪里来、浏览器默认帮你挡了什么」,再讲你该怎么写代码才不拆墙。 |
| 代码块 | 含// 语法要点、//正确示例、//错误示例;安全话题也要能落地到代码,而不是口号集合。 |
| 啥时候用 | 每个小节后列「联调、上线、接入第三方脚本」常见触点。 |
| 与前文的关系 | 你在《前端网络层》里写过 Cookie 与 CORS;在《浏览器存储》里写过 HttpOnly、SameSite;本篇把它们放进统一威胁模型里读,而不是三条孤立知识点。 |
目标:能说清XSS / CSRF / 点击劫持 / 敏感信息落点四类主线的原理边界;能在评审里指出「这句 innerHTML 为什么危险、这个登录态为什么不该进 localStorage、这段 CORS 配置到底方便了谁」。
免责声明:安全是系统级问题;前端守住的是输入输出与最小暴露面。任何「靠前端加密密钥」的方案都不成立——因为浏览器里的字符串用户都能看见。
2 威胁建模速记:STRIDE(知道一张表就够)
定义:STRIDE是微软经典威胁分类法,用以穷举思考攻击面(不是前端独舞,但产品评审时很好用):
| 字母 | 含义 | 前端常触点 |
|---|---|---|
| SSpoofing 伪装 | 冒充用户/站点 | 会话窃取、钓鱼页 UI |
| TTampering 篡改 | 改数据、改包 | 无完整性校验的静态资源、被劫持 CDN |
| RRepudiation 抵赖 | 否认操作 | 审计日志应在服务端 |
| IInformation Disclosure 泄露 | 不该看的被看了 | XSS、Referer、错误栈上传 |
| DDenial of Service 拒绝服务 | 拖垮可用性 | 主线程炸弹、无限弹窗(体验/可用性) |
| EElevation of Privilege 提权 | 低权限变高权限 | 越权接口、IDOR |
本篇不展开每一条的攻防全书,但你在 PR 描述里写一句「此项改动影响 STRIDE 的 I/T」——评审档次立刻不一样。
二、先搭「威胁模型」:我们在防谁
1 资产、威胁、暴露面
定义:
- 资产— 用户数据、会话、钱、声誉。
- 威胁— 窃取会话、伪造请求、钓鱼、植入恶意脚本、拖库(多在后端)。
- 暴露面— 浏览器能读写的所有入口:
HTML拼接、postMessage、URL 参数、第三方脚本、存储 API。
前端的目标不是「绝对不被黑」,而是提高攻击成本、避免低级失误把大门敞开。
// 语法要点:凡是「把不可信字符串塞进能执行的环境」都是高危//错误示例element.innerHTML=userInput;//正确示例——先问:这是纯文本还是富文本?纯文本就用 textContentelement.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)白名单+URLAPI 解析;禁止javascript: |
CSS(style/style属性) | expression()、-moz-binding(历史) | 尽量不动态拼完整样式字符串;用类名切换 |
JSON进<script type="application/json"> | </script>断出有脚本 | 按 JSON 规则转义并由后端生成;切勿字符串拼接 |
一条总原则:任何「把不可信数据」写进能产生执行语义的位置,都是高危 sink;先定上下文,再谈转义表——不要幻想「统一escapeHtml一把梭」。
//正确示例——把用户输入当作**文本节点**constp=document.createElement('p');p.textContent=userName;root.append(p);//错误示例——模板字符串拼 hrefa.href=`javascript:alert(1)`;// 若 user 可控,灾难//正确示例——http(s) 白名单校验后再赋值functionsafeHref(url){try{constu=newURL(url,location.href);if(u.protocol==='https:'||u.protocol==='http:')returnu.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 收集误杀。
4upgrade-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配合 CSPrequire-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) 关键操作要求二次认证啥时候用
- 所有改状态的接口:转账、改邮箱、删数据。
2SameSiteCookie:有用但不是银弹
定义:你在《浏览器存储与缓存策略》读过: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 请求带头awaitfetch('/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>啥时候用
- 评审旧接口— 业务说「历史原因」时,把这张表拍桌上。
5Origin/Referer校验:补 CSRF 时的注意点
定义:服务端可校验Origin或Referer是否来自可信域——但Referer可被用户隐私设置 /Referrer-Policy去掉;Origin在多数浏览器发起的 CORS 简单/预检请求里更可靠,但不是万能鉴权,仍应配合 token。
精确不要:把「只验 Header」当成「已经防了 CSRF」——攻击页面仍可发起同源策略允许的请求形态(视方法、Content-Type、Cookie 携带而定)。
啥时候用
- 无 CSRF token 的老接口应急— 与后端一起定退出条件切到正式 token 方案。
五、点击劫持与 UI 欺骗
1X-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'啥时候用
- 后台管理、支付页— 默认拒绝被嵌;若业务需要嵌入,白名单具体父域。
2X-Content-Type-Options: nosniff
定义:阻止浏览器嗅探非脚本类型为可执行内容,降低部分上传攻击面。
X-Content-Type-Options: nosniff啥时候用
- 所有静态资源响应— 尤其用户上传文件下载接口。
3 减少泄漏与权限收缩:Referrer-Policy、Permissions-Policy
1Referrer-Policy:别把下一跳的 URL 细节送给全世界
定义:控制是否在请求里附带Referer以及附带多少(完整路径、是否含源)。收紧可减轻路径/查询串泄露;太严可能影响合法的分析与安全反爬策略——要和业务一起定。
// 语法要点(响应头示意) Referrer-Policy: strict-origin-when-cross-origin<!-- 兜底(页面级)——别替代全站策略 --><metaname="referrer"content="strict-origin-when-cross-origin"/>啥时候用
- URL 带敏感 id—— 与日志脱敏、权限校验一起看。
2Permissions-Policy:哪些强大 API 在整站「默认关掉」
定义:以前常叫 Feature Policy;告知浏览器本页及子 iframe能否使用摄像头、麦克风、地理位置、全屏、PaymentRequest、USB 等能力。被禁止时调用会失败或抛错(依 API 而定)。
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://pay.example.com")啥时候用
- 嵌入未知第三方 iframe 的内容站— 先禁再按需开,比出事下架便宜。
3Cross-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 私钥、数据库密码、Stripesk_live——永远不能出现在浏览器。前端最多只有可公开或可被限制的 key(配合域名白名单、后端签名)。
//错误示例constAWS_SECRET='xxxx';//正确示例——需要签名的上传,走后端签发 STS / presigned URL啥时候用
- 代码评审第一反应:搜
secret、BEGIN PRIVATE KEY。
2 Token 放哪:localStoragevsHttpOnly 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 --><scriptsrc="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(依包管理器择一)npmci#错误示例——生产镜像构建脚本写 npm install --no-package-lock啥时候用
- 被审计的客户— 第一条就查你有没有 lockfile 与 SBOM。
3npm 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 等)。
这些措施成本可控、见效明确,把日常开发从「偶然犯错」转成「可管理的风险」。
