本文目录导读:

- 核心原则:不要直接渲染用户输入的字符串
- 使用
render_template(文件模板)而非render_template_string - 禁用或限制模板访问的危险功能
- 对用户输入进行严格的过滤和验证
- 使用内容安全策略(CSP,Content Security Policy)
- 保持依赖包更新
- 针对需要动态模板的替代方案(如果实在无法避免
render_template_string) - 最佳实践清单
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 转义(< 变成 <),即使 name 包含恶意代码,也只会被当作纯文本显示。
使用 render_template(文件模板)而非 render_template_string
render_template:从文件加载模板,模板本身是静态的、由开发者控制的,用户数据只作为变量传入,不会通过模板引擎解析其内容。这是推荐的默认方式。render_template_string:从字符串加载模板,如果这个字符串包含任何用户输入,攻击者就可以通过模板语法注入代码。仅在你完全控制模板字符串内容(无用户输入)时才能使用。
禁用或限制模板访问的危险功能
即使你使用了 render_template_string(某些场景下必须动态生成简单模板),也可以主动限制 Jinja2 的沙箱能力,不过请注意:Jinja2 的沙箱不是绝对安全的,只是增加攻击难度,不能作为唯一依赖。
禁用 config、self 等全局变量
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 绕过方法,保持 flask、jinja2、werkzeug 等包为最新版本,可以避免因已知漏洞被攻击。
针对需要动态模板的替代方案(如果实在无法避免 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 环境,并严格限制访问。
最佳实践清单
- 首选
render_template+ HTML 文件模板,永远不要将用户输入拼接进模板字符串。 - 绝对避免在
render_template_string中直接使用用户输入。 - 如果必须使用
render_template_string,请额外注意:- 清空
env.globals。 - 对用户输入进行严格的字符白名单过滤(只允许
[a-zA-Z0-9 ])。 - 考虑使用简单的变量替换替代模板引擎。
- 清空
- 保持 Flask 和 Jinja2 库为最新版本。
- 设置 Content-Security-Policy 头作为深度防御。
- 定期进行安全审计,使用工具扫描代码中可能存在的 SSTI 风险。
遵守以上原则,你的 Flask 应用就能有效抵御大部分模板注入攻击。