Lesson 4 — Structural safety: how XSS is prevented by design
The XSS problem
Cross-site scripting (XSS) is one of the most common web vulnerabilities. It happens when user-supplied content is rendered as raw HTML, allowing an attacker to inject <script> tags or event handlers.
In ERB, the default changed in Rails 3: strings are now HTML-escaped unless you call html_safe or raw. But that escape hatch exists, and developers use it — sometimes correctly, sometimes not. The vulnerability surface is always present.
Phlex takes a different approach. It makes XSS structurally impossible — not through escaping as an afterthought, but through the design of the API itself.
How Phlex prevents XSS
In Phlex, text content is always escaped. There is no opt-out at the point of rendering text. If you pass user data as text content, it is escaped. Full stop.
|
|
Output:
|
|
The <script> tag is rendered as literal text, not as HTML. The browser displays it as text. The script never executes.
Attribute values are also escaped
|
|
Output:
|
|
Both the attribute value and the text content are escaped. Neither attack vector works.
What if you genuinely need raw HTML?
Sometimes you have HTML content that is safe — for example, rendered markdown from a trusted library. Phlex provides raw for this, but it requires you to explicitly mark the content as safe using safe:
|
|
Output:
|
|
The key constraint: raw only accepts content that has been wrapped with safe. If you try to pass a plain string to raw, Phlex raises an error. You can’t accidentally bypass the safety mechanism — you have to deliberately and explicitly mark content as safe.
Compare this to ERB’s <%= raw some_string %> which accepts any string with no ceremony. Phlex requires an intentional declaration.
The structural safety model
The Phlex safety model is:
- Text content: always escaped. No exceptions, no opt-out.
- Attribute values: always escaped. No exceptions, no opt-out.
- Raw HTML: only accepted if wrapped in
safe(), which is an explicit developer declaration of trust. - Attribute names: validated against a known-good list. You cannot inject arbitrary attribute names from user input.
This is “structural safety” — the safe path is the default path, and the unsafe path requires deliberate effort.
A practical demonstration: safe vs unsafe
|
|
Output:
|
|
The attack is defused at the rendering layer, automatically, without any developer action.
Exercise
Create 04_exercise.rb. Build a UserProfileComponent that displays a user’s name, bio, and website URL — all supplied as strings. Demonstrate that injecting <script> tags into any field produces harmless escaped output. Then add a second variant that accepts a bio_html: parameter — pre-rendered, trusted HTML from a markdown processor — and renders it using raw(safe(...)).
Solution to Exercise 04
|
|