1. 前端安全面试的底层逻辑拆解
1.1 为什么前端安全成了面试必考题
早些年面前端,安全这块顶多问一句“你知道XSS吗”,候选人回一句“跨站脚本攻击,要转义”,面试官点点头就过去了。现在完全不是这个节奏。我最近帮团队筛简历、做技术面,安全相关的追问能占到整个面试时长的四分之一,尤其是中高级岗位,安全答不上来基本就挂了。
原因很直接:前端能碰到的敏感数据越来越多。Token存在哪、用户输入怎么渲染、第三方脚本怎么加载、跨域请求怎么带凭证,这些决策一旦做错,轻则数据泄露,重则整个账号体系被击穿。面试官考安全,考的不是你背了多少名词,而是你有没有“攻击者视角”——你写代码的时候,脑子里有没有一个假想敌。
这一章我打算把XSS、CSRF、CSP、JWT这四个高频考点拆开讲透。每个点我都会按“面试官想听什么→实际原理是什么→代码层面怎么防→常见追问怎么答”这个顺序来展开。你如果正在准备面试,可以把它当成一份带答案的复习提纲;你如果是已经工作的前端,也可以拿它对照检查自己项目里的安全水位。
1.2 面试官到底在考察什么
先把这个说清楚,不然你背再多知识点也是散的。面试官问安全,通常想验证三件事。
第一,你知不知道攻击面在哪。比如一个搜索框,用户输入的内容直接innerHTML到页面上,这就是一个典型的DOM型XSS入口。你能不能一眼看出来,决定了你写代码时会不会主动规避。
第二,你懂不懂防御的层次。安全从来不是单点方案。防XSS,你需要输入校验、输出编码、CSP兜底;防CSRF,你需要SameSite、Token、Referer校验。面试官喜欢听你说“我会从几个层面来做”,而不是“我用了一个库就搞定了”。
第三,你有没有实战排查经验。比如线上突然报了一个XSS告警,你怎么定位是哪个参数、哪个页面、什么类型的注入?这种问题没有标准答案,但能区分出“看过书”和“干过活”的人。
提示:面试时遇到安全题,先别急着背定义。用一句话说清楚“这个漏洞的本质是什么”,再展开防御手段,节奏会稳很多。
1.3 四个考点的关联关系
XSS、CSRF、CSP、JWT不是四个孤立的知识点,它们之间有清晰的逻辑链条。
XSS是“攻击者往你页面里塞了恶意脚本”,CSRF是“攻击者借你的手发了请求”,两者经常组合使用。CSP是防XSS的兜底手段,相当于给浏览器立规矩:只准加载我白名单里的脚本。JWT则是身份凭证的载体,一旦泄露或者校验不严,攻击者就能直接冒充用户,绕过前面所有的防线。
理解了这个链条,你在回答时就能串起来讲。比如面试官问“JWT存在localStorage里安全吗”,你可以答:“如果页面存在XSS漏洞,localStorage里的JWT可以被脚本直接读取,所以JWT的安全性强依赖于XSS的防御。这也是为什么很多方案会把JWT放在HttpOnly Cookie里,同时配合CSRF Token使用。”这样答,层次就出来了。
2. XSS考点全解析:从反射型到DOM型
2.1 XSS的三种类型与本质区别
XSS全称Cross-Site Scripting,为了和CSS区分才缩写成了XSS。它的本质就一句话:攻击者让浏览器执行了本不该执行的脚本。根据恶意脚本的“注入路径”和“触发方式”,分成三类。
反射型XSS是最常见的一种。攻击者构造一个带恶意参数的URL,诱导用户点击。服务端把参数原样拼到HTML里返回,浏览器一渲染,脚本就执行了。比如https://example.com/search?q=<script>alert(1)</script>,如果搜索页直接把q的值输出到页面,就中招了。反射型的特点是“一次性的”,不存库,需要诱导点击。
存储型XSS危害最大。恶意内容被存进了数据库,比如评论区、用户昵称、留言板。任何访问这个页面的用户都会触发脚本。CTF里常见的“留言板打管理员Cookie”就是典型场景。存储型的排查难度也最高,因为注入点可能在几个月前就埋下了。
DOM型XSS比较特殊,它不经过服务端。恶意数据直接在浏览器端被JavaScript处理并插入DOM。比如document.getElementById('output').innerHTML = location.hash.slice(1),如果URL的hash里带了<img src=x onerror=alert(1)>,脚本就执行了。DOM型XSS的隐蔽性很强,因为服务端日志里看不到异常,传统的WAF也拦不住。
| 类型 | 注入位置 | 是否经过服务端 | 触发条件 | 危害等级 |
|---|---|---|---|---|
| 反射型 | URL参数 | 是 | 诱导点击 | 中 |
| 存储型 | 数据库 | 是 | 访问页面 | 高 |
| DOM型 | 浏览器端JS | 否 | 特定DOM操作 | 中高 |
2.2 面试高频追问:DOM型XSS怎么防
DOM型XSS是这两年面试的宠儿,因为它能区分出真正理解原理的人。面试官常问:“服务端已经做了转义,为什么还有DOM型XSS?”
答案在于:服务端的转义只对服务端输出的内容生效。如果前端JS从location、document.referrer、window.name这些地方取数据,然后直接塞进innerHTML,服务端的转义根本管不到。数据从头到尾没经过服务端。
防御DOM型XSS,核心是避免把不可信数据当HTML解析。具体做法:
- 能用
textContent就不用innerHTML。textContent会把内容当纯文本,不会解析标签。 - 如果必须用
innerHTML,先做转义。可以用DOMPurify这类库,它专门做HTML清洗,比手写正则靠谱得多。 - 避免
eval、setTimeout(string)、new Function这类动态执行字符串的API。 - 对
location.hash、location.search、document.referrer这些来源的数据保持警惕,使用前先校验。
// 危险写法 const userInput = location.hash.slice(1); document.getElementById('content').innerHTML = userInput; // 安全写法一:用textContent document.getElementById('content').textContent = userInput; // 安全写法二:用DOMPurify清洗 import DOMPurify from 'dompurify'; document.getElementById('content').innerHTML = DOMPurify.sanitize(userInput);注意:
DOMPurify的默认配置已经能挡住绝大多数XSS向量,但如果你的业务允许富文本,需要仔细配置白名单,别图省事直接放开script和on*事件。
2.3 存储型XSS的实战排查思路
面试官如果问你“线上发现存储型XSS,你怎么排查”,这是在考实战能力。我分享一下自己的排查流程。
第一步,确认注入点。看告警里的Payload出现在哪个页面、哪个字段。常见的高危字段有:用户昵称、评论内容、个人签名、文件名、订单备注。
第二步,判断存储位置。是存进了MySQL,还是Redis,还是直接写进了静态文件?不同存储位置的清理方式不一样。
第三步,追溯输入源头。这个字段是从哪个接口进来的?有没有做输入校验?是前端直接提交的,还是经过服务端处理的?
第四步,评估影响范围。有多少条记录被污染?有没有触发过?有没有Cookie或Token泄露的迹象?
第五步,修复与清理。修复分两层:一是清理已存储的恶意数据,二是修补输入输出逻辑。输出层一定要做编码,输入层做校验。如果字段允许HTML,用白名单清洗;如果不允许,直接转义。
这里有个坑:很多团队只修了输出层,没清理数据库里的脏数据。结果老数据一被访问,还是弹窗。所以清理和修复要同步做。
2.4 XSS防御的编码细节
输出编码不是简单地把<换成<就完事了。不同的输出位置,编码规则不一样。
- HTML正文:编码
&、<、>、"、' - HTML属性:除了上述字符,还要注意属性值必须加引号
- JavaScript代码块:不能直接输出用户数据,必须用
JSON.stringify序列化,并且转义<、>、& - URL参数:用
encodeURIComponent - CSS:尽量避免用户数据进入CSS,如果必须,做严格的白名单校验
// HTML实体编码函数 function escapeHtml(str) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return str.replace(/[&<>"']/g, m => map[m]); }面试时如果能说出“不同上下文用不同编码方式”,面试官会知道你确实踩过坑。因为很多XSS漏洞就是因为编码方式用错了,比如在JS上下文里用了HTML编码,照样能被绕过。
3. CSRF考点全解析:从原理到绕过
3.1 CSRF的本质:借刀杀人
CSRF全称Cross-Site Request Forgery,跨站请求伪造。它的本质是:攻击者诱导用户在已登录的网站上执行非预期的操作。
关键点在于“已登录”。用户在某银行网站登录了,Cookie还在有效期内。攻击者做一个恶意页面,里面藏一个表单,自动向银行网站提交转账请求。浏览器会自动带上银行的Cookie,服务端一看Cookie有效,就执行了转账。用户全程无感知。
CSRF和XSS的区别:XSS是“攻击者在你页面里执行脚本”,CSRF是“攻击者借你的浏览器发请求”。XSS能拿到Cookie,CSRF拿不到Cookie,但能利用Cookie。
面试官常问:“CSRF能拿到用户数据吗?”答案是:单纯的CSRF拿不到数据,因为它只能发请求,读不到响应。但如果配合XSS,就能读响应了。所以CSRF通常用来做“写操作”,比如转账、改密码、发消息。
3.2 CSRF防御的三道防线
防御CSRF,业界公认的方案有三层,面试时按这个顺序答,基本满分。
第一层:SameSite Cookie属性。这是最简单也最有效的方案。设置SameSite=Lax或SameSite=Strict,浏览器在跨站请求时就不会带上Cookie。Lax允许顶级导航的GET请求带Cookie,Strict完全禁止。现在主流浏览器默认就是Lax,所以很多CSRF攻击已经天然失效了。
第二层:CSRF Token。服务端生成一个随机Token,嵌入表单或请求头。提交时服务端校验Token是否匹配。攻击者因为同源策略拿不到Token,所以伪造的请求会被拒绝。Token要满足几个条件:随机、一次性或有时效、和用户会话绑定。
第三层:Referer/Origin校验。检查请求的来源域名是否在白名单内。这个方案有局限性,因为Referer可能被隐私设置屏蔽,但Origin头在POST请求里比较可靠。
// 服务端校验CSRF Token的伪代码 function verifyCsrfToken(req) { const tokenFromRequest = req.headers['x-csrf-token'] || req.body._csrf; const tokenFromSession = req.session.csrfToken; if (!tokenFromRequest || tokenFromRequest !== tokenFromSession) { throw new Error('CSRF token mismatch'); } }提示:CSRF Token不要放在Cookie里,否则就失去了意义。放在表单隐藏字段或自定义请求头里,攻击者跨域拿不到。
3.3 CSRF绕过的常见姿势
面试官如果问“CSRF Token能不能被绕过”,这是在考深度。答案是:能,但需要特定条件。
场景一:Token泄露。如果页面存在XSS漏洞,攻击者可以直接读取Token。所以CSRF防御的前提是XSS已经防住了。
场景二:Token未绑定会话。有些实现只校验Token存在,不校验Token属于哪个用户。攻击者用自己的账号获取一个合法Token,然后诱导受害者提交,服务端只检查Token格式,就绕过了。
场景三:GET请求未防护。很多团队只对POST做了Token校验,忽略了GET。如果敏感操作支持GET,比如/transfer?to=xxx&amount=100,攻击者直接用一个<img>标签就能触发。
场景四:子域名问题。如果a.example.com存在XSS,攻击者可以往example.com写Cookie,因为同源策略对子域名的限制较松。这种情况下,主域的CSRF防御可能被绕过。
DVWA靶场的CSRF High级别,就是通过结合XSS来绕过Token校验的。CTF里常见的“打管理员”题目,也是这个思路。
3.4 前后端分离下的CSRF新问题
现在很多项目是前后端分离的,前端用React/Vue,后端用Spring Boot。这种架构下,CSRF的形态变了。
如果认证用的是JWT,并且JWT存在localStorage里,通过Authorization头传递,那CSRF的风险其实降低了。因为攻击者跨域发请求时,浏览器不会自动带上Authorization头。但前提是你不把JWT放在Cookie里。
如果JWT放在Cookie里,那CSRF风险依然存在。这时候需要配合CSRF Token或者SameSite。
还有一种情况:SPA项目用JWT做登录验证,Token续签逻辑没做好。攻击者可以伪造一个续签请求,让用户的Token一直有效。这种攻击比较隐蔽,面试时如果提到,会加分。
// Spring Boot中配置CSRF防护的示例 @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); } }4. CSP考点全解析:内容安全策略
4.1 CSP是什么,为什么需要它
CSP全称Content Security Policy,内容安全策略。它的作用是告诉浏览器哪些资源可以加载、哪些脚本可以执行。你可以把它理解成一份白名单,浏览器只认白名单里的东西。
CSP最大的价值在于兜底。你不可能保证代码里100%没有XSS漏洞,但CSP可以在漏洞被利用时,阻止恶意脚本执行。比如你设置了script-src 'self',那么即使攻击者注入了<script src="https://evil.com/x.js">,浏览器也会拒绝加载。
CSP通过HTTP响应头或<meta>标签来设置。响应头的方式更推荐,因为<meta>标签有一些限制,比如不支持frame-ancestors。
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;面试官问CSP,通常想看你有没有实际配置过。因为CSP的坑很多,配不好会把正常功能搞挂。
4.2 CSP指令详解与配置策略
CSP的指令很多,面试常考的有这几个。
default-src:兜底指令,其他指令没设置时用它。建议设为'self'。
script-src:控制脚本来源。这是最重要的指令。'self'表示只允许同源脚本,'unsafe-inline'允许内联脚本(不建议),'unsafe-eval'允许eval(不建议),'nonce-xxx'允许特定nonce的脚本。
style-src:控制样式来源。'unsafe-inline'在样式里很常见,因为很多框架会动态插入样式。但这也带来了风险,攻击者可能通过CSS注入来窃取数据。
img-src:控制图片来源。通常设为'self' data: https:。
connect-src:控制XHR、WebSocket、fetch的请求目标。这个指令经常被忽略,但很重要。如果设得太松,攻击者可以通过fetch把数据外传。
frame-ancestors:控制页面能被哪些网站嵌入。用来防点击劫持。'none'表示不允许任何网站嵌入。
| 指令 | 作用 | 推荐值 | 风险点 |
|---|---|---|---|
| default-src | 兜底 | 'self' | 设太松等于没设 |
| script-src | 脚本来源 | 'self' + nonce | 'unsafe-inline'风险高 |
| style-src | 样式来源 | 'self' 'unsafe-inline' | CSS注入可窃取数据 |
| connect-src | 请求目标 | 'self' + API域名 | 设太松可外传数据 |
| frame-ancestors | 防点击劫持 | 'none' | meta标签不支持 |
4.3 CSP的nonce和hash机制
'unsafe-inline'是CSP配置里最常见的妥协。很多项目因为用了内联脚本,不得不开这个选项。但开了之后,CSP对XSS的防护就大打折扣了。
更好的方案是nonce或hash。
nonce是服务端每次请求生成一个随机字符串,嵌入到<script nonce="xxx">里,同时在CSP头里声明script-src 'nonce-xxx'。浏览器只执行nonce匹配的脚本。攻击者因为猜不到nonce,注入的脚本会被拒绝。
<!-- 服务端生成的nonce --> <script nonce="d8f7a6b5c4e3"> console.log('this will run'); </script> <!-- 攻击者注入的脚本,没有正确的nonce --> <script>alert('xss')</script>hash是直接计算脚本内容的哈希值,在CSP头里声明。浏览器只执行哈希匹配的脚本。适合静态内联脚本。
Content-Security-Policy: script-src 'sha256-abc123...'nonce的缺点是每次请求都要生成,对缓存不友好。hash的缺点是脚本内容一变,哈希就要重算。实际项目中,nonce更常用。
4.4 CSP的常见绕过与局限
CSP不是万能的,面试官如果问“CSP能不能被绕过”,你要能说出几种情况。
情况一:配置了'unsafe-inline'。这等于给内联脚本开了后门,攻击者直接注入<script>alert(1)</script>就能执行。
情况二:白名单域名存在JSONP接口。如果script-src允许了某个CDN,而这个CDN上有JSONP接口,攻击者可以利用JSONP来执行任意代码。这种绕过在CTF里很常见。
情况三:base-uri未设置。如果页面用了相对路径加载脚本,攻击者可以注入<base href="https://evil.com">,把脚本请求劫持到恶意服务器。
情况四:script-src允许了data:。攻击者可以构造<script src="data:text/javascript,alert(1)">。
情况五:CSP报告模式。如果只设置了Content-Security-Policy-Report-Only,策略不会生效,只会上报违规。有些团队误以为已经开启了防护。
注意:CSP的部署建议先用
Report-Only模式跑一段时间,收集违规报告,确认不影响正常功能后再切换到强制模式。直接上强制模式很容易把线上搞挂。
5. JWT考点全解析:从结构到漏洞
5.1 JWT的结构与验证流程
JWT全称JSON Web Token,是目前最流行的无状态认证方案。它的结构分三部分,用点号分隔:Header.Payload.Signature。
Header是Base64Url编码的JSON,包含算法类型和Token类型。比如{"alg":"HS256","typ":"JWT"}。
Payload也是Base64Url编码的JSON,包含声明(Claims)。标准声明有iss(签发者)、exp(过期时间)、sub(主题)、aud(受众)。自定义声明可以放用户ID、角色等。
Signature是对前两部分的签名,用来防篡改。服务端用密钥对Header.Payload进行签名,客户端拿到Token后,服务端验证签名是否匹配。
// JWT的结构示例 const header = { alg: 'HS256', typ: 'JWT' }; const payload = { userId: 123, role: 'admin', exp: 1735689600 }; // 签名过程(伪代码) const signature = HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret); const token = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + signature;面试官问JWT,第一层是考结构,第二层是考验证流程,第三层是考漏洞。很多人只知道JWT长什么样,但不知道验证时要注意什么。
5.2 JWT的常见漏洞与攻击手法
JWT的漏洞主要集中在算法混淆和密钥管理上。
漏洞一:alg=none。JWT规范里允许alg为none,表示不签名。如果服务端没有严格校验算法,攻击者可以把alg改成none,去掉签名,伪造任意Payload。防御方法很简单:服务端强制指定算法,拒绝none。
漏洞二:HS256与RS256混淆。HS256是对称加密,用同一个密钥签名和验证。RS256是非对称加密,用私钥签名、公钥验证。如果服务端用的是RS256,但验证时没限制算法,攻击者可以把alg改成HS256,然后用公钥作为HMAC密钥来签名。因为公钥是公开的,攻击者能拿到,所以能伪造Token。防御方法是服务端明确指定算法,不信任Header里的alg。
漏洞三:弱密钥。如果HS256的密钥太短或太常见,攻击者可以暴力破解。比如密钥是secret、123456,用hashcat几分钟就能跑出来。防御方法是使用足够长、足够随机的密钥。
漏洞四:kid注入。kid是Header里的一个字段,表示密钥ID。如果服务端用kid来查密钥文件,攻击者可以构造kid为../../etc/passwd,导致路径穿越。或者构造kid为SQL注入语句,如果服务端用kid查数据库,就可能被注入。
漏洞五:Token未校验过期。有些实现只验证签名,不检查exp。攻击者可以拿一个过期的Token继续用。防御方法是验证时检查exp、nbf、iat等时间声明。
| 漏洞类型 | 攻击手法 | 防御措施 |
|---|---|---|
| alg=none | 去掉签名 | 强制指定算法 |
| 算法混淆 | HS256冒充RS256 | 服务端固定算法 |
| 弱密钥 | 暴力破解 | 使用强密钥 |
| kid注入 | 路径穿越/SQL注入 | 校验kid格式 |
| 过期未校验 | 重用旧Token | 检查exp声明 |
5.3 JWT的存储位置与安全性
面试官常问:“JWT应该存在哪里?”这个问题没有标准答案,但你要能说出权衡。
localStorage:方便,前端可以直接读取。但XSS能直接偷走。如果页面有XSS漏洞,Token就没了。
sessionStorage:和localStorage类似,但关闭标签页就清空。安全性略好,但XSS依然能偷。
HttpOnly Cookie:JavaScript读不到,XSS偷不走。但会带来CSRF风险,需要配合CSRF Token或SameSite。另外,Cookie有大小限制,JWT如果太大可能放不下。
内存:存在JS变量里,刷新页面就丢。安全性最高,但用户体验差,需要配合Refresh Token。
实际项目中,比较常见的方案是:Access Token存在内存或sessionStorage,Refresh Token存在HttpOnly Cookie。Access Token短期有效,Refresh Token长期有效但只能用来换新Token。
// 前端存储JWT的示例 // 不推荐:localStorage localStorage.setItem('token', jwt); // 推荐:内存 + Refresh Token let accessToken = null; function setToken(token) { accessToken = token; } // Refresh Token由服务端通过HttpOnly Cookie下发提示:不管存在哪里,JWT的安全性强依赖于XSS的防御。如果XSS防不住,存哪都不安全。所以安全是一个整体,不能只盯着一个点。
5.4 JWT续签与注销的工程实践
JWT是无状态的,服务端不存Session。这带来了一个工程难题:怎么注销Token?用户点了退出登录,但Token还在有效期内,攻击者拿到旧Token还能用。
常见的解决方案有几种。
方案一:短期Access Token + 长期Refresh Token。Access Token有效期设短一点,比如15分钟。Refresh Token有效期长,比如7天。退出登录时,服务端把Refresh Token加入黑名单。Access Token过期后,攻击者就用不了了。这个方案牺牲了一点实时性,但工程上最常用。
方案二:Token黑名单。服务端维护一个黑名单,存已注销的Token。每次验证时查黑名单。缺点是失去了无状态的优势,黑名单本身要存储和查询。
方案三:版本号机制。用户表里存一个tokenVersion,签发Token时把版本号放进去。注销时版本号加一。验证时对比版本号,不匹配就拒绝。这个方案比较优雅,但需要查库。
// Spring Security整合JWT的续签逻辑(伪代码) public String refreshToken(String refreshToken) { if (isBlacklisted(refreshToken)) { throw new RuntimeException("Token已注销"); } Claims claims = parseToken(refreshToken); String newAccessToken = generateAccessToken(claims.getSubject()); return newAccessToken; }面试时如果能把续签和注销讲清楚,说明你确实在项目里用过JWT,而不是只背了概念。
6. 安全防御的工程化落地
6.1 全局过滤器处理XSS的实践
在实际项目里,防XSS不能靠每个开发手动转义,那样迟早会漏。比较靠谱的做法是全局过滤器。
以Spring Boot项目为例,可以写一个XssFilter,对所有请求参数做清洗。对于普通表单字段,直接转义HTML特殊字符。对于富文本字段,用白名单清洗,保留允许的标签。
// Spring Boot全局XSS过滤器示例 @Component public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(req); chain.doFilter(wrapper, response); } } public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) return null; return Arrays.stream(values).map(this::cleanXss).toArray(String[]::new); } private String cleanXss(String value) { // 转义HTML特殊字符 return value.replaceAll("<", "<") .replaceAll(">", ">") .replaceAll("\"", """) .replaceAll("'", "'"); } }这里有个坑:上传PDF文件时的XSS。如果PDF文件名或内容里带了恶意脚本,而前端直接渲染,可能触发XSS。处理方式是:上传时校验文件类型和内容,下载时设置Content-Disposition: attachment,强制下载而不是预览。如果必须预览,用PDF.js这类库,并且设置CSP。
6.2 CSP与富文本白名单的配合
富文本是XSS的重灾区。用户需要加粗、斜体、链接、图片,这些都需要HTML标签。但一旦允许HTML,XSS的风险就来了。
比较稳妥的方案是服务端白名单清洗 + CSP兜底。
服务端用jsoup或OWASP Java HTML Sanitizer做白名单清洗,只保留允许的标签和属性。比如只允许<b>、<i>、<a>、<img>,并且<a>的href必须是http或https开头,<img>的src必须是白名单域名。
CSP层面,设置script-src 'self',禁止内联脚本。这样即使清洗漏了,恶意脚本也执行不了。
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://trusted-images.com; object-src 'none';注意:
object-src 'none'可以禁止Flash等插件,减少攻击面。base-uri 'self'可以防止base标签劫持。
6.3 安全开发的检查清单
最后分享一份我在项目中常用的安全检查清单,面试时如果被问到“你怎么保证前端安全”,可以按这个思路答。
- 所有用户输入都做校验,前端校验为了体验,服务端校验为了安全。
- 所有输出都做编码,根据上下文选择HTML编码、JS编码、URL编码。
- 富文本用白名单清洗,不用黑名单。
- 设置CSP,至少
script-src 'self',禁用unsafe-inline。 - Cookie设置
HttpOnly、Secure、SameSite。 - 敏感操作加CSRF Token,校验Referer/Origin。
- JWT设置合理的过期时间,验证时检查
exp,强制指定算法。 - 第三方脚本用
integrity属性做SRI校验。 - 定期做安全扫描,关注依赖库的漏洞公告。
安全不是一次性的工作,而是持续的过程。每次代码评审时多问一句“这里有没有注入风险”,比事后补救强得多。
6.4 面试中的安全场景题应对
面试官有时候会出场景题,比如:“你负责一个电商网站的前端,用户可以在商品详情页留言。你怎么设计安全方案?”
这种题没有标准答案,但你可以按层次来答。
第一层,输入校验。留言内容限制长度,过滤敏感词,服务端校验用户身份。
第二层,输出编码。留言渲染时,如果允许富文本,用白名单清洗;如果不允许,直接转义。
第三层,CSP兜底。设置script-src 'self',防止漏网的XSS。
第四层,CSRF防护。留言接口加CSRF Token,校验来源。
第五层,监控与告警。记录异常输入,设置告警规则,发现攻击及时响应。
这样答下来,面试官会觉得你有体系化的思维,而不是零散地背知识点。
我在实际面试中,最怕遇到那种“一问XSS就背定义,一问防御就说转义”的候选人。安全考的是思维,是你在写代码时有没有把攻击者放在心里。你不需要成为安全专家,但你需要知道常见的坑在哪,以及怎么用工程手段把坑填上。