ruby / escaping

Templates escape with = by default, and the Haml subset rejects raw HTML construction. A small number of code paths still produce HTML strings that the template emits with != (raw output). Each of those paths is a place to escape, not a place to trust input.

Where raw HTML comes from

Anything that flows into != must be escaped at the source.

Formatters

A formatter that builds HTML in Ruby escapes every dynamic piece, even when it looks safe today:

require "cgi"

module Fmt
  module Lists
    def self.list(name, url)
      esc_name = CGI.escapeHTML(name.to_s)
      esc_url  = CGI.escapeHTML(url.to_s)
      %(<a href="#{esc_url}">#{esc_name}</a>)
    end
  end
end

The same rule covers titles, domains, event names, and any other user-controlled strings interpolated into HTML. A formatter that returns HTML and forgets to escape one parameter is one of the most common XSS sources in a server-rendered app.

ts_headline

Postgres' ts_headline returns a snippet with <b>...</b> markers around matching terms. The terms are user input. If a search query contains <script>, the snippet will too unless escaped.

Escape the entire snippet, then reintroduce only the markers the function adds:

require "cgi"

def safe_headline(snippet)
  CGI.escapeHTML(snippet.to_s)
    .gsub("&lt;b&gt;",  "<b>")
    .gsub("&lt;/b&gt;", "</b>")
end

The handler returns this string and the template renders it with != row.headline.

Flash messages

A flash that interpolates a user-controlled name renders in the next response. Escape on the way in:

flash_next(:notice, "Merged into #{CGI.escapeHTML(target.name)}")

If every flash value is treated as HTML by the layout, the producer is the right place to escape. The reader cannot tell which strings are safe.

One auditable layer

The Haml subset concentrates raw HTML output behind a single operator (!=). Search the codebase for != and audit every producer:

rg -nU '^[[:space:]]*!=' ui/views/

Adding the rule "every dynamic value at a != boundary is escaped at its source" gives a small, finite set of places to review.

For data that lands in spreadsheets instead of HTML, see ruby / csv for the equivalent neutralization at that boundary.

← All articles