Taint Analysis vs. Heuristic Pattern Matching

The real difference between the two engineering approaches Rails security tools use, with concrete Ruby examples of where each gets it right and where each gets it wrong.

Honesty check first: Scryer does not do taint/data-flow analysis. It does heuristic pattern matching. This page explains what that distinction means in practice — it's a comparison, not a claim that Scryer is something it isn't. If you need the highest precision Rails security analysis available today, that's Brakeman, not Scryer — see where Scryer fits instead.

What taint analysis actually does

Taint/data-flow analysis tracks a value's origin and asks whether it can reach a dangerous operation through the actual paths the program executes. Concretely, for Rails: it marks params (and anything derived from cookies, headers, or the database) as "tainted," then traces that taint through assignments, method calls, and even across files — does this tainted value eventually arrive, unsanitized, at a SQL query, a shell command, an eval, a redirect? If a sanitizing step (an escape, a whitelist check, a type coercion) sits on that path, the taint is cleared and no finding fires.

This is what makes it precise: a data-flow tool can tell the difference between these two, even though they look almost identical at the call site:

# NOT flagged by a taint analyzer — the value is sanitized before it arrives
status = params[:status].to_s
raise ArgumentError unless %w[open closed].include?(status)
Order.where("status = ?", status)  # already parameterized AND validated

# Flagged — tainted value reaches the sink with no sanitization on this path
Order.where("status = '#{params[:status]}'")

Building this requires understanding Rails' actual call graph (which methods call which, across files, including framework internals like before_action filters) — real engineering investment, which is exactly why Brakeman, built specifically for this, is more mature at it than any general-purpose heuristic scanner will be.

What heuristic pattern matching does instead

A heuristic scanner skips the call-graph tracing entirely and asks a narrower, syntactic question: does a dangerous-shaped call and a reference to untrusted input appear together in the same expression? Scryer's sql_injection rule, concretely: walk the parsed syntax tree for a call to where/find_by/order/etc. whose first argument is a string literal containing #{...} interpolation. No tracing of where the interpolated value came from — just "this shape, right here, is risky."

The tradeoff is exactly what you'd expect: faster (no call-graph construction, no framework boot), simpler to build and audit, but it can't tell sanitized-and-safe apart from tainted-and-dangerous when they don't differ syntactically:

# Flagged by both approaches — genuinely risky
User.where("name = '#{params[:name]}'")

# Flagged by heuristic pattern matching, but a taint analyzer would clear it —
# the value never actually reaches user input
ALLOWED_STATUSES = %w[open closed]
Order.where("status = '#{ALLOWED_STATUSES.sample}'")

That second example is exactly why every Scryer finding says "review this," never "this is definitely a bug" — and why idor, the check with the least reliable signal in the gem (an ID from params reaching Model.find, without being able to trace whether a class-level before_action elsewhere already gates it), carries that caveat most heavily. See the full comparison footnotes for the specific precision gap on each harder check.

Insecure Rails patterns a heuristic scanner is well-suited to catch

The precision gap matters most on checks that need to reason about a value's actual origin. It matters much less — heuristic and data-flow approaches converge — on checks that are really about a syntactic shape or a configuration state, independent of any specific tainted value:

This is also exactly the set of rules Scryer's fix mode can resolve automatically without an LLM — see Scryer::MechanicalFixer in the README. Rules that genuinely need data-flow reasoning (mass_assignment's correct .permit list, idor's correct authorization check) don't get a mechanical fixer, because there isn't one correct answer a syntax-level tool can derive.

Which one should you use?

Not an either/or in practice — they answer different questions and cost different amounts to run:

Taint/data-flow (Brakeman)Heuristic (Scryer)
Precision on hard checks (IDOR, auth)HigherLower — "review this," not "confirmed"
Needs Rails/framework bootYesNo
Also covers performance/dependenciesNoYes, same pass
Cross-category severity rankingNoYes
Maturity on Rails-specific securityYears, purpose-builtNewer, broader scope

Running both costs nothing but CI minutes — different scanning strategies, no shared state, no conflict. Brakeman for the deepest security-only analysis; Scryer for the cross-category "what's actually most worth fixing this week" view across security, performance, dependencies, and code quality.

Further reading