Fix sensitive: on a nested block redacting nothing - #21
Conversation
Code review — #21 Fix sensitive: cascadeVerdict: mergeable with fixes. The redaction fix is correct, minimal, fail-closed, and proven end to end against both base and head. Nothing here should delay shipping it. But the cascade is resolved privately inside the registry step, so the gem's two other readers of Verified
Strengths
Important
Minor
|
| fields.each do |field| | ||
| Permittable.filter_parameter_registry.add(field[:name]) if field[:sensitive] | ||
| register_sensitive_params(field[:fields]) if field[:fields] | ||
| sensitive = field.key?(:sensitive) ? field[:sensitive] : inherited |
There was a problem hiding this comment.
Because Rails ParameterFilter checks individual leaf keys, cascading the sensitive: state into register_sensitive_params(field[:fields], inherited: sensitive) fixes silent leaks of nested attributes (e.g. card_number inside payment).
| # app-wide that happens to contain it — occasionally a worse outcome than | ||
| # the leak it prevents. | ||
| def register_sensitive_params(fields, inherited: false) | ||
| fields.each do |field| |
There was a problem hiding this comment.
Providing sensitive: false as an explicit opt-out is a crucial escape hatch to prevent common sub-attribute names like :id from being globally redacted via substring matching.
A contract declaring
optional :payment, sensitive: true do
required :card_number, :string
end
printed the card number in the clear. sensitive: on a container was a
complete no-op, and it looked correct in review.
Rails' parameter filtering matches the leaf key it is currently looking
at, never the path that led there. Registering only the container's own
name therefore redacts nothing: the filter proc is handed
("payment", {...}), a Hash is not a String so nothing is replaced, and
ParameterFilter then recurses and asks about "card_number" — which the
container's name never matches.
sensitive: now cascades to every field inside a nested or array
container, at any depth. The spec proves it through
ActiveSupport::ParameterFilter on a realistic payload rather than only
asserting on the registry, which is what let the original behaviour
pass: the existing spec marked the SUB-field sensitive, so it never
exercised a marked container at all.
A sub-field can opt out with sensitive: false. That escape hatch is
load-bearing rather than decorative: matching is a case-insensitive
SUBSTRING match, so cascading a generic name like :id would redact
every parameter in the app containing "id" — user_id, valid,
identity — which is occasionally a worse outcome than the leak it
prevents.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1a8d69c to
a726159
Compare
The bug
printed the card number in the clear.
sensitive:on a nested block or array was a complete no-op — and it looked correct in review.Reproduced against
master:Why
Rails' parameter filtering matches the leaf key it is currently looking at, never the path that led there. So registering only the container's own name redacts nothing:
("payment", {...}).value.replace(FILTERED)doesn't fire.ParameterFilterrecurses and asks about"card_number"— which the container's name never matches.The fix
sensitive:now cascades to every field inside a nested or array container, at any depth:Why the existing spec didn't catch it
It marked the sub-field, so it never exercised a marked container — and it asserted on the registry rather than on redaction. The new spec goes through
ActiveSupport::ParameterFilteron a realistic payload, which is the only assertion that would have failed.sensitive: false— load-bearing, not decorativeMatching is a case-insensitive substring match (mirroring Rails' symbol-filter semantics). So cascading a generic name would redact every parameter in the app containing it —
:idwould take outuser_id,valid,identity. That is occasionally a worse outcome than the leak it prevents, so a sub-field can opt out:It works on a container too, excluding its whole subtree. The
user_idabove staying readable is that opt-out working.Verification
ParameterFilterproof