Skip to content

Close the filtering gaps: keys by their words, HMAC hashes, card numbers, URL secrets, request IDs and SQL binds - #24

Merged
ndbroadbent merged 7 commits into
mainfrom
nathan/filtering-gaps
Oct 18, 2026
Merged

ndbroadbent merged 7 commits into
mainfrom
nathan/filtering-gaps

Conversation

@ndbroadbent

@ndbroadbent ndbroadbent commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Ninth of twelve stacked PRs for 0.2.0. These are the fixes from a security audit of the filtering.

Keys

  • Sensitive key names are matched by their words, whatever their case or separators: apiKey, X-Api-Key,
    HTTP_AUTHORIZATION, Set-Cookie and plurals like passwords are filtered by default, and so are cookie,
    session, jwt, bearer, otp and passphrase. Matching whole words leaves author, keyboard and
    signature_field_id alone. filter_keys entries match by their words too.
  • Rails' dotted filter_parameters ("card.number", /\Acard\.number\z/) match nested keys by their path, and
    three-argument filter blocks get the parameters around the key, as in Rails. New filters.path_matchers setting.
  • Breaking: hashed values (emails, filter_keys_with_hashes) are an HMAC-SHA256, 16 characters long, keyed with
    hash_salt or, by default, a key derived from the app's secret_key_base. The default was a published salt
    ("l0g5t0p"), so anyone with a list of addresses could match them to the hashes. A filtered String no longer logs its length.
  • The filter configuration example adds to the default lists instead of replacing them.

