"我的应用很小,谁会攻击我?"——攻击者不挑食:自动化扫描器全天候扫全网,一个小漏洞就可能被批量利用(挖矿、钓鱼、拖库)。安全不是"大公司的事",而是默认习惯。
直觉类比:安全像"锁门"——不是因为小偷一定会来,而是不锁门的代价远大于锁门的成本。Flask 的默认设置已经帮了不少忙,但你要知道"哪些是默认安全的,哪些要自己负责"。
💡 关键直觉:安全的三个默认原则——不信任任何输入、转义一切输出、最小化暴露。所有攻击都是这三条被违反后的结果。
攻击:用户提交 <script>alert(document.cookie)</script>,若未转义直接渲染,脚本在他人浏览器执行,可窃取 Cookie、篡改页面。
防御:Jinja2 默认自动转义——{{ user_input }} 输出的 < > 会被转成实体,浏览器按文本显示而非执行。
# 默认安全 return render_template('show.html', content=user_input) # 模板里 {{ content }} —— 自动转义,脚本不会执行 # ⚠️ 危险:|safe 或 Markup() 关闭了转义 {{ content | safe }} # 除非内容可信,否则不要用 {{ form.content(value=...) }} # WTForms 也自动转义
要点:
{{ }} 而非 |safe;url_for 生成链接时参数也会被转义。攻击:用户登录了你的站,被诱导访问恶意页面,页面自动向你的站发 POST(如改密码)——浏览器自动带 Cookie,服务器误认为本人操作。
防御:Flask-WTF 的 CSRF token(3.3 已讲)——表单带一次性 token,服务端校验。全局开启 + 模板渲染 hidden_tag。
from flask_wtf import CSRFProtect csrf = CSRFProtect() csrf.init_app(app) # 所有 POST 表单必须带 token;测试配置才允许关闭
攻击:字符串拼接 SQL,输入 ' OR '1'='1 绕过校验或删表。
防御:永远用 ORM/参数化查询,绝不拼字符串。
# 危险 User.query.filter_by(name=f"'{name}'") # 或 db.session.execute(f"SELECT...{name}") # 安全:ORM 自动参数化 User.query.filter_by(name=name).first() # 原生 SQL 也要参数化 db.session.execute( text('SELECT * FROM users WHERE name = :name'), {'name': name} )
| 泄露点 | 风险 | 防御 |
|---|---|---|
| SECRET_KEY 提交到 Git | 会话可被伪造 | 环境变量 + .env 不入库 |
| debug=True 上线 | 调试器可 RCE | 生产强制关闭 |
| 错误页带堆栈 | 暴露代码结构 | 生产用通用错误页,日志单独记录 |
| 日志打印密码/Token | 凭据泄露 | 日志脱敏 |
| API 返回整表字段 | 泄露 password_hash | 序列化白名单 |
app.config.update( SESSION_COOKIE_HTTPONLY=True, # JS 无法读取(防 XSS 偷 Cookie) SESSION_COOKIE_SECURE=True, # 仅 HTTPS 发送 SESSION_COOKIE_SAMESITE='Lax', # 限制跨站携带(CSRF 辅助防御) REMEMBER_COOKIE_HTTPONLY=True, REMEMBER_COOKIE_SECURE=True, )
from flask_talisman import Talisman csp = { 'default-src': ["'self'"], 'script-src': ["'self'"], 'style-src': ["'self'", 'https://fonts.googleapis.com'], } Talisman(app, force_https=True, # 强制 HTTPS content_security_policy=csp, # CSP 限制脚本来源 strict_transport_security=True) # HSTS
常用安全头一览:
| 头 | 作用 |
|---|---|
| X-Frame-Options | 防点击劫持(禁止 iframe 嵌入) |
| Strict-Transport-Security | 强制 HTTPS |
| Content-Security-Policy | 限制可加载的脚本/资源来源 |
| X-Content-Type-Options | 防 MIME 嗅探 |
generate_password_hash(自动加盐 pbkdf2),禁止 md5/sha1;pip-audit # 检查依赖已知漏洞 pip install --upgrade flask # 保持 Flask 与扩展最新
| 误区 | 现象 | 正解 |
|---|---|---|
| 认为小站没人攻击 | 被扫描器批量利用 | 默认安全习惯 |
| 滥用 |safe | XSS 漏洞 | 默认转义,白名单净化 |
| 忘记 CSRF | 伪造请求得逞 | 全局 CSRFProtect |
| 拼字符串 SQL | 注入 | ORM 参数化 |
| 密钥提交 Git | 会话伪造 | 环境变量 |
| 生产开 debug | 远程执行 | 强制关闭 |
| 错误信息暴露堆栈 | 信息泄露 | 通用错误页 |
# 1. 生产配置 class ProductionConfig(Config): DEBUG = False SECRET_KEY = os.environ['SECRET_KEY'] SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = 'Lax' # 2. 全局 CSRF csrf.init_app(app) # 3. 安全头 Talisman(app, force_https=True, content_security_policy=csp) # 4. 统一错误页(不露堆栈) @app.errorhandler(500) def internal_error(e): app.logger.error(f'500: {e}', exc_info=True) # 日志记录,页面不显示 return render_template('500.html'), 500 # 5. 模板输出默认转义(不用 |safe)
按这份清单逐项对照你的应用——每一条都是一道防线,合起来就是攻击者绕不过去的门槛。
|safe 慎用,富文本用 bleach。安全不是"加几个防护"就完事,而是一种工程常态。
第一,纵深防御(Defense in Depth)。 单层防护被攻破不等于失守,多层防线让攻击成本指数上升。以"用户密码"为例:HTTPS 保护传输(层1)→ 表单校验限长度防注入(层2)→ 密码哈希保护存储(层3)→ 限流防爆破(层4)→ 异常登录告警(层5)。每一层都不完美,但叠加起来足够让攻击者放弃。
第二,默认安全 vs 默认不安全。 Flask 的默认值整体是安全的(模板自动转义、session 签名、debug 默认关),但部署配置默认不安全(没有 HTTPS、没有限流、密钥可能是默认值)。"默认不安全"的每一项都需要显式配置——把生产部署清单(2.7 的检查表 + 本章要点)过一遍,是最低成本的加固。
第三,攻击面管理。 你的应用暴露了多少"入口"?公开路由、管理后台、API、文件上传、用户输入、第三方回调、错误页……每个入口都是攻击面。定期审视:哪些路由真的需要公开?后台要不要限制 IP?上传目录能不能执行脚本?减少暴露面是性价比最高的安全措施——不存在的入口不会被攻破。
第四,安全测试要"攻击者思维"。 上线前做一次"自我攻击":提交 <script> 看是否转义、POST 无 token 看是否被拒、输入 ' OR 1=1 看是否注入、越权访问 /admin 看是否被拦、上传 .py 文件看是否被拒。用攻击者的脚本(自动化扫描器)扫描自己的应用,很多免费工具(如 OWASP ZAP)能自动发现常见漏洞。
第五,日志与告警是安全的一部分。 攻击者总会留下痕迹:大量 401/403、异常参数、非工作时间访问、上传异常文件。安全日志 + 告警规则(同 IP 短时间内大量 401 触发告警)让攻击在早期被发现。没有日志的安全措施,被攻破都不知道从哪进来的。
第六,安全是持续过程。 依赖会出新漏洞(依赖扫描)、框架会出新版本(及时升级)、攻击手法会进化(关注安全资讯)。建议每月:跑一次依赖漏洞扫描、检查安全配置、review 登录/上传/权限相关代码。安全不是一次上线就完事,而是"持续加固"的常态工作——把安全检查排进发布流程(CI 里跑扫描),是最省力的持续方式。