资讯动态

Flask CSRF防护实战:从攻击原理到CSRFProtect落地

发布时间:2026/10/16 7:01:49 来源:尧图企业网站定制
我之前接手过几个Flask项目几乎每次做安全审查CSRF都是必聊话题。有意思的是很多开发者第一反应是我项目又不算大谁会特意来攻击我可等到安全测试报告出来第一页往往就是CSRF漏洞而且不少还是高危。这篇从攻击原理讲到Flask项目里的完整防护落地适合正在写Flask应用、特别是用服务端渲染加表单提交的团队参考如果你正在搭前后端分离接口里面也有对应方案可以借鉴。1. 先弄清CSRF攻击从哪一刻开始生效——登录态被“借用”的真相1.1 一个老旧的攻击场景现在依然存在假设用户登录了某电商网站账号信息存在Cookie里浏览器只要访问该网站就会自动带上Cookie。这时候用户又打开了另一个站点那个页面里有一张“图片”图片的地址其实是https://example.com/cart/delete?item_id1如果电商网站把删除操作做成了GET请求用户的购物车数据就在毫秒之间被删掉了用户自己还没点任何东西。这就很离谱但根因不在“删除为什么用了GET”而在服务器默认信任了“Cookie能证明用户身份”这件事。有人可能说那我把删除操作改成POST不就行了问题没这么简单。攻击者照样可以做隐藏表单页面加载时用JavaScript自动提交form actionhttps://example.com/cart/delete methodPOST styledisplay:none input nameitem_id value1 /form scriptdocument.forms[0].submit();/script跨域表单提交并不会受到同源策略的限制浏览器会照常把请求发出去Cookie也照常带上。服务器收到这个POST请求时看到的只是“一个持有合法Cookie的用户发起了一个POST请求”它根本不知道这个请求是不是用户本人在页面上主动操作的结果。这就是CSRF攻击的核心逻辑。1.2 Flask收到请求后为什么“看不见”请求来源Flask处理请求的默认流程是路由匹配、请求方法检查、视图函数执行。这个流程里默认没有任何环节会去验证“这个请求是不是从用户自己的页面里发出的”。Cookie本身只能证明“这个浏览器之前登录过”不能证明“这个请求是用户主动发起的”。这是Web早期的历史包袱。协议设计之初就没有在请求层面上强制要求来源校验Origin和Referer头虽然存在但服务端默认不去检查。再加上很多浏览器会隐藏或裁剪Referer所以“靠检查Referer判断来源”也不可靠。后果就是服务器把浏览器自动携带的Cookie当成了用户的“本人意愿”。这里还要顺带区分一下CSRF 和 XSS 不是一回事。XSS 是往页面里注入恶意脚本一旦被注入成功CSRF Token 也会跟着被偷走所以不要因为做了CSRF防护就觉得可以放松对XSS的警惕。CSRF 防的是“跨站伪造请求”XSS 防的是“恶意脚本注入”两层都要做才算完整。2. 方向选择默认用CSRFProtect还是自己维护一套Token机制2.1 CSRFProtect在背后做了什么Flask生态里最常用的现成方案是 flask-wtf 里的 CSRFProtect。它的工作方式大致是这样用户第一次请求页面时如果Session里没有csrf_token就生成一个随机Token存进去。模板渲染时把这个Token输出到表单隐藏字段或页面里。用户提交表单或发AJAX请求时CSRFProtect在 before_request 阶段做拦截检查请求携带的Token和Session里的Token是否一致。不一致或缺失直接返回403。关键点在于Token存在服务端Session里攻击者的跨站请求拿不到这个值。攻击者虽然能自动提交表单但没法读取目标站点返回的页面内容也就拿不到随机Token校验就过不了。我用代码演示一下接入后的体验from flask import Flask from flask_wtf import CSRFProtect app Flask(__name__) app.config[SECRET_KEY] your-random-secret-here csrf CSRFProtect(app)这段代码加完所有 POST/PUT/PATCH/DELETE 请求默认都会走 CSRF 校验。之后只需要确保每个表单都带Token或者每个AJAX请求都带自定义请求头就能正常通过。2.2 自己实现Token校验时必须想清楚的四个问题网上也有团队选择自己写比如登录时生成Token存Session前端拿到后放请求头。这套思路本身没问题但我见过很多自己实现的版本漏洞往往出现在细节上第一Token有没有足够随机性。有的实现干脆用时间戳或者用户ID拼一个串攻击者如果知道生成算法可以直接预测Token。正确做法是使用密码学安全的随机源比如 Python 的secrets.token_urlsafe(32)。第二Token存哪、怎么比较。建议存服务端Session比较的时候用安全方式。Python里可以用secrets.compare_digest()做常数时间比较避免通过响应时间差异来猜Token。第三Token要不要过期。如果Token长期不失效等于把钥匙一直挂在门口。建议给Token附带过期时间比如15分钟到2小时过期后要求重新生成。CSRFProtect的默认过期时间是3600秒你可以按业务调整。第四校验逻辑放在哪个阶段。一般靠在 before_request 钩子里做全局拦截但要注意未登录用户访问登录页时也会触发校验需要把登录页请求排除掉。这个环节没设计好很容易出现“登录页都进不去”的尴尬局面。2.3 几种常见方案的适用边界对比我把实际项目里考虑过的方案整理成一个表格方便对照选型方案原理主要优点主要缺点推荐场景CSRFProtectSession TokenToken存Session表单字段或请求头回传接入快、生态成熟、服务端可控性强依赖Session对无状态接口不友好服务端渲染、传统表单项目自定义Header Token客户端保存Token每个请求加自定义请求头不依赖Session前后端分离友好前端配合较多跨域需要配置CORS单页应用、移动端APICookie双重提交Token写在Cookie中前端读取后放进自定义Header回传无Session负担实现简单依赖自定义Header低版本浏览器兼容性一般分布式服务、多实例部署我的建议很简单如果是典型 Flask Jinja2 模板项目直接上CSRFProtect别自己造轮子如果项目是前后端分离接口同时给App和网页用就用自定义请求头方案但要把Token生成、过期、刷新都当成正式功能来设计。至于“什么都不做”这个选项不推荐除非你非常清楚自己在干什么。3. 从零接入CSRFProtect的落地步骤配置、模板与常见报错3.1 安装与初始化代码先装依赖pip install flask-wtf初始化方式有两种。一种是应用创建时直接绑定from flask import Flask from flask_wtf import CSRFProtect app Flask(__name__) app.config[SECRET_KEY] 需要替换成足够复杂的随机串 csrf CSRFProtect(app)另一种是延迟初始化方便配合工厂模式或写测试csrf CSRFProtect() def create_app(): app Flask(__name__) app.config[SECRET_KEY] 需要替换成足够复杂的随机串 csrf.init_app(app) return app不管哪种方式SECRET_KEY必须配置。CSRFProtect会用SECRET_KEY或独立的WTF_CSRF_SECRET_KEY做签名基础没设置的话启动时就会报错。3.2 模板表单里的Token怎么渲染如果用的是 Flask-WTF 的 Form 类模板里通常会有hidden_tag()自动输出隐藏字段。但很多项目用的是纯HTML表单这时候需要手动加隐藏inputform methodpost action/orders/create input typehidden namecsrf_token value{{ csrf_token() }} !-- 其他表单项 -- button typesubmit提交/button /form提交后CSRFProtect会优先读取表单字段里的csrf_token然后和Session里的Token比对。打开页面源码你应该能看到隐藏input里有一长串随机值。看不到就说明模板写错了。另一个常见误区有人以为只要{{ csrf_token() }}出现在页面里就行其实它必须出现在被提交的表单请求体里。如果表单是动态生成的或者页面用了局部加载Token没有跟着表单走提交时照样报“The CSRF token is missing”。3.3 CSRFProtect的配置项与豁免时机CSRFProtect提供了一些配置项时间久了容易忘。我把我常用的整理一下WTF_CSRF_ENABLED默认为 True。测试环境临时关闭时设成 False但千万别带到生产环境。WTF_CSRF_SECRET_KEY如果不想直接用SECRET_KEY可以单独设置CSRF签名密钥。WTF_CSRF_TIME_LIMITToken有效期单位秒默认3600秒。WTF_CSRF_SSL_INVALIDHTTPS环境下如果请求的 Referer 与站点不一致是否直接拦截。官方建议在HTTPS下拒绝但某些反向代理或短链场景下Referer可能会丢失导致用户莫名403。这种情况需要评估代理配置而不是急着关掉校验。WTF_CSRF_METHODS默认保护 [POST, PUT, PATCH, DELETE]可按需调整。豁免某个视图可以这样写app.route(/callback, methods[POST]) csrf.exempt def payment_callback(): # 处理外部回调 return ok豁免场景不是没有但要认真评估。像外部系统主动回调我们、而不是浏览器用户提交请求的场景确实不适合用页面CSRF Token但即便如此我也更建议用签名校验或固定Token替代而不是简单把CSRF校验关掉后面第6章会展开讲。4. 最容易被卡住的地方AJAX请求403了我是怎么一步步查的4.1 故障复现同一个表单POST和AJAX结果不一样经典情况是这样的普通表单提交一切正常内容也能保存但前端把提交方式改成AJAX后接口直接返回403。原因很直接——普通表单提交时浏览器会把表单里的csrf_token隐藏字段一起POST上去但AJAX请求默认不会自动带任何Token除非你手动加请求头或请求体。我在实际项目里还遇到过一个更隐蔽的场景页面引用了外部JS文件JS文件里有发请求的逻辑但那个JS文件不是通过Jinja2模板渲染出来的拿不到{{ csrf_token() }}。于是每次请求都缺Token。这类问题用DevTools一看就是“The CSRF token is missing”但第一次排查时很容易误判成“模板没渲染成功”实际上模板渲染得没问题。4.2 按顺序排查的完整链路第一次碰这种403别瞎猜按下面顺序走一遍第一步确认Flask日志或响应体。CSRFProtect拦截时响应体一般会写The CSRF token is missing.或者The CSRF token has expired.。先区分是缺失还是过期两个处理方向完全不一样。第二步打开浏览器DevTools的Network面板找到出错的请求查看请求头。AJAX场景看有没有X-CSRFToken表单提交场景看Form Data里有没有csrf_token。第三步看响应头的Set-Cookie确认服务端是否真的给浏览器种下了csrf_token的Cookie。有时候跨站场景需要设置SameSiteNone; Secure才能让Cookie正确携带这个细节很容易漏。第四步回到页面源码搜索csrf_token。如果页面里根本没渲染那是模板问题如果渲染了但JS取不到那就是取值路径的问题。第五步注意多域名或多端口场景。本地开发时localhost:5000和127.0.0.1:5000会被当成不同站点Cookie作用域不同请求就带不上接口就会一直403。4.3 稳定可复用的前端发送方案想让前端稳定带上Token最省事的方式是加载页面时先输出一个meta标签meta namecsrf-token content{{ csrf_token() }}然后在全局请求封装里统一读取并加请求头。用jQuery可以这样写$.ajaxSetup({ beforeSend: function(xhr) { xhr.setRequestHeader(X-CSRFToken, $(meta[namecsrf-token]).attr(content)); } });用fetch可以这样写const csrfToken document.querySelector(meta[namecsrf-token]).getAttribute(content); fetch(/api/orders, { method: POST, headers: { Content-Type: application/json, X-CSRFToken: csrfToken }, body: JSON.stringify({ item_id: 1 }) });这样处理后每个页面只要渲染了正确的meta标签前端所有AJAX请求都会自动带上Token。单页应用要注意的是页面局部刷新或路由切换后如果meta标签被覆盖或删除Token就会变成旧值或空值。所以SPA项目我更建议把Token放到全局状态里请求拦截器从状态里取值而不是每次都读DOM。5. 前后端分离与接口场景下的CSRF策略调整5.1 不用Session时的Token方案如果接口是给小程序、App、前端页面共用的依赖Session就不现实。这时候的CSRF思路要切换成“客户端保存Token每次请求放进自定义请求头”。流程大致是用户登录后服务端返回一个随机Token同时也可以种到Cookie里做兼容方案。客户端把Token保存下来后续每次请求都在X-CSRFToken头里带上。后端在需要校验的请求前检查请求头里是否存在合法Token。这里有个容易被忽略的点自定义请求头本身就有防护价值。因为浏览器跨域AJAX请求只要带了自定义请求头会先触发一次OPTIONS预检请求跨站页面默认无法通过预检因此攻击者的跨站AJAX会被挡在门外。原生表单提交又无法带自定义请求头等于这层防线天然有效。但要提示一下预检机制的副作用是跨域部署时必须配置好CORS的Access-Control-Allow-Headers否则正经客户端也会被预检挡下来前端报的就会是CORS错误而不是业务错误。5.2 SameSite属性正在帮我们挡住什么近年浏览器默认把Cookie的SameSite设置成Lax跨站POST请求默认不携带Cookie。这意味着经典的“跨站表单自动提交CSRF攻击”在现代浏览器里已经少了一大半。如果接口只在同一站点内部使用CSRF攻击面确实缩小了。但我不建议把这个当作唯一防线。原因有几个第一部分企业内网还是老版本浏览器对SameSite支持参差不齐。第二SameSite与Chrome等浏览器对“Site”的定义在不同版本间调整过边界情况容易踩坑。第三如果系统内部存在XSS攻击者可以从当前页面直接发起同源请求这部分SameSite管不到。第四有些业务场景必须支持第三方嵌入或子域交互SameSiteLax不一定符合业务需要。所以我的态度是SameSite可以设是很好的纵深防御但该做的Token校验不要省。5.3 登录态设计中的CSRF提示前后端分离项目里登录态通常不是Session而是AccessToken或JWT。这类设计天然规避了一部分CSRF因为请求头里的Token不会被浏览器自动携带但Token本身的存放位置要约束好。不要放在URL参数里查询字符串会进访问日志和浏览器历史一旦泄露就是长期身份凭证也不要放在localStorage里虽然方便但任何XSS脚本都能直接读取。相对更好的做法是放在内存变量里页面刷新后重新走静默登录或刷新Token流程再次一点的方案是放到httpOnlyCookie里再结合CSRF Token机制做双重校验。每次登录成功建议重新生成Token退出时清空。表单提交后如果校验失败要区分是Token过期还是用户信息过期分别给前端不同的错误码排查问题时会省很多力气。6. 容易被忽略的安全边界和维护建议6.1 容易被忽视的几点边界CSRF防护不是把CSRFProtect装上就完事了。有几个边界情况我见过团队翻车专门拿出来说一是GET请求改变状态。CSRF保护默认不检查GET但如果你在视图里用GET做了删除、改状态这类操作攻击者只需要一个图片链接就能触发。这其实不是CSRF防护的问题是接口设计的问题必须整改成POST/DELETE。二是Token出现在不该出现的地方。某些团队为了调接口方便把Token直接放在前端接口文档里或者放在公共JS常量里这种等于没有Token。Token必须和用户Session绑定或者至少是每个用户独立生成全局共享一个Token没有任何防护意义。三是校验失败时直接返回403前端拿403后没有统一处理用户一脸懵。我建议在before_request里捕获CSRF校验异常统一返回JSON或跳转到错误页并把失败原因记录到日志方便后续做风控分析。四是外部回调豁免过多。接支付、物流、推送这类外部回调时很多人图省事直接csrf.exempt。如果回调接口被探测到且没有任何签名校验攻击者可以伪造回调内容。我的习惯是回调仍然走“签名加时间戳防重放”的校验而不是单纯靠豁免来避开CSRF。6.2 我在项目迭代中沉淀下来的检查习惯每次给Flask项目加或查CSRF防护我基本按这个清单过一遍效率很高检查SECRET_KEY或WTF_CSRF_SECRET_KEY是否用环境变量或密钥管理服务保存而不是硬编码在仓库里。查看所有受保护视图的入口确认是否都经过CSRF校验有没有exempt标记并写明豁免理由。用浏览器打开关键页面查看源码确认csrf_token正常渲染。用脚本或DevTools模拟一次不带Token的POST确认返回403再用带Token的POST确认正常通过。检查前端是否有一个统一请求封装所有AJAX请求是否都带了请求头。检查日志系统里有没有统计CSRF失败次数突然大量失败可能意味着有人在扫接口。如果团队有自动化测试我也会给CSRF相关接口加一条用例先GET页面解出Token再提交表单确保“真实用户走流程”这条路是通的。6.3 当CSRF不再适用回调接口与签名校验最后说一种“不需要CSRF”的接口。CSRF的本质是“确认请求来自可信页面”但有些接口的调用方不是浏览器页面而是第三方服务器。比如支付回调、物流轨迹推送、定时任务触发。这种情况下第三方服务器没有浏览器Cookie和Token的概念做CSRF校验没有实际意义。这类接口的正确做法是签名校验双方约定一个密钥请求方把参数和时间戳一起算出签名放在请求里服务端用同样方式重算一遍一致才放行。再加上时间戳防重放比如只允许5分钟内的请求同时记录请求中的随机数避免重复播放同一请求。这套方案在Flask里实现很简单本质上就是几个装饰器的事但效果比“豁免CSRF后裸奔”可靠得多。我在实际项目操作里的体会是安全防护最怕的不是技术太难而是“这里没人管”的空白地带。CSRF防护的价值不是阻止一些有多高明的黑客而是让自动化扫描工具扫不出明显的低危漏洞让系统不至于在最基础的环节被攻破。把CSRF防线搭好再把上面这些边界检查一遍Flask项目的安全底子基本就立住了。

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

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

免费获取报价 →
↑