flawopen.com/Cross-Site Scripting/Rust

Cross-Site Scripting in Rust

High CWE-79 Draft — pending review
ELI5

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.

Key terms on this page
output encoding
Converting characters that have special meaning in HTML (like < and >) into harmless equivalents before inserting untrusted text into a page, so the browser displays it as text instead of running it as markup or script.
DOM
The in-memory tree structure a browser builds from a page's HTML — where an element's innerHTML is set determines whether inserted content is rendered as inert text or executable markup.

What's happening

Cross-site scripting happens when untrusted input reaches the rendered page without proper output encoding. Rust's standard library has no HTML awareness at all, so this almost always means format! was used to build an HTML response directly instead of going through an auto-escaping template crate.

Real-world impact

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.

Vulnerable vs. fixed

VULNERABLE
// format! has no HTML awareness
let body = format!(
  "<div>Welcome, {}</div>", name
);
FIXED
// Askama auto-escapes .html templates
#[derive(Template)]
#[template(path = "welcome.html")]
struct Welcome<'a> { name: &'a str }
// welcome.html: <div>Welcome, {{ name }}</div>

Why the fix works

Askama (and similar crates like Tera) auto-escape template expressions based on the template file's extension — a .html template escapes HTML-special characters in {{ name }} automatically. format! has no concept of output context; it substitutes the value as literal text into the format string, whatever that text contains.

Rust-specific gotchas

The standard library provides zero HTML escaping

Unlike Go's html/template, Rust's std has no HTML-aware output mechanism at all — the safety has to come entirely from an external crate (Askama, Tera, Maud) chosen and used correctly.

Askama's escaping is extension-based, which is easy to misconfigure

A template file saved with a non-.html extension, or explicitly marked with a different escape mode, won't get HTML escaping even inside an Askama project — worth confirming the escape mode explicitly rather than assuming it from context.

Building JSON to embed in a <script> tag needs its own care

serde_json::to_string() produces valid JSON, but valid JSON isn't automatically safe to embed directly inside an HTML <script> block — certain characters still need HTML-context escaping on top of JSON serialization.

Common misconceptions

"Rust's memory safety extends to this kind of bug"

Memory safety and output-encoding safety are unrelated properties — a perfectly memory-safe format! call can still produce exploitable HTML output.

"I'm using a template crate, so I'm automatically safe"

True only if the specific template/expression is using the crate's auto-escaping path — most template crates also offer an explicit unescaped-output mode for legitimate raw-HTML cases, which carries the same risk as any other language's raw-output escape hatch.

How to check if you're affected

grep -rn "format!(" --include="*.rs" . | grep -i "<div\|<p\|<span\|<html"
No mainstream Rust linter currently ships a dedicated HTML-context escaping check — a CI grep for HTML-shaped format! calls is the practical check until tooling catches up.

Prevention checklist

FAQ

Is this the same bug as SQL injection?

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.

Is Maud safer than Askama here?

Maud (a compile-time HTML macro crate) also escapes by default for text content — the underlying principle is the same across these crates: check what each one's default behavior actually does for the values you interpolate, and where its explicit unescaped-output option is.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)