flawopen.com/xss/Python
Cross-Site Scripting (XSS) em Python (Flask, Jinja2, Django)
Envolver a entrada do usuário em Markup, mark_safe ou numa f-string passada a render_template_string faz o texto de um visitante rodar como JavaScript no navegador de outros usuários, com a sessão deles (CWE-79). Onde Flask, Jinja2 e Django escapam por você, os quatro pontos em que não escapam e como permitir texto rico com segurança.
Imagine uma gráfica com um formulário protegido: o texto escrito no campo em branco é impresso como texto comum. Mas, se um funcionário colar o recado de um cliente na folha de instruções da impressora antes de iniciar o trabalho, o recado pode virar um comando de impressão. Uma f-string Python enviada a render_template_string faz isso: insere a entrada no código-fonte Jinja2 antes que o autoescape possa tratá-la como variável. Markup e mark_safe são como um carimbo de “HTML já conferido”; eles não conferem nada. Mantenha a entrada em variáveis do template e deixe o autoescape agir, ou use format_html para um pequeno fragmento Django.
Conceitos Centrais e Termos
Jinja2 autoescape- O Flask ativa o autoescape do Jinja2 para templates
.html,.htm,.xmle.xhtmle pararender_template_string, então{{ q }}vira<img ...>. Umjinja2.Environment()avulso vem com autoescape desligado, a menos que você passeautoescape=select_autoescape(). Markup / |safe / mark_safe- Os três marcam uma string como HTML já seguro, e o template deixa de escapá-la. São promessas do desenvolvedor, não sanitizadores:
Markup(user_input)oumark_safe(user_input)publica o que o usuário digitou. render_template_string(f"...")- Uma f-string coloca o valor no código do template antes de o Jinja2 analisá-lo, então o autoescape nunca o vê como variável. A mesma linha também é injeção de template no servidor: um valor como
{{ 7*7 }}é avaliado pelo Jinja2. django.utils.html.format_html- Monta HTML como
str.format, mas antes escapa cada argumento comconditional_escapee só então marca o resultado como seguro. É a forma aprovada pelo Django de montar um pequeno fragmento HTML em Python, no lugar demark_safe(f"..."). nh3.clean()- Texto rico precisa de sanitizador, não de escape.
nh3.clean()(o ammonia do Rust) mantém uma allowlist de tags e atributos e remove<script>, manipuladores de eventos e URLsjavascript:. O Bleach está descontinuado; o nh3 é seu sucessor mantido.
Mapeamentos de frameworks
- Referência CWE para a fraqueza descrita neste guia: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Fluxo de Ataque Passo a Paso
A entrada chega numa requisição
Uma caixa de busca, um formulário de perfil ou um parâmetro de link envia q, bio ou next ao servidor. A vítima pode receber um link preparado, ou o valor fica salvo e aparece para todos que veem o perfil.
O valor escapa do escape
Uma f-string o coloca no código do template, Markup() ou mark_safe() o marcam como confiável, ou ele cai num href, onde escapar < e > não basta. O valor de teste inofensivo <img src=x onerror=alert(1)> passa intacto.
O navegador o interpreta como marcação
A resposta contém <img src=x onerror=alert(1)> como um elemento real. Se o recurso da imagem não carregar, onerror será disparado, e uma URL javascript: no link roda quando clicada.
O script roda como se fosse o site
Nos caminhos Flask/Jinja e Django mostrados, a marcação emitida é executada na origem da aplicação. Mantenha valores não confiáveis em posições de dados com escape automático e permita HTML apenas após sanitização adequada ao contexto.
Código-Fonte: Vulnerável vs. Seguro
# views.py: cada handler abaixo deixa texto da requisição chegar à página como HTML
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup
app = Flask(__name__)
# a f-string monta o código do template, então o Jinja2 nunca escapa q
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string(f"<h1>Results for {q}</h1>")
# Markup() diz ao Jinja2 que a bio é HTML confiável
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))
# mark_safe sobre uma f-string publica name como HTML
def greeting(name):
return mark_safe(f"<p>Hello, {name}</p>")
# o autoescape não impede uma URL javascript: no href
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
# views.py: valores continuam variáveis de template; texto rico é sanitizado
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request
app = Flask(__name__)
# q é variável de template, então o autoescape a codifica
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string("<h1>Results for {{ q }}</h1>", q=q)
# o nh3 aplica uma allowlist de tags antes de o HTML ser confiado
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio|safe }}</div>", bio=nh3.clean(bio))
# format_html escapa cada argumento antes de marcar o resultado como seguro
def greeting(name):
return format_html("<p>Hello, {}</p>", name)
# só caminhos do próprio site são aceitos como destino do link
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
if not url.startswith("/") or url.startswith("//"):
url = "/"
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
Lista de Verificação de Segurança para Engenharia
- ✓Faça a CI falhar com os Bandit B703/B308 (
mark_safe) e B704 (Markupsobre não literais), e com qualquerrender_template_stringcujo primeiro argumento seja f-string,%ou resultado de.format(). - ✓Passe valores aos templates como variáveis e deixe o autoescape cuidar deles; no Jinja2 avulso, crie
Environment(autoescape=select_autoescape()). - ✓Use
format_html()em vez demark_safe()para fragmentos pequenos, enh3.clean()para todo HTML enviado por usuários. - ✓Valide toda URL controlada pelo usuário antes que chegue a
hrefousrc: aceite só caminhos do próprio site começando com uma única/, ou uma allowlist explícita de http/https. - ✓Envie uma Content-Security-Policy com
script-srcbaseado em nonce e sem'unsafe-inline', e marque os cookies de sessão comoHttpOnlyeSameSite=Lax.
Fontes
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Flask — Security Considerations: Cross-Site Scripting
- Django — Security: Cross site scripting (XSS) protection
- Django — format_html
- nh3 — Python bindings to the ammonia HTML sanitizer
- OWASP Cross Site Scripting Prevention Cheat Sheet
- nh3 — Python bindings to the ammonia HTML sanitizer