0.0
The project is in a healthy, maintained state
htmlsafeslip parses Ruby source with Ripper to find a call to `.html_safe` directly on an expression, or a `raw(...)` wrapping one, where that expression is traceable (directly, or via a same-method local-variable assignment) to `params[...]`/`params.dig(...)`, with no `sanitize(...)`/`strip_tags(...)`/`h(...)`/`ERB::Util.html_escape(...)` call applied to it first. Both `.html_safe` and `raw(...)` tell Rails' view layer "this string is already safe HTML, don't auto-escape it" -- calling either directly on unsanitized, user-controlled input defeats Rails' single biggest built-in XSS defense, turning a reflected or stored script-tag payload into live, executing HTML the instant it's rendered.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 3.13
 Project Readme

htmlsafeslip

Gem Version License: MIT

Flags a .html_safe call, or a raw(...) call, applied to a value traceable — directly, or via a same-method local variable — to params[...]/params.dig(...), with no sanitization call applied to it first.

Both .html_safe and raw(...) tell Rails' view layer "this string is already safe HTML, don't auto-escape it." Rails' default auto-escaping (on by default since Rails 3) is the single biggest built-in XSS defense the framework has — calling either directly on unsanitized, user-controlled input defeats it entirely, turning a <script>...</script> payload typed into a comment, bio, or URL param into live, executing HTML the instant it's rendered. See DETAILS.md for the exact failure mode.

Honest prior-art note: Brakeman — the standard, widely used Rails security scanner — already has a mature, built-in check (CheckCrossSiteScripting) for exactly this pattern class. This is a real, meaningful overlap, not a different, uncovered gap. See DETAILS.md for the full, honest comparison — in short, Brakeman requires a full, bootable Rails application to run against; htmlsafeslip is a standalone, zero-Rails/Brakeman-dependency Ripper check that runs against any .rb file or directory, with no app-boot step.

Quick start

gem install htmlsafeslip
htmlsafeslip check app/controllers app/views app/helpers
htmlsafeslip: 1 finding(s)
[HS001] app/controllers/comments_controller.rb:4 params[:comment].html_safe -- CommentsController#create: params[:comment].html_safe marks a value traceable to `params[:comment]' in #create as already-safe HTML, with no sanitize(...)/strip_tags(...)/h(...)/ERB::Util.html_escape(...) call applied to it first -- this defeats Rails' default auto-escaping entirely, so a <script>...</script> (or any other HTML) payload in this value executes live, the instant it's rendered. Run it through sanitize(...) or strip_tags(...) before marking it safe, or drop .html_safe and let Rails auto-escape it. See DETAILS.md.

Exits 1 if any finding, 0 otherwise — wire it into CI as a pre-merge gate on app/controllers/, app/views/, and app/helpers/.

As a library

require "htmlsafeslip"

findings = Htmlsafeslip.check(paths: Dir.glob("app/**/*.rb"))
findings.each { |f| puts "[#{f.code}] #{f.kind} on #{f.receiver} (#{f.file}:#{f.line})" }

What it checks (v0.1)

A .html_safe call, or a raw(...) call, on a value that is:

  1. a direct params[...]/params.dig(...) read, or
  2. a bare local variable assigned from one of those earlier in the same method (traced anywhere later in the same method body),

...with no sanitization call (sanitize(...), ActionController::Base.helpers.sanitize(...), strip_tags(...), ERB::Util.html_escape(...)/h(...)) applied to that value first, anywhere in the chain — including nested inside raw(...)'s own argument (raw(sanitize(params[:x])) is correctly recognized as safe).

params[:bio].html_safe                 # flagged
raw(params[:comment])                  # flagged
bio = params[:bio]; bio.html_safe      # flagged (same-method trace)
params.dig(:user, :bio).html_safe      # flagged

sanitize(params[:bio]).html_safe       # not flagged -- sanitized first
strip_tags(params[:bio]).html_safe     # not flagged -- sanitized first
raw(sanitize(params[:x]))              # not flagged -- nested sanitize
"<b>Static trusted HTML</b>".html_safe # not flagged -- hardcoded literal
raw("<hr>")                            # not flagged -- hardcoded literal
h(params[:name])                       # not flagged -- out of scope, the safe pattern
@user.bio.html_safe                    # not flagged -- DB attribute, see limitation

Not flagged / explicitly out of scope (see DETAILS.md for the full, honest list):

  • A database/model-attribute value with no traceable connection to params in the same method — a real, documented scope limitation, not a guarantee the value is actually safe (it may have originated from an unsanitized params value written in a different method/request entirely).
  • A hardcoded string literal — the common, legitimate Rails idiom for marking genuinely-static trusted HTML as safe.
  • h(...)/ERB::Util.html_escape(...) calls themselves — these are the safe escaping pattern, so they're never matched as findings at all (only recognized as a sanitizer when they wrap a traced value before .html_safe/raw(...)).
  • Same-method scope only — a variable assigned from params in one method isn't traced into a different method, even under the identical variable name.

Class/method boundaries are found by parsing with Ruby's own Ripper, not regex over raw source — see DETAILS.md for exactly how, and its honest limits.

Requirements

Developed and tested against Ruby 3.0.2, matching this workspace's other Ruby packages. No known incompatibility with newer 3.x versions. No Rails or ActionView installation is required to run htmlsafeslip — it's a pure static-source check.

License

MIT