前端安全考察
面试官考察安全,不是要你背漏洞定义,而是看你在编码习惯和架构设计中有没有“红线意识”。 考察形式通常有三种:①直接问“XSS和CSRF的区别”;②给你一段代码让你找漏洞;③给一个场景(如文件上传、登录)问你需要注意什么。
考点一:XSS(跨站脚本攻击)—— 前端头号杀手
基础认知(面试官怎么问):
- 问法1:“什么是XSS?有哪些类型?”
- 问法2:“看这段代码
<div></div>,如果userInput是<script>alert(1)</script>会发生什么?怎么修?” - 问法3:“React 中
dangerouslySetInnerHTML为什么危险?”
进阶原理(三大类型与防御本质):
- 反射型(非持久):恶意脚本藏在URL参数里(如
?q=<script>...),服务端直接回显到页面。攻击者诱导用户点击恶意链接。 - 存储型(持久):恶意脚本存入数据库(如评论区、昵称),每次访问页面自动执行。危害最大(如某年微博蠕虫)。
- DOM型:前端JS直接操作DOM(如
innerHTML = location.hash),绕过服务端过滤。 - 防御本质:永远不要信任用户的输入。核心原则是 “输出时转义(Escape)”,而不是“输入时过滤”(因为输出场景不同,富文本可能需要保留HTML)。
- 反射型(非持久):恶意脚本藏在URL参数里(如
源码/实战回答(结合你的项目,重点背):
(基础回答) “在 WeLink IM 这种高频通讯场景,我严格遵循 ‘默认转义,按需放行’ 策略。React 的 JSX 默认会转义所有字符串(
<变成<),这是第一道防线。但如果有富文本消息(支持加粗、斜体),绝对不能直接拼接 HTML,必须用 DOMPurify 库进行消毒(Sanitize),只保留白名单标签,过滤onerror、onload等事件属性。” (代码示例——找漏洞 + 修复):javascript// ❌ 高危代码(面试官最爱问) function Message({ content }) { return <div dangerouslySetInnerHTML={{ __html: content }} />; } // 如果 content = '<img src=x onerror="alert(1)">',弹窗直接执行! // ✅ 正确修复(结合你的实际写法) import DOMPurify from 'dompurify'; function SafeMessage({ content }) { const cleanHtml = DOMPurify.sanitize(content, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'span'], // 白名单 ALLOWED_ATTR: ['class'] // 只允许class,禁用onerror/onload }); return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />; }(结合 IAMP 项目):“在 IAMP 隧道检测平台,我接收后端返回的点云文件路径,如果文件名是用户可控的(比如
../etc/passwd),我不会直接拼接到fetchURL 中。我会做 路径规范化(Normalize),限制只能访问特定目录下的.las文件。”
考点二:CSRF(跨站请求伪造)—— 借刀杀人
基础认知(面试官怎么问):
- 问法1:“用户登录了银行A,点开了黑客的钓鱼网站B,为什么B网站能转走A的钱?”
- 问法2:“怎么防御 CSRF?Token 放在 Header 还是 Cookie?”
进阶原理(为什么能成功):
- 核心漏洞点:浏览器发送请求时,会自动携带目标域名的 Cookie(即使是第三方网站发起的请求)。如果服务器只靠 Cookie 识别身份,黑客就能让用户“不知不觉”发起恶意请求(如修改密码、转账)。
- 防御三件套(必须脱口而出):
- SameSite Cookie(最强基础):设置
Set-Cookie: SameSite=Strict,跨站请求完全不携带 Cookie。如果业务需要跨站跳转带 Cookie,用SameSite=Lax(允许顶级导航带,禁止 POST 跨站带)。 - CSRF Token(同步令牌):服务端下发给前端一个随机字符串,前端在请求体或 Header(如
X-CSRF-Token)中带上。黑客无法读取页面源码拿到 Token,所以无法伪造。 - 双重 Cookie 验证:请求时取 Cookie 里的值放在 Header,服务端比对两者是否一致(较古老,不推荐,因为子域可能被污染)。
- SameSite Cookie(最强基础):设置
源码/实战回答(结合你的项目):
“在 IRMP 报告生成平台(涉及合同金额敏感数据),我配合后端实现了 双重 CSRF 防御:
- Cookie 设置:登录成功后,后端在
Set-Cookie中携带SameSite=Strict,确保跨站请求无法携带身份凭证。 - 请求拦截器(Axios):我在全局请求拦截器中,从服务端下发的 Meta 标签读取 Token,自动添加到请求头
X-CSRF-Token。
为什么需要两层?因为曾经发现有个别老旧浏览器不认
SameSite,所以前端主动带 Token 做兜底。”javascript// 你的 Axios 全局拦截器(展示给面试官看) axios.interceptors.request.use(config => { // 从 meta 标签获取服务端下发的 CSRF Token const token = document.querySelector('meta[name="csrf-token"]')?.content; if (token) { config.headers['X-CSRF-Token'] = token; } return config; });- Cookie 设置:登录成功后,后端在
考点三:点击劫持(Clickjacking)—— 看不见的陷阱
基础认知:黑客把你的网站放在一个透明的
<iframe>里,诱导用户点击看似“抽奖”的按钮,实际上点到了“转账”或“删除”按钮。进阶防御(响应头):服务端设置
X-FRAME-OPTIONS响应头。DENY:禁止任何页面嵌套。SAMEORIGIN:只允许同域嵌套。ALLOW-FROM uri:只允许指定域名嵌套(老旧)。
实战结合(WeLink 企业级要求):
“华为 WeLink 涉及企业内部通讯录和邮件,为了防止钓鱼,我在 Nginx 配置中强制添加了
add_header X-Frame-Options DENY always;。同时在登录页面,我用window.top !== window.self做前端 JS 兜底,检测到被嵌套就自动跳转破窗(Break Out)。”
考点四:CSP(内容安全策略)—— 终极白名单
基础认知:CSP 是浏览器自带的一套“白名单”机制,告诉浏览器哪些资源(脚本、样式、图片)是合法的,不在白名单的一律不加载/不执行。即使 XSS 注入成功,恶意脚本也跑不起来。
进阶回答(展示架构视野):
“我们在 WeLink 生产环境开启了严格的 CSP 策略:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.wechat.com;。这意味着只允许加载同域脚本和指定 CDN 脚本,禁止eval()执行字符串代码(防止动态注入)。 注意事项:开启 CSP 后,如果用了 Vue/React 的运行时编译(template字符串),会被 CSP 拦截报错。所以必须使用 Vue/React 的运行时构建(Runtime-only),即预编译好的render函数。这一点在 IRMP 项目升级 Vite 时我特别注意过。”
考点五:敏感信息泄露(LocalStorage / Cookie / 源码)
基础认知(面试官怎么问):
- 问法1:“登录后的 Token 应该存哪里?localStorage 还是 Cookie?”
- 问法2:“前端代码里可以写死数据库密码吗?为什么?”
进阶原理与答法(必杀技):
- Token 存储之争(面试高频杠精题):
- 存
localStorage:容易受 XSS 攻击窃取(JS 能直接读)。 - 存
Cookie且设置HttpOnly:JS 无法读取,抵御 XSS 窃取,但容易受 CSRF 攻击。 - 终极方案(你的选择):“在 WeLink 这种高安全级别应用,我们采用 ‘双管齐下’:身份凭证(Refresh Token)存在
HttpOnlyCookie 中防止 XSS 窃取;同时利用SameSite=Lax防止 CSRF。前端内存中只存一个短暂的Session Token,用于接口鉴权。这样即便 XSS 注入成功,黑客也拿不到最重要的长令牌。”
- 存
- 源码暴露:“Webpack/Vite 打包后的代码依然可以被反编译。绝对不能在代码里硬编码 数据库密码、AK/SK 密钥。我们所有机密配置通过 环境变量(
.env)+ 后端接口下发 的方式注入,并且敏感字段在打包时会被transform替换为占位符。”
- Token 存储之争(面试高频杠精题):
考点六:文件上传安全(结合你的 IRMP 报告上传)
基础认知:用户上传 PDF/图片,存在恶意脚本伪装成文件的风险。
实战回答(全面防御):
“在 IRMP 平台支持用户上传检测报告附件。我做了三层防御:
- 前端限制(防君子):
accept=".pdf,.docx"限制文件选择类型。 - 文件头校验(魔数检测):上传前用 JS 读取文件前 4 个字节(如
%PDF),确保真实类型匹配扩展名,防止.exe伪装成.pdf。 - 重命名存储:上传前建议后端采用 UUID + 时间戳 重命名文件,剥离原始文件名中的危险字符(如
../路径穿越)。”
- 前端限制(防君子):
考点七:依赖库安全(供应链攻击)
基础认知:npm 生态中,一个恶意库可能盗取环境变量。
进阶回答(体现责任心):
“在项目中,我每周会跑一次
npm audit检查已知漏洞。引入新库时,我遵循 ‘三原则’:- 看下载量(优先每周 > 10万 的稳定库)。
- 看维护状态(最近 3 个月有 commit)。
- 看依赖树(避免引入
left-pad这种单行库或带有postinstall高危脚本的库)。
在 WeLink,大型版本升级前,我会用
pnpm的lockfile与基线版本做 diff,审查所有间接依赖的升级变动,防止恶意版本偷渡。”
面试现场组合技(必杀话术——安全总结陈词)
如果面试官最后问:“你觉得前端安全的核心原则是什么?”,请用这段话来收尾,展现你的格局:
“面试官,在我看来,前端安全没有绝对的金钟罩,核心原则是 ‘纵深防御(Defense in Depth)’。
- 编码层面:默认转义(React/Vue 自动转义),杜绝
innerHTML拼接。- 传输层面:全站 HTTPS + 安全响应头(CSP、X-Frame-Options)。
- 存储层面:敏感凭证存
HttpOnly Cookie,废弃localStorage存 Token 的习惯。- 流程层面:依赖库定期审计 + 代码 Review 必查
dangerouslySetInnerHTML和eval。我举个具体例子:在 WeLink IM,即使开发图方便用了
innerHTML,MR 阶段一定会被打回,因为安全是华为的 ‘红线’。正是因为这套机制,WeLink 才通过了金融等高安全等保认证。”
关于 XSS 攻击,你提到的 xssprotect 这类方法,正是企业级防御体系中的一环。下面我从攻击方式和防御体系两个方面,帮你系统梳理一下。
🎯 一、 XSS 的三种注入方式
XSS 攻击的核心,就是攻击者找到了“注入”恶意脚本的入口。根据注入方式的不同,主要分为三类:
1. 存储型 XSS(Stored XSS)—— 危害最大
- 原理:恶意脚本被永久存储在目标服务器的数据库、留言板等地方。当用户正常访问页面时,这些脚本会从服务器端取出并执行。
- 常见场景:在博客评论区、论坛帖子、用户个人信息等可以持久化存储文本的地方植入恶意代码。
- 核心特征:数据来源于服务器端数据库,影响所有访问该页面的用户。
2. 反射型 XSS(Reflected XSS)—— 最普遍
- 原理:恶意脚本不存储在服务器,而是作为** URL 参数**的一部分发送给服务器。服务器未经处理,直接将这段参数“反射”回浏览器并执行。
- 常见场景:攻击者构造一个含恶意参数的链接,通过钓鱼邮件等方式诱导用户点击。
- 核心特征:数据来源于当前请求的 URL,是一次性的,通常通过诱导用户点击链接触发。
3. DOM 型 XSS(DOM-based XSS)—— 纯前端
- 原理:攻击者利用前端 JavaScript 代码本身的漏洞,通过修改页面 DOM 环境来执行恶意脚本。整个过程不经过服务器。
- 常见场景:前端代码直接使用
document.write、innerHTML或eval()等不安全的方式处理来自document.URL、location.hash等用户可控的数据。 - 核心特征:恶意代码的源头是客户端的 JavaScript 逻辑,不依赖服务器响应。
🛡️ 二、 企业级 XSS 防御:四层纵深体系
企业通常采用“纵深防御”策略,构建多层次的防护体系。
第一层:代码层的“过滤与净化”
- 原则:永远不要信任用户的输入。
- 做法:
- 输入验证:对用户输入进行白名单校验,只接受符合预期格式的数据。
- 输出编码(核心):根据数据输出的上下文(HTML、属性、JavaScript等),将特殊字符(如
<,>,")转义为对应的HTML实体。 - 使用净化库:对于需要保留部分富文本的场景,使用专业的净化库(如
DOMPurify、js-xss)。 - 使用安全的 API:在操作 DOM 时,优先使用
textContent替代innerHTML,避免使用eval()等危险函数。
第二层:浏览器层面的“安全策略”
- Content Security Policy (CSP) —— 终极武器:通过设置 HTTP 响应头
Content-Security-Policy,告诉浏览器哪些资源是合法的,只执行白名单内的脚本。 X-XSS-Protection响应头 —— 旧版浏览器兜底:这是一个非标准标头,用于启用旧版浏览器的内置XSS过滤器。但由于其存在安全问题且已被CSP取代,现代企业更推荐使用 CSP。
- Content Security Policy (CSP) —— 终极武器:通过设置 HTTP 响应头
第三层:数据存储层的“安全加固”
HttpOnly与Secure的 Cookie:为关键的认证 Cookie 设置HttpOnly属性,使其无法被 JavaScript 读取,从根本上防止 XSS 窃取会话;设置Secure属性,确保其仅通过 HTTPS 传输。
第四层:流程与工具层的“持续防护”
- 依赖管理:定期使用
npm audit等工具检查并升级项目依赖,修复已知漏洞。 - 自动化扫描:在 CI/CD 流程中集成自动化安全扫描工具,尽早发现潜在问题。
- 安全培训与代码审查:定期进行安全培训,并将 XSS 防护作为 Code Review 的硬性检查项。
- 依赖管理:定期使用
💡 关于你提到的 xssprotect
你提到的 xssprotect 是一个自定义的过滤函数,这本质上就是“代码层的过滤与净化”实践。
一个典型的自定义 xssprotect 函数会做这些事情:
- 字符替换:将
<,>,",',&等特殊字符替换为对应的 HTML 实体。 - 移除危险标签:通过正则表达式移除
<script>,<iframe>,onerror=等危险的标签和属性。
需要注意的是,虽然自定义方法简单直接,但很容易因为考虑不周而被绕过。在企业级应用中,更推荐使用经过社区广泛验证的成熟库(如 DOMPurify),它们对各种复杂的攻击手段有更完善的防护。
💎 总结与面试话术
你可以这样组织你的回答:
“面试官,关于 XSS 防护,我从防御体系的角度来阐述。首先,XSS 根据注入方式分为存储型、反射型和 DOM 型三种。
其次,企业的常规做法是构建四层纵深防御体系:
- 代码层:核心是‘过滤与净化’,包括输入验证、输出编码和使用
DOMPurify等专业库。您提到的xssprotect就是一种自定义的过滤函数。- 浏览器层:通过设置 CSP (内容安全策略) 响应头,限制浏览器只执行白名单内的脚本,这是最有效的防御手段。
- 数据层:为关键 Cookie 设置
HttpOnly和Secure属性,防止其被脚本窃取。- 流程层:通过依赖管理、自动化扫描和代码审查,确保持续安全。
总的来说,XSS 防御是一个系统工程,需要从代码到架构全方位进行考量。”