Skip to content

fix(compute-gateway): mask every result shape, not just nodes - #1396

Merged
mdheller merged 1 commit into
mainfrom
fix/masking-covers-all-result-shapes
Aug 4, 2026
Merged

fix(compute-gateway): mask every result shape, not just nodes#1396
mdheller merged 1 commit into
mainfrom
fix/masking-covers-all-result-shapes

Conversation

@mdheller

@mdheller mdheller commented Aug 4, 2026

Copy link
Copy Markdown
Member

Closes a live hole. Masking went on in #1387. _iter_records walked data["nodes"] and nothing else, so rows / table / edges shaped results were returned UNMASKED while the decision output reported the policy active.

rows is a shape this estate actually emits — both hellgraph-service and compute-gateway produce it. So personal data was being served in cleartext on those paths with masking nominally enforced.

I pinned that limit with a test instead of fixing it when the policy landed. Wrong call for something already enforcing in production.

The fix

The walker is now shape-agnostic and depth-bounded — every nested dict is a candidate record. An allowlist of container keys only moves the hole to the next shape someone adds.

This is safe because _mask_record rewrites only keys named in mask_fields. A dict with no configured field comes back untouched. So:

  • over-walking → nothing happens
  • under-walking → personal data is served

Fail toward the former. output.data itself is excluded: it's the envelope, not a record, and masking its top level would rewrite container keys that collide with a field name.

Tests

Test Asserts
test_every_result_shape_is_masked_not_just_nodes nodes / rows / edges / records all leak nothing
test_nested_payloads_are_reached a record under result.page.items is still masked
test_over_walking_is_harmless a payload with no configured field returns byte-identical
test_non_read_kinds_still_untouched widening the walker didn't widen the governed kinds

252 tests pass.

@mdheller
mdheller force-pushed the fix/masking-covers-all-result-shapes branch from b451d8b to 96bef83 Compare August 4, 2026 14:59
Closes a LIVE hole. The masking PDP went on in #1387, and `_iter_records` walked
`data["nodes"]` and nothing else — so a result shaped as rows / table / edges was returned
UNMASKED while the decision output reported the policy active. `rows` is a shape this estate
actually emits (hellgraph + compute-gateway both do), so personal data was being served in
cleartext on those paths with masking nominally enforced.

I pinned that limit with a test rather than fixing it when the policy landed. That was the
wrong call for something already enforcing in production.

The walker is now shape-agnostic and depth-bounded: every nested dict is a candidate record.
An allowlist of container keys only moves the hole to the next shape somebody adds. This is
safe because _mask_record rewrites ONLY keys named in the policy's mask_fields — a dict with
no configured field comes back untouched — so over-walking does nothing while under-walking
serves personal data. test_over_walking_is_harmless asserts a payload with no configured
field is returned byte-identical, so widening coverage cannot corrupt unrelated envelopes.

output.data itself is excluded: it is the envelope, not a record, and masking its top level
would rewrite container keys that happen to collide with a field name.

Tests: every shape (nodes/rows/edges/records) asserted to leak nothing; nested payloads
reached; over-walking harmless; READ_KINDS unchanged so widening the walker does not widen
the kinds governed. 252 pass.
@mdheller
mdheller force-pushed the fix/masking-covers-all-result-shapes branch from 96bef83 to 0cd23cc Compare August 4, 2026 15:47
@mdheller
mdheller enabled auto-merge (squash) August 4, 2026 16:39
@mdheller
mdheller merged commit aba4e85 into main Aug 4, 2026
94 checks passed
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