Preventing XSS in Rails views
Fast recall
Quick revision
Core answer
Whether the learner understands contextual output handling rather than treating Rails as automatically XSS-proof.
Using raw or html_safe on user input, or assuming a JSON API removes XSS risk from the frontend.
Separate stored/reflected sources, HTML escaping, sanitization, safe DOM APIs, and CSP defense in depth.
Interview-ready guide
Interview answer
Cross-site scripting happens when attacker-controlled data reaches a browser context as active markup or code under the application's trusted origin. The value may be stored in a comment, reflected from a request, or inserted later by JavaScript. The important question is not only where it entered, but which browser sink finally interprets it.
For plain text, normal ERB output such as <%= @comment.body %> is the right default. Rails HTML-escapes unsafe characters, so a submitted tag is displayed as text instead of becoming an element. The small runnable example shows that encoding step directly; a Rails template performs it automatically on an ordinary unsafe string.
If the product intentionally accepts limited formatting, use sanitize(rich_html, tags: ALLOWED_TAGS) with a small, explicit allowlist. Sanitizing chooses which markup may survive; escaping treats all markup as text. Calling raw, .html_safe, or <%== on untrusted content skips the normal protection instead of validating it.
HTML text, URLs, script data, CSS, and DOM APIs follow different grammars. Validate URL schemes, serialize data instead of building JavaScript source, and prefer textContent to innerHTML for client-side text. Content Security Policy adds defense in depth, but it does not make an unsafe sink safe. Protection belongs at the final rendering context.
Preventing XSS in Rails views
Direct Answer
Cross-site scripting occurs when attacker-controlled data is interpreted as active browser markup or script in another user's origin. Rails escapes dynamic ERB output by default and provides allowlist sanitization for intentional rich HTML, but raw, html_safe, unsafe DOM sinks, URL contexts, and third-party scripts still require deliberate controls.
Deep Dive & Architecture
HTML escaping treats every character sequence as text. When ERB renders <%= @comment.body %>, Rails produces an HTML-safe output buffer and escapes unsafe string content for that context. A value containing <img onerror=...> appears literally rather than creating an image element. This is the right contract for names, titles, search terms, messages, and any field that is supposed to be plain text.
Sanitization is for a different product requirement: accepting some HTML while removing disallowed elements and attributes. Rails' sanitize helper applies an allowlist-based sanitizer. The application should define which formatting it supports, keep the sanitizer dependency updated, and test dangerous URL schemes, malformed markup, event-handler attributes, SVG or MathML cases relevant to the chosen allowlist, and nested encodings.
Marking a string with html_safe, passing it to raw, or using <%== skips ordinary escaping. Those APIs do not inspect whether a string is trustworthy. Concatenating one safe fragment with untrusted data can also produce a dangerous result. Prefer Rails tag and link helpers that escape values, and never make a complete user-controlled string safe merely to preserve formatting.
Context matters beyond HTML text. A URL needs scheme and destination validation, JavaScript data should be serialized rather than interpolated into executable source, and CSS has its own grammar. On the client, textContent creates text while innerHTML parses markup. Framework escaping works when values remain in normal text bindings; escape hatches and direct DOM manipulation require review.
Content Security Policy can restrict script sources and reduce the impact of some injection, but it is defense in depth rather than permission to use unsafe sinks. HttpOnly cookies can hide cookie values from normal script reads, yet an injected script may still send same-origin requests or alter the page. Tests should render realistic payloads, inspect the resulting DOM, and confirm the payload stays text or is reduced to the explicitly permitted markup.
Review third-party widgets and Markdown renderers as output paths too. Their configuration may enable raw HTML, permissive links, or script-capable embeds even when the surrounding ERB is escaped. Pin, update, and test the component that performs the final transformation.
Code Example
<%# Plain text: Rails escapes the value. %>
<p><%= @comment.body %></p>
<%# Rich text: allow only the product's supported markup. %>
<div class="formatted-comment">
<%= sanitize(@comment.formatted_body, tags: %w[p strong em a], attributes: %w[href]) %>
</div>
Neither output path marks the original user value with raw or html_safe.
Common Mistakes & Pitfalls
Using raw or html_safe on user input, or assuming a JSON API removes XSS risk from the frontend.
Check your understanding
Answer these without looking back at the article.
- What is the difference between escaping and sanitizing HTML?
- Why can a JSON API still participate in an XSS vulnerability?
- What protection does Content Security Policy add?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.