Skip to content

fix(request-logger): walk redactPaths through owned data properties only - #2

Merged
Hiprax merged 3 commits into
mainfrom
fix/redact-paths-own-data-walk
Sep 25, 2026
Merged

Hiprax merged 3 commits into
mainfrom
fix/redact-paths-own-data-walk

Conversation

@Hiprax

@Hiprax Hiprax commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Resolves the CodeQL alert js/prototype-polluting-assignment (CWE-1321) on redactEntryPath in src/request-middleware.ts.

Root cause

redactEntryPath read each redactPaths segment as container[segment], which follows inherited properties and runs getters, and it checked ownership only at the final target. The __proto__ / constructor / prototype deny-list (a Set.has check CodeQL does not recognize as a sanitizer) kept it off Object.prototype itself, but probing the built 1.2.0 package showed three real defects:

  1. A path through an object parked on Object.prototype overwrote that one process-wide object.
  2. A path through a response-header value kept by reference (a class instance with its own toJSON(), a Date with an own object field) wrote "[REDACTED]" into a plain object the application owns, in the running app.
  3. Getters on the path ran during logging; a throwing prototype getter escaped the path pass and the whole request went unlogged.

Fix

The walk now steps through and writes into arrays and objects whose prototype is exactly Object.prototype only (every container the package owns has one of those shapes; every by-reference value has neither), reads own data properties through Object.getOwnPropertyDescriptor (never an inherited property, never an accessor), refuses __proto__ / constructor / prototype on every segment with literal comparisons, and never throws. No option or output format changes; every path that redacted a package-owned copy still does.

Tests

7 new tests in tests/request-middleware.spec.ts: 6 failed on the old code for the reasons above, 1 pins the literal deny-list to FORBIDDEN_OBJECT_KEYS. Four deliberate regressions (dropping a literal, reading through the prototype chain, checking ownership only at the final target, removing the try/catch) each turn at least one of them red.

Local: npm run verify passes; the full suite passes on Node 18, 22 and 24 (1092 passed, 1 Windows-only skip, 100% coverage).

redactEntryPath read each segment as container[segment], which follows
inherited properties and runs getters, and it checked ownership only at the
final target. CodeQL flagged the write as js/prototype-polluting-assignment
(CWE-1321). The deny-list kept the walk off Object.prototype itself, but a
path could still overwrite an object parked on Object.prototype, write into
a plain object the application owns behind a by-reference header value (a
toJSON-defining instance, a Date), and run a getter whose throw dropped the
whole log entry.

The walk now steps through and writes into arrays and Object.prototype
objects only, reads own data properties through their descriptors, refuses
__proto__ / constructor / prototype on every segment with literal
comparisons, and never throws.
@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

- normalizeHeaders' allow-list takes only the bag's own keys, so a listed
  header the request lacks cannot pick up a value inherited from a polluted
  Object.prototype and hand that shared object to the path pass.
- redactEntryPath tests the descriptor's own `value`, so an inherited
  `value` / `writable` cannot make an accessor look like a writable data
  slot; descends from the entry only into the fields the package copies;
  and keeps the split inside its try, so it never throws.
- createRequestLogger copies redactPaths at construction, so later changes
  to the caller's array cannot bypass validation or make the body pass and
  the entry pass disagree.
…g paths

Copying redactPaths at construction would silently drop a path an
application adds later and log what it was meant to hide. The middleware
keeps the caller's array as before, and redactEntryPath returns for a
non-string path before reading it, so an entry that bypassed validation can
neither break logging nor run a caller-supplied split(). Also records the
shape-based ownership boundary next to isOwnedPathContainer.
@Hiprax
Hiprax merged commit bb3c05e into main Sep 25, 2026
10 checks passed
@Hiprax
Hiprax deleted the fix/redact-paths-own-data-walk branch September 25, 2026 11:19
@Hiprax Hiprax mentioned this pull request Sep 25, 2026
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.

2 participants