flawopen.com/Cross-Site Scripting/Python
Imagine a guestbook where visitors write a public note. Stored XSS is like someone writing a note that isn't just text — it's a hidden trick that makes the guestbook page itself start doing things, like stealing the next visitor's login session, the moment anyone opens the page to read it.
Cross-site scripting happens when untrusted input is rendered into a page without proper output encoding, letting an attacker's own HTML or JavaScript run in another user's browser under your site's identity. In Python web apps, this usually means either bypassing a template engine's default auto-escaping, or building HTML manually with string formatting instead of using the template engine at all.
In 2005, an 19-year-old user exploited a stored XSS flaw on MySpace to create the "Samy" worm — a script that added itself to every profile that viewed it, infecting over a million profiles within about 20 hours and forcing MySpace offline to contain it. It remains one of the most-cited demonstrations of how quickly stored XSS can self-propagate.
Source: widely documented in security industry retrospectives — see References below.# username inserted with no escaping
@app.route("/welcome")
def welcome():
name = request.args.get("name")
return f"<div>Welcome, {name}</div>"
# Jinja2 template auto-escapes by default @app.route("/welcome") def welcome(): name = request.args.get("name") return render_template( "welcome.html", name=name ) # welcome.html: <div>Welcome, {{ name }}</div>
Flask's Jinja2 templates auto-escape every {{ }} expression by default — special HTML characters in name are converted to their entity equivalents before insertion, so the browser renders them as visible text rather than parsing them as tags or scripts. Building the response with an f-string bypasses the template engine entirely, so nothing escapes anything.
Jinja2 auto-escapes by default, but {{ comment|safe }} or wrapping a value in Markup() tells the engine to trust it as-is. Django's templates work the same way, via |safe or mark_safe() — both are explicit opt-outs, and both are exactly as dangerous as not escaping at all when applied to untrusted input.
Any response built outside of render_template() — an f-string, manual string concatenation, a raw Response() body — gets none of Jinja2's or Django's protection, regardless of how safely the rest of the app is templated.
Escaping meant for HTML context doesn't protect a value later reflected into JavaScript or a URL — Flask's jsonify() handles JSON-context safety correctly, but manually building a <script> block with an f-string reintroduces the same class of bug in a different context.
True for standard template expressions — false the moment |safe, mark_safe(), or a response built outside the template engine is involved.
Any reflected value counts — a search query echoed back on a results page, a URL parameter shown in an error message, a filename displayed after upload.
grep -rn "|safe\|mark_safe(\|Markup(" --include="*.html" --include="*.py" .
grep -rn "return f\"<\|return \"<.*+\|Response(f\"<" --include="*.py" .
Same underlying shape — untrusted data mixed into a command's structure without encoding — but the target and damage differ: SQL injection targets the database, XSS targets other users' browsers.
No — CSP is a valuable second layer of defense that limits what injected script can do, but output encoding is still the primary fix. Relying on CSP alone leaves gaps CSP doesn't cover.