Rails escapes output in ERB templates by default, so XSS in Ruby apps usually comes from places that opt out: marking strings as safe, building HTML by hand, trusting user URLs, or running without a content security policy. Keep default escaping, whitelist what you allow, and add a nonce-based policy.
What does the Rails guide say about XSS?
The Rails security guide calls XSS one of the most widespread and devastating web vulnerabilities, and says Rails provides helper methods to fend off these attacks. It lists entry points broadly: message posts, comments, project titles, document names, search results, and any URL parameter, obvious or hidden.
It also describes what attackers do with it: steal cookies, hijack sessions, redirect victims to fake sites and change page content to collect information. That is why a single stored title field can matter.
Where does XSS still show up in Rails?
Think about the places a developer overrides the default.
Marked-safe strings. Calling html_safe or raw on a value that contains user data tells Rails to skip escaping. A constructed example: a team adds a "status message" feature. The message is formatted with a helper that returns html_safe so a bold tag works. A user saves a message with a script tag, and every teammate who opens the dashboard runs it. The fix is to build the markup from escaped pieces, using content_tag or a template, and never mark user data safe.
Allowlists versus blocklists. The guide says to allow only what you intend, with permitted tags such as strong, rather than trying to remove script. It shows why a blocklist fails: removing script once turns sc plus script plus ript back into a working tag. The sanitize helper works from an allowlist of tags and attributes, and you can tighten it.
Links and attributes. Escaping text does not make a javascript: URL safe. Check user-supplied URLs against allowed schemes before using them in href.
How does a content security policy help?
The guide documents a policy DSL in an initializer. It recommends nonces over unsafe-inline for existing code and shows a nonce generator using SecureRandom.base64(16). There is a csp_meta_tag helper so inline scripts can receive the per-session nonce. The guide also notes caching tradeoffs: a nonce that is per request may cause cache fragmentation or stale content if your caching strategy does not account for it.
A policy limits damage when escaping fails. It does not replace fixing the bug.
How do you review a Rails app for this?
Treat three patterns as review triggers: html_safe, raw and sanitize with loose options. Grep for each, trace the data source, and ask whether a user could have set it, directly or through an import, an API or an admin tool. Also check JavaScript responses and JSON, which can carry user values into script contexts. OWASP says to use an encoder documented for the specific context, not manual escaping.
What to do
- Run a search for
html_safe,raw(,.html_safeandsanitizeacross views, helpers, mailers and presenters. - For each hit, write down where the string comes from. If any user can influence it, rebuild with
content_tagor a template. - Replace blocklist cleanup with
sanitizeand an explicit tag and attribute allowlist. - Validate link schemes before rendering user-supplied URLs.
- Enable a content security policy with a nonce generator. Start in report-only mode.
- Check that sanitizer and front-end gems are current. Safeguard's SBOM and dependency scanning, plus the free scan at gold.safeguard.sh/scan, can show vulnerable packages in a lockfile.
- Add a request spec that stores a markup payload in each user-editable field and asserts it renders as text.
Quick check
Is ERB output escaped automatically?
Yes, by default in Rails views. The risk is in places that mark a string as safe or that build HTML outside normal escaping.
Why not just strip script tags?
The Rails guide shows a crafted nested tag that becomes a working script tag after one pass of removal. Use an allowlist instead.
Sources
Last reviewed: 18 October 2026.