Flask应用如何防止模板注入

wen 网络安全 2

本文目录导读:

Flask应用如何防止模板注入

  1. 核心原则:不要直接渲染用户输入的字符串
  2. 使用 render_template(文件模板)而非 render_template_string
  3. 禁用或限制模板访问的危险功能
  4. 对用户输入进行严格的过滤和验证
  5. 使用内容安全策略(CSP,Content Security Policy)
  6. 保持依赖包更新
  7. 针对需要动态模板的替代方案(如果实在无法避免 render_template_string
  8. 最佳实践清单

Flask 应用防止模板注入(SSTI,Server-Side Template Injection)的核心原则是:永远不要信任用户输入,并严格限制模板引擎对用户数据的处理方式

以下是几种具体、可操作的防御方法,从最基础到最深入:


核心原则:不要直接渲染用户输入的字符串

这是最根本、也最容易犯的错误。

错误做法(高危):

from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    # 危险!直接将用户输入拼接成模板字符串
    template = f"<h1>Hello, {name}!</h1>"
    return render_template_string(template)

如果用户传入 {{ config }}{{ ''.__class__.__mro__[2].__subclasses__() }},就会触发 SSTI。

正确做法(安全):

from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    # 安全:使用.render() 传入变量,而不是直接拼接模板
    return render_template("hello.html", name=name)

hello.html 文件中:

<h1>Hello, {{ name }}!</h1>

{{ name }} 中的内容会被 Jinja2 自动进行 HTML 转义(< 变成 &lt;),即使 name 包含恶意代码,也只会被当作纯文本显示。


使用 render_template(文件模板)而非 render_template_string

  • render_template:从文件加载模板,模板本身是静态的、由开发者控制的,用户数据只作为变量传入,不会通过模板引擎解析其内容。这是推荐的默认方式
  • render_template_string:从字符串加载模板,如果这个字符串包含任何用户输入,攻击者就可以通过模板语法注入代码。仅在你完全控制模板字符串内容(无用户输入)时才能使用

禁用或限制模板访问的危险功能

即使你使用了 render_template_string(某些场景下必须动态生成简单模板),也可以主动限制 Jinja2 的沙箱能力,不过请注意:Jinja2 的沙箱不是绝对安全的,只是增加攻击难度,不能作为唯一依赖

禁用 configself 等全局变量

from jinja2 import Environment, FileSystemLoader
# 创建一个不包含任何默认全局变量的环境
env = Environment(loader=FileSystemLoader('templates'), autoescape=True)
env.globals.clear()  # 移除所有默认全局对象(如 config, request, session)
env.globals['list'] = list  # 如果你需要,可以显式添加安全的函数
# 然后使用 env.from_string().render() 来渲染模板

使用 @jinja2.contextfilter 或自定义过滤器,并禁用 方法 这比较复杂且容易遗漏,不推荐作为主要防御手段,了解即可,核心思路是阻止访问 __class____mro____subclasses__ 等属性。


对用户输入进行严格的过滤和验证

即使不能完全信赖,输入验证仍然是一道重要防线,对于可能出现在模板中的用户输入:

  • 类型验证:如果期望是数字,确保它是数字;如果是字符串,严格控制长度。
  • 字符白名单:只允许某些安全字符(如字母、数字、空格),拒绝 、、、 等特殊字符。
  • HTML 实体编码:在将用户输入放入 HTML 上下文之前,将其中的 <>&、、 等转换为实体,Flask 的 render_template 默认已经做了这一点。

安全策略(CSP,Content Security Policy)

虽然 CSP 主要用于防止 XSS 攻击,但它也能缓解 SSTI 带来的部分影响(阻止攻击者注入的 <script> 标签执行)。

在 Flask 响应头中添加 Content-Security-Policy

@app.after_request
def add_csp(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline';"
    return response

保持依赖包更新

Flask 和 Jinja2 的开发者会在新版本中修复已知的 SSTI 绕过方法,保持 flaskjinja2werkzeug 等包为最新版本,可以避免因已知漏洞被攻击。


针对需要动态模板的替代方案(如果实在无法避免 render_template_string

极少数情况下,你需要让用户自定义少量的模板内容(一个可以自定义通知格式的功能),此时可以:

  • 使用白名单机制:只允许用户使用少数几个预定义的变量占位符({user_name}{datetime}),并通过正则或字符串替换来替换它们,而不是将整个字符串交给模板引擎解析。
    import re
    user_input = "Hello, {user_name}! Today is {datetime}."
    safe_message = user_input.replace("{user_name}", user_name).replace("{datetime}", datetime)
    # 注意:这里使用字符串替换,而不是模板渲染
  • 使用受限的沙盒环境:创建一个只包含基本类型和少量安全函数的 Jinja2 环境,并严格限制访问。

最佳实践清单

  1. 首选 render_template + HTML 文件模板,永远不要将用户输入拼接进模板字符串。
  2. 绝对避免render_template_string 中直接使用用户输入。
  3. 如果必须使用 render_template_string,请额外注意:
    • 清空 env.globals
    • 对用户输入进行严格的字符白名单过滤(只允许 [a-zA-Z0-9 ])。
    • 考虑使用简单的变量替换替代模板引擎。
  4. 保持 Flask 和 Jinja2 库为最新版本。
  5. 设置 Content-Security-Policy 头作为深度防御。
  6. 定期进行安全审计,使用工具扫描代码中可能存在的 SSTI 风险。

遵守以上原则,你的 Flask 应用就能有效抵御大部分模板注入攻击。

抱歉,评论功能暂时关闭!