Python后端开发里,安全漏洞很少来自语言本身的缺陷,更多来自开发者对框架特性、库行为和HTTP协议细节的误判。很多人以为用了Django或Flask就天然安全,实际上框架只挡住了最基础的注入和CSRF,大量问题出现在反序列化、依赖管理、日志处理和接口设计这些容易被忽略的环节。下面直接讲几个高频出事点以及对应的避坑方法。
反序列化是重灾区,不要随便loads外部数据Python标准库pickle的文档写得很清楚:不要对不信任的数据进行unpickle。但实际项目里还是有人用pickle做缓存、session存储或任务队列的消息体。攻击者只要构造一个恶意序列化对象,就能在反序列化时执行任意系统命令。如果你必须用序列化,优先选JSON,它只处理基础数据类型,没有代码执行能力。如果业务需要传输复杂对象,可以用MessagePack或Protocol Buffers,并在反序列化前做签名校验。对于Redis、Celery这类组件,确认它们没有使用pickle作为默认序列化器,必要时显式改成JSON。
SQL拼接的变体:ORM里的raw和extra同样危险ORM能防住大部分SQL注入,但很多开发者在需要复杂查询时会用raw SQL、extra()或RawSQL表达式,并且习惯性地用f-string拼接参数。例如:
User.objects.raw(f"SELECT * FROM user WHERE name = '{username}'")
这种写法绕过了ORM的参数化查询保护。正确做法是始终使用参数占位符,Django的raw支持%s传参,SQLAlchemy使用:name绑定变量。另一个隐蔽点是order_by字段,如果排序字段名来自前端,直接拼进查询字符串同样会注入。建议维护一个白名单,只允许固定字段名进入排序逻辑。
依赖漏洞比代码漏洞更容易被忽略Python项目的requirements.txt或pyproject.toml经常锁定了过旧的版本,而很多安全事故的根因是第三方库存在已知CVE。比如旧版Pillow的图像处理漏洞、旧版PyYAML的任意代码执行、旧版Requests的代理泄露问题。每次上线前至少跑一次pip-audit或safety check,把漏洞扫描纳入CI流程。不要只看直接依赖,间接依赖同样会引入风险。对于长期不维护的库,尽快找替代品,而不是指望它永远不出事。
JWT和Session:不要自己发明token校验逻辑JWT本身不加密,只做签名。很多开发者误以为把用户ID放进payload就安全了,结果alg=none攻击、弱密钥爆破、token不过期等问题层出不穷。使用PyJWT时,必须显式指定算法列表,禁止none算法,并且验证exp、iss、aud等声明。密钥不要硬编码在代码里,至少从环境变量或密钥管理服务读取。Session方面,Flask的secret_key如果太弱,攻击者可以伪造session数据。生成密钥时用secrets.token_hex(32)级别的随机性,并且不同环境使用不同密钥。
文件上传:扩展名和Content-Type都不可信处理上传文件时,不能只检查文件扩展名或前端传来的MIME类型。攻击者可以上传一个名为shell.jpg的文件,内容却是Python脚本或PHP代码。如果后续有本地文件包含或静态文件服务配置不当,就可能被执行。正确做法是:限制文件大小、使用imghdr或python-magic检测真实文件类型、重命名文件为随机字符串、存储到专门的目录并禁止该目录执行脚本权限。如果文件要公开访问,最好走对象存储并设置Content-Disposition为attachment,避免浏览器直接渲染。
日志记录:敏感信息脱敏是基本要求很多后端在记录请求日志时,会把整个request body、header甚至cookie打进去。密码、token、身份证号、手机号就这样明文落盘,一旦日志系统被入侵或误发到错误位置,就是批量泄露。建议在日志中间件里统一做字段过滤,对password、token、authorization、cookie等键值进行打码或删除。另外,日志级别要合理,debug级别不要在生产环境开启,避免堆栈信息暴露内部路径和代码结构。
SSRF:服务端请求伪造比想象中更容易触发凡是接受URL并让服务端去请求的功能,比如远程图片加载、Webhook回调、RSS订阅抓取,都可能被利用来探测内网。攻击者提交http://169.254.169.254/latest/meta-data/就能拿到云服务器元数据。防护上要做三件事:禁止用户输入IP形式的地址、解析域名后检查IP是否属于内网段、限制请求协议只允许http/https。Python的requests库默认会跟随重定向,所以还要禁止自动重定向,否则攻击者可以用302跳转绕过第一层检查。
模板注入:不要用render_template_string处理用户输入Jinja2的沙箱不是安全边界,很多开发者以为在沙箱里渲染用户提供的模板就没问题,实际上沙箱逃逸的利用链已经公开多年。只要用户能控制模板内容,就可能通过{{ config.__class__.__init__.__globals__ }}这类方式拿到os模块并执行命令。如果你的业务确实需要用户自定义模板,建议换用专门的模板引擎如Liquid或Mustache,它们不支持任意Python表达式。否则就彻底禁止用户输入进入模板渲染流程。
并发与竞态条件:Python的GIL不解决业务原子性很多人认为Python有GIL,多线程不会出数据竞争问题。实际上GIL只保证字节码层面的线程安全,业务逻辑里的“先查后改”依然会出问题。例如两个请求同时读取库存为1,然后都执行扣减操作,最终库存变成-1。这种问题在秒杀、优惠券领取、余额扣减场景里非常常见。解决方法是使用数据库行锁(select_for_update)、原子更新语句(UPDATE ... SET stock = stock - 1 WHERE stock > 0)或Redis的原子操作。不要依赖Python层面的锁,因为多进程部署时锁不共享。
错误处理:异常信息不要直接返回给前端生产环境如果开启了DEBUG模式,Django或Flask会把完整的traceback返回给浏览器,里面包含文件路径、数据库表结构、第三方库版本等信息。即使关闭DEBUG,自定义的异常处理器也经常把e.__str__()直接返回给用户,可能泄露SQL语句或内部逻辑。建议统一捕获异常,返回模糊的错误提示,详细错误只记录到服务端日志。对于API接口,返回结构化的错误码和通用消息,不要把异常堆栈放进response。
CSRF和CORS:不要因为前后端分离就放松前后端分离项目中,很多人觉得用token鉴权就不需要CSRF防护了,因为token放在header里不会被自动携带。但如果你同时使用了cookie作为认证方式,或者token存储在localStorage但请求通过表单提交,CSRF风险依然存在。正确做法是:如果认证完全依赖Authorization header,可以不做CSRF token,但要确保CORS配置严格,只允许白名单域名,且不允许携带cookie。如果使用了cookie session,则必须启用CSRF中间件,并在前端统一处理CSRF token。
加密与哈希:密码存储和敏感数据加密要分开密码存储必须使用专门的哈希算法,如bcrypt、argon2或scrypt,而不是MD5、SHA1甚至SHA256加盐。后者的计算速度太快,容易被暴力破解。Python的bcrypt库和passlib都提供了简单接口。对于需要可逆加密的敏感数据,比如API密钥、支付信息,使用cryptography库的Fernet或AES-GCM模式,密钥从KMS或环境变量获取,不要硬编码。注意区分哈希和加密的适用场景,密码永远不要可逆。
HTTP安全头:基础但有效很多Python后端没有设置安全响应头,导致点击劫持、MIME嗅探、XSS等问题被放大。至少要在中间件里统一加上:X-Content-Type-Options: nosniff、X-Frame-Options: DENY、Content-Security-Policy: default-src 'self'、Strict-Transport-Security。Django可以通过SECURE_开头的配置项快速开启,Flask可以使用flask-talisman库。这些配置成本极低,但能挡住不少低级别的攻击。
定时任务和后台脚本同样需要安全审查安全审查往往集中在Web请求链路,但定时任务、消息消费者、数据迁移脚本同样处理外部数据。如果这些脚本直接读取数据库里的用户输入并执行危险操作,攻击者可以先通过正常接口写入恶意数据,再等定时任务触发。例如一个定时任务读取用户填写的URL并下载文件,就构成了延迟SSRF。所有处理外部数据的代码路径,无论是否直接暴露HTTP接口,都要做输入校验和权限检查。
安全编码不是一次性工作,而是贯穿开发、测试、部署的持续过程。Python的灵活性给了开发者很多便利,但也意味着需要更主动地去理解底层行为和攻击面。把上面这些点落实到代码审查清单里,比事后修复漏洞的成本低得多。