Strings

  • Card numbers of 13 to 19 digits are scrubbed: written together, in fours with one kind of separator, or in American
    Express and Diners Club groups. Numbers that fail the Luhn check are left alone.
  • URL passwords: the user part ends at its first colon (user:pass:word kept user:pass: before), an empty user is
    matched (redis://:pw@host), and so are lowercase %3a/%40 in encoded URLs.
  • Sensitive query params in URLs are filtered (?token=, ?apiKey=, user[password]=, OAuth's ?code=). The
    names are checked like keys. New filters.url_query_params setting, on by default.
  • Hash keys that aren't plain identifiers are scrubbed (an email address used as a key), and so are a filtered Hash's
    _keys, Symbol values and GlobalIDs.

What the integrations write

  • Request, blocked host, CSRF and IP spoofing lines, and the request context sent to error reporters, have the
    Referer without its query and fragment, which can carry password reset, confirmation and magic link tokens or OAuth
    codes.
  • ActiveStorage url lines leave out the signed URL: anyone who has it can read the file until it expires.
  • Error reporters (Sentry, Bugsnag, Rollbar, Honeybadger and custom handlers) get the context filtered and scrubbed
    like a log line, instead of raw parameters, referers and mail recipients.
  • The request ID, program name and Sidekiq job context are filtered. They were added after the line was filtered,
    by parsing the line's JSON and writing it again, so an X-Request-Id header carrying a card number was logged as
    sent. LogStruct::Formatter#call now takes the request ID and job context and filters them with the rest of the
    line, and the SemanticLogger formatter no longer parses and regenerates each line.
  • SQL bind values are filtered by their column names with sql_log_bind_params on, like request params. Before,
    only values that looked like tokens were filtered, so a password bind was logged. Binary values are logged as their
    size, and type_casted_binds given as a lambda are read instead of raising.
  • The colour formatter (opt-in, for development) and loggers added with broadcast_to get scrubbed strings too.

Docs

The filtering page, the SOC 2 page, the README and the site's sample log lines describe filtering as it now works.
They had described email keys, [PASSWORD] scrubbing, a _size field and a payment_data key that LogStruct
didn't have, and said Rails' dotted filters and three-argument blocks don't apply.

Testing

New tests for each change. Sorbet and the unit suite (332 tests) pass at this branch's tip.


Stack for 0.2.0, merged in order: #16 → #17 → #18 → #19 → #20 → #21 → #22 → #23 → #24 → #25 → #26 → #27

- Sensitive key names are matched by their words, whatever their case or
  separators: apiKey, X-Api-Key, HTTP_AUTHORIZATION, Set-Cookie and plurals
  like passwords are filtered by default, and so are cookie, session, jwt,
  bearer, otp and passphrase. Whole words keep author, keyboard and
  signature_field_id. filter_keys entries match by their words too.
- Rails' dotted filter_parameters ("card.number", /\Acard\.number\z/) match
  nested keys by their path, and three-argument filter blocks get the
  parameters around the key, as they do in Rails. New path_matchers setting.
- Hashed values (emails, filter_keys_with_hashes) are an HMAC-SHA256 keyed
  with hash_salt, or by default with a key derived from the app's
  secret_key_base instead of a salt shared by every install, and are 16
  characters long. A filtered String no longer logs its length.
- The filter configuration example adds to the default lists instead of
  replacing them.
- Card numbers of 13 to 19 digits are scrubbed, written together, in fours
  with one kind of separator, or in American Express and Diners Club groups.
  Numbers that fail the Luhn check are left alone.
- URL passwords: the user part ends at its first colon (user:pass:word kept
  "user:pass:" before), an empty user is matched (redis://:pw@host), and
  lowercase %3a/%40 in encoded URLs too.
- Sensitive query params in URLs are filtered (?token=, ?apiKey=,
  user[password]=, OAuth's ?code=): the names are checked like keys, with
  the filter keys, the default pattern and Rails' filter_parameters. New
  url_query_params setting, on by default.
- Hash keys that aren't plain identifiers are scrubbed (an email address as
  a key), and so are a filtered Hash's _keys, Symbol values and GlobalIDs.
- Request, blocked host, CSRF and IP spoofing lines, and the request context
  sent to error reporters, have the Referer without its query and fragment,
  which can carry password reset, confirmation and magic link tokens or
  OAuth codes.
- ActiveStorage url lines leave out the signed URL (S3 signatures, Disk
  tokens): anyone who has it can read the file until it expires. Rails logs
  it at debug only.
- Error reporters (Sentry, Bugsnag, Rollbar, Honeybadger and custom
  handlers) get the context filtered and scrubbed like a log line, instead
  of raw parameters, referers and mail recipients.
The request ID and the running Sidekiq job's context came from SemanticLogger's
named tags after the line had been filtered, by parsing the line's JSON and
writing it again. An X-Request-Id header can carry anything (a card number
was logged as it was sent), and filtering req_id had no effect.

LogStruct::Formatter#call now takes the request ID and job context, and puts
them and the program name through the same filters and scrubbers as the rest
of the line. A line's own req_id wins over the request's. The SemanticLogger
formatter no longer parses and regenerates each line's JSON.
With sql_log_bind_params on, bind values were filtered only when they looked
like tokens or hashes, so a password bind was logged as it was. Binds are now
paired with their column names and filtered like request params (the filter
keys, the default pattern and Rails' filter_parameters), as Rails' own SQL
lines do. Binary values are logged as their size, and type_casted_binds
passed as a lambda (Rails 6.0 and later) are read instead of raising.

The integration's comment said SQL is always parameterized: it isn't (string
conditions, or prepared_statements off), so it now says the SQL is scrubbed
but not filtered.
The colour formatter (opt-in, for development) wrote plain messages, their
payloads, exceptions and tags as they were logged, and loggers added with
broadcast_to got String messages and block results unscrubbed. Both now go
through the string scrubbers, like the strings in a JSON line.
The filtering page said email keys are filtered with a hash by default (they
are scrubbed to [EMAIL:hash] unless the key is filtered too), that
"Password: secret123" is scrubbed to [PASSWORD] (only URL passwords are),
and that Rails' dotted filters and three-argument blocks don't apply. It
now covers word-based key matching and the default pattern, Rails' paths
and blocks, HMAC hashes and their key (pseudonyms, not anonymous data),
query params, card numbers and the Luhn check, scrubbed keys, and what the
integrations leave out: Referer queries, ActiveStorage URLs, delivery job
arguments, SQL binds, error report context. The configuration example is
shown once.

The SOC 2 page listed a _size field that doesn't exist and a payment_data
key that isn't filtered, and promised retention "without privacy concerns".
The docs' sample log generator gave filtered strings a length, arrays a
_length and password keys "[PASSWORD]", and filtered keys LogStruct
doesn't. The README mentions key filtering, Rails' dotted filters and the
Referer without its query.
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 40 billable files and costs up to $10.00.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 40 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 54 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 8e7295b2-7f30-4c92-b6ea-61527ecc732a
📥 Commits

Reviewing files that changed from the base of the PR and between c85cbd5 and bdd8406.

📒 Files selected for processing: 40
  • .cspell/project-words.txt
  • README.md
  • docs/app/docs/filtering-sensitive-data/page.tsx
  • docs/app/docs/soc2-compliance/page.tsx
  • docs/lib/log-generation/__tests__/random-data-generator.test.ts
  • docs/lib/log-generation/log-generator.ts
  • docs/lib/log-generation/random-data-generator.ts
  • lib/log_struct/concerns/configuration.rb
  • lib/log_struct/concerns/error_handling.rb
  • lib/log_struct/config_struct/filters.rb
  • lib/log_struct/formatter.rb
  • lib/log_struct/hash_utils.rb
  • lib/log_struct/integrations/active_record.rb
  • lib/log_struct/integrations/active_storage.rb
  • lib/log_struct/integrations/host_authorization.rb
  • lib/log_struct/integrations/lograge.rb
  • lib/log_struct/integrations/rack_error_handler/middleware.rb
  • lib/log_struct/log/active_storage/url.rb
  • lib/log_struct/param_filters.rb
  • lib/log_struct/semantic_logger/color_formatter.rb
  • lib/log_struct/semantic_logger/concerns/log_methods.rb
  • lib/log_struct/semantic_logger/formatter.rb
  • lib/log_struct/string_scrubber.rb
  • rails_test_app/templates/test/integration/active_storage_test.rb
  • rails_test_app/templates/test/integration/host_authorization_test.rb
  • rails_test_app/templates/test/integration/request_logging_test.rb
  • rails_test_app/templates/test/integration/sql_bind_params_test.rb
  • schemas/log_sources/active_storage.yml
  • test/code_examples/configuration_test.rb
  • test/log_struct/color_formatter_test.rb
  • test/log_struct/configuration_test.rb
  • test/log_struct/error_handling_test.rb
  • test/log_struct/formatter_test.rb
  • test/log_struct/hash_utils_test.rb
  • test/log_struct/integrations/active_record_test.rb
  • test/log_struct/integrations/lograge_formatter_test.rb
  • test/log_struct/integrations/sidekiq_test.rb
  • test/log_struct/param_filters_test.rb
  • test/log_struct/semantic_logger_test.rb
  • test/log_struct/string_scrubber_test.rb
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant