Class: RuboCop::Cop::DevDoc::Style::AvoidSymbolizingBoundaryInput

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/style/avoid_symbolizing_boundary_input.rb

Overview

Flag .to_sym (including &.to_sym, and through laundering hops like .presence/.to_s/.downcase) chained directly onto an attacker-controlled boundary source: params[...] (also .dig, .fetch, .require) or request.headers[...].

Rationale

A bare .to_sym lets ANY client-supplied value cross into the symbol domain, where downstream case/== dispatch treats it as known vocabulary. The allow-list is the missing DECISION: which values do we recognise, and what happens to the rest? Skipping it also mints a symbol per novel value (the classic symbol-DoS vector; GC reclaims dynamic symbols since Ruby 2.2, so today this is churn rather than a hard leak — the vocabulary laundering is the real bug).

Resolve the value through the glib-web helper, which only ever returns a symbol already in the allow-list:

❌
@mode = params[:mode]&.to_sym || :active

✔️ (glib-web >= 5.1.2)
@mode = glib_allowlist_symbol_param(params[:mode], MODES, default: :active)

When recognised inputs must map to differently-spelled symbols (e.g. 'spring-promo':spring_promo), use an explicit case-when at the boundary instead — same allow-list property.

Sources detected

  • params[…], params.dig(…), params.fetch(…), params.require(…)
  • request.headers[…]

ENV[…] is deliberately NOT a source here (unlike the sibling DevDoc/Style/StringSymbolComparison): ENV is operator-controlled, not attacker-reachable, and .to_sym on boot-time config is legitimate where the controller helper doesn't even exist.

Relationship with DevDoc/Style/StringSymbolComparison

Complementary pair over the same boundary doctrine: that cop flags comparing an UNCONVERTED string source against a symbol (always false); this cop flags CONVERTING without validating. The sanctioned exit from both is the allow-list helper.

Limitations

Only the direct form is caught. Once the value flows through a helper method (report_params[:x]) or a local/instance variable, static analysis loses the boundary and review has to catch it. Laundering transforms (.presence, .to_s, .strip, .downcase, .upcase, .squish) do NOT reset the trail — they transform, they don't validate.

Examples:

# bad
@mode = params[:mode]&.to_sym || :active
@theme = request.headers['X-Theme']&.to_sym
@segment = params[:segment].presence&.to_sym
@sort = params[:sort].to_s.downcase.to_sym

# good
@mode = glib_allowlist_symbol_param(params[:mode], MODES, default: :active)

Constant Summary collapse

MSG =
'Unvalidated `.to_sym` on `%<source>s` lets any client value into the symbol ' \
'domain (and mints a symbol per novel value). Allow-list it instead: ' \
'`glib_allowlist_symbol_param(value, ALLOWED, default:)`. Laundering through ' \
'`.to_s`/`.presence`/`.downcase` is not validation.'.freeze
RESTRICT_ON_SEND =
%i[to_sym].freeze
TRANSFORM_HOPS =

String-preserving transforms that commonly sit between the boundary read and the .to_sym — they launder the value, not validate it, so the chain is still an offense.

%i[presence to_s strip downcase upcase squish].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object Also known as: on_csend



85
86
87
88
89
90
# File 'lib/rubocop/cop/dev_doc/style/avoid_symbolizing_boundary_input.rb', line 85

def on_send(node)
  source = boundary_source_in(node.receiver)
  return unless source

  add_offense(node, message: format(MSG, source: source.source))
end