Skip to content

Fix/rm 050 review blockers - #484

Open
toderian wants to merge 11 commits into
developfrom
fix/rm-050-review-blockers
Open

Fix/rm 050 review blockers#484
toderian wants to merge 11 commits into
developfrom
fix/rm-050-review-blockers

Conversation

@toderian

@toderian toderian commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

What changed:
- harden excerpt redaction for multi-token and URL-safe credential shapes
- bound DNS and streamed HTTP capture with existing timeout constants
- prove per-node evidence isolation through final comparison aggregation

Why:
- close privacy, availability, and contract blockers found by independent review

Checks:
- focused RM-050 backend suite: 300 passed, 15 subtests passed
- full RedMesh suite: 2045 passed, 4 unrelated baseline failures, 3 skipped
What changed:
- cover compound credential key spellings in excerpt redaction
- follow redirects manually without reading intermediate bodies
- enforce a wall deadline around streamed reads and preserve exact-cap hashes

Why:
- close residual privacy, availability, and evidence-loss findings

Checks:
- response fingerprint and aggregation suites: 55 passed, 9 subtests passed
@toderian toderian self-assigned this Aug 5, 2026
What changed:
- Pin redirect hops to the authorized endpoint. The manual hop loop followed
  any Location, so a target answering `302 -> http://169.254.169.254/` made
  this node fetch cloud metadata and archive it as scan evidence. Each hop is
  now validated against the scanned host and port; an out-of-scope hop is
  refused and the last in-scope response is kept as the evidence.
- Sanitize final_url and title. Both were archived raw while only the excerpt
  was scrubbed, so userinfo credentials and token query values persisted.
  Both now pass through sanitize_excerpt, and a new redaction pattern strips
  URL userinfo (which no key/value pattern recognised).
- Emit body_complete in the http dict. When a body is truncated, body_length
  falls back to the peer's Content-Length and title comes from a partial body;
  consumers comparing vantages need to distinguish measured from declared.

Why:
- The redirect-scope escape is a live SSRF on the shipped path, not only a
  review blocker. Pinning to the authorized host+port is the whole control:
  classifying the hop IP would add nothing and would break scanning private
  targets, which is a normal authorized case.

Checks:
- red_mesh response-fingerprint suite: 41 passed, 7 subtests.
- Full red_mesh suite: 2052 passed; the 4 rulebook-purge failures are
  pre-existing on this branch without these changes (verified by stash).
- git diff --check clean.
What changed:
- Add two payloads to the off-scope redirect cases: `http://host@evil.test/`
  (userinfo trick, where the real host is evil.test) and `//evil.test/x`
  (protocol-relative escape).

Why:
- These are the two classic ways a host pin gets bypassed. Both are refused
  today; encoding them keeps a later refactor of the scope check from
  silently reintroducing them.

Checks:
- response-fingerprint suite: 41 passed, 9 subtests. git diff --check clean.
What changed:
- redirect_stays_on_target now requires urlparse AND urllib3's parse_url to
  agree on host and port. Validating with urlparse alone was bypassable:
  `http://169.254.169.254\@authorized-host/` reads as host authorized-host to
  urlparse but as 169.254.169.254 to urllib3, the parser requests actually
  dials with. The guard passed the hop and the node fetched cloud metadata -
  the SSRF this branch claims to close.
- Scrub captured response headers. `location` is archived and routinely carries
  a token in its query string, especially on a hop refused as off-target whose
  raw Location was stored verbatim.
- Test helper models headers with CaseInsensitiveDict as real responses do; a
  plain dict let a lookup pass in test while missing in production.

Why:
- Found by an independent security review of the pushed branch. The previous
  guard was necessary but not sufficient: it trusted a different parser than
  the one performing the connection. Requiring agreement fails closed against
  the whole differential class, not one spelling of it.

Checks:
- Full red_mesh suite: 2057 passed, 76 subtests; the 4 rulebook-purge failures
  are pre-existing and unrelated (verified earlier by stash).
- Adversarial sweep of 14 payloads: 0 unsafe allows; every URL the guard
  permits resolves to the authorized host under requests' own preparation.
- git diff --check clean.
What changed:
- Extract the reader-shutdown sequence in read_bounded_response_body into a
  single _stop() used by all exit paths, and drain the queue before closing
  the response rather than after.

Why:
- Only the deadline path drained the one-slot queue; the byte-cap and
  queue-empty paths just set the flag and returned. Checking `stopped` before
  a put is not sufficient on its own: a put that was already blocked completes
  as soon as the consumer takes an item, refilling the queue, and the reader
  can then block on a further put before observing the flag. Queue.put has no
  timeout, so that thread waited forever, pinning a chunk and the response.
- Draining before the close matters because response.close() can itself block
  on the lock the reader holds inside iter_content.

Checks:
- response-fingerprint suite: 44 passed, 14 subtests.

Note: this does NOT fix the separate slow-drip hang, where response.close()
blocks past the deadline against a real socket. That is tracked separately and
still blocks merge.
What changed:
- P0.1 Release the connection with socket shutdown instead of Response.close().
  close() acquires the same buffer lock the reader thread holds inside
  iter_content, so against a peer that keeps dripping bytes it blocked far past
  the deadline - a 2s budget had not returned after 60s - stalling the whole
  scan job inline in the phase plan. shutdown() makes the blocked read fail at
  once and the reader unwinds on its own.
- P0.2 Distinguish a token from prose by an unbroken alphanumeric run rather
  than by alphabet. Admitting `-` and `_` without that condition redacted
  ordinary hyphenated titles, CSS class names and paths that develop leaves
  intact, collapsing two genuinely different vantages to one token and
  manufacturing a false "no divergence".
- P1 Match credential keys with (?<![A-Za-z0-9]) rather than \b. An underscore
  is a word character, so \b could never match after one and db_password=,
  jwt_secret= and similar keys archived their values verbatim.
- P1 Guard the body read/parse block. It is no longer safe-by-construction now
  the body is streamed, and an uncaught error there reached execute_job's
  catch-all and skipped every remaining scan phase.
- P0.3 Add real-socket tests: a slow-drip peer must not outlast the deadline
  (fails without P0.1 and fails fast rather than hanging), plus complete and
  oversized capture end-to-end through requests.

Why:
- Every existing test mocked the socket, so a close that blocks and a deadline
  that never fires were both invisible behind a green suite. Blockers found by
  three independent reviews of the pushed branch.

Checks:
- response-fingerprint suite: 49 passed, 21 subtests, 1.36s (was a 15s+ hang).
- Full red_mesh suite: 2062 passed; the 4 rulebook-purge failures are
  pre-existing and unrelated.
- Redaction matrix: 3 prose cases pass through, urlsafe/base64/stripe tokens
  still redact; regex timing 30-38ms at the 4096-char scan cap.
- git diff --check clean.
What changed:
- The byte-cap exit path still called response.close() directly instead of
  _stop(), so the blocking-close hang was only half fixed: a peer that bursts
  past the 1MB cap and then drips keeps the socket alive, close() blocks on the
  reader's buffer lock, and neither the deadline nor _fingerprint_http's
  finally is ever reached. Verified: reverting this one line hangs the new test
  for 20s; with it the read returns in under a second.
- Drop the response.close() fallback from release_response_connection. It was
  the blocking call the helper exists to replace, on the one path where the
  reader is blocked, and the caller's finally already closes the response.
- Remove `=` from the base64 rule's trailing exclusion. Including it made a long
  run followed by padding-plus-more unmatchable, narrowing the rule against what
  develop caught (~3% of previously matched tokens).
- Guard the finally in _fingerprint_http: a raising close would escape both
  handlers, leak the session, and skip every remaining scan phase.
- Consolidate the two near-identical test servers into one _Peer driven by a
  chunk script, with serving()/dripping() constructors, cleanup on both, and a
  clear failure when the local connect fails. Removes ~25 duplicated lines and
  makes the burst-then-drip case expressible - which is what the byte-cap
  regression test needed.

Why:
- Second independent review round. The half-fixed teardown is the one that
  mattered: the docstring stated "every exit path must call this" and one did
  not.

Checks:
- response-fingerprint suite: 50 passed, 21 subtests, 1.39s.
- Full red_mesh suite: 2063 passed; 4 pre-existing rulebook-purge failures.
- Byte-cap regression test verified to fail with the fix reverted.
- git diff --check clean.
What changed:
- Match session-cookie and SAML parameter names explicitly: jsessionid,
  phpsessid and samlresponse carry no `session`/`token` substring, so the key
  alternation never saw them and their values were archived verbatim.
- Make the userinfo password half optional. `https://TOKEN@host/` is a
  credential, and without this the e-mail rule matched it first and replaced
  the host along with it - removing the field that makes two vantages
  comparable in the first place.

Why:
- Second security review round. Both reach the immutable archive through the
  captured location header and the body excerpt.
- `code`, `state`, `sig` and `ticket` were reported alongside these and are
  deliberately NOT added: they are ordinary English words, and redacting them
  would destroy comparison signal exactly as the base64 rule did before it was
  narrowed. Recorded as a known residual instead.

Checks:
- response-fingerprint suite: 53 passed, 27 subtests.
- Full red_mesh suite: 2066 passed; 4 pre-existing rulebook-purge failures.
- Verified both directions: ordinary URLs, prose and CSS class names pass
  through untouched; userinfo, session ids and keyed credentials are redacted.
- git diff --check clean.
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