fix: strip Cf bidi/format controls in terminal-bound error sanitizers (#183)#188
fix: strip Cf bidi/format controls in terminal-bound error sanitizers (#183)#188beardthelion wants to merge 7 commits into
Conversation
…rs (#183) Terminal-bound error sanitizers must strip Cf bidi/format controls as well as Cc control bytes (INV-6), or a right-to-left override in attacker-influenceable bytes can visually reorder the displayed line (Trojan Source, CWE-451). gl already did this with a local is_bidi_format; two sibling sanitizers shipped with only the Cc half (#183). Centralize the reordering-subset predicate here so all consumers reuse one audited definition instead of re-deriving the set. Deliberately excludes legitimate Cf chars (ZWJ, soft hyphen).
Replace gl's local is_bidi_format copy with the centralized gitlawb-core predicate so exactly one definition exists in the workspace and the three terminal sanitizers cannot drift apart. Behavior-preserving; the existing strip test is unchanged and a must-not-over-strip guard (ASCII + Arabic U+0627 + ZWJ U+200D survive) pins the reordering-only scope.
…ies (#183) safe_error_body_excerpt filtered only Cc control bytes, so a Cf bidi override in a hostile or MITM'd node's HTTP error body reached the operator's terminal and could visually reorder the line (Trojan Source, CWE-451). Drop the reordering Cf subset via the shared gitlawb-core::sanitize::is_bidi_format, without setting pending_space so stripped chars leave no phantom gap. Covered per bidi class plus a transport-path proof through handle_connect, RED observed before the fix.
…pts (#183) sanitize_excerpt filtered only Cc, so a Cf bidi override in a node-advertised iCaptcha service's reason/body reached the terminal on the bail! path (Trojan Source, CWE-451). Drop the reordering Cf subset via the shared gitlawb-core predicate, without pending_space. The existing sanitize_strips_control_chars_and_escapes test asserted only is_control() and so was vacuous for this class (why the gap shipped); a dedicated per-class bidi test now covers it, RED observed before the fix.
The per-class strip tests placed bidi chars only mid-string. A bidi char at index 0 exercises the empty-excerpt/out path (where the pending_space guard behaves differently), confirming a leading override still yields no phantom leading space. Traced correct; now asserted.
Correctness, adversarial, and testing reviewers all flagged that no test locks in the drop-before-count ordering: a dropped bidi char must not consume MAX_ERR_CHARS budget. Behavior verified correct by execution during review; this test guards it against regression (interleaved bidi across the cap still yields exactly MAX_ERR_CHARS visible chars, no phantom spaces).
📝 WalkthroughWalkthroughThe change centralizes bidi-format detection in ChangesBidi sanitization
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/icaptcha-client/src/lib.rs (1)
67-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftConsider hoisting the whole sanitizer loop into
gitlawb-core, not just the predicate.sanitize_excerptandsafe_error_body_excerptare byte-for-byte identical terminal sanitizers (trim → stripCc→ strip bidi → collapse whitespace → cap), differing only in the char-cap constant. This PR adds the same bidi-drop block to both, so the two security-sensitive sanitizers must now be kept in sync by hand — the divergence risk that centralizing the predicate was meant to eliminate. A sharedgitlawb_core::sanitize::sanitize_terminal_excerpt(s, max_chars)would collapse both.
crates/icaptcha-client/src/lib.rs#L67-L72: replacesanitize_excerpt's hand-rolled loop with a call to the shared helper, passingMAX_ERR_CHARS.crates/git-remote-gitlawb/src/main.rs#L573-L578: replacesafe_error_body_excerpt's hand-rolled loop with the same shared helper, passingMAX_ERROR_BODY_CHARS.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/icaptcha-client/src/lib.rs` around lines 67 - 72, Centralize the duplicated terminal sanitization loop in a shared gitlawb_core::sanitize::sanitize_terminal_excerpt(s, max_chars) helper, preserving the existing trim, control/bidi stripping, whitespace collapsing, and character-cap behavior. In crates/icaptcha-client/src/lib.rs at lines 67-72, replace sanitize_excerpt’s hand-rolled loop with the helper using MAX_ERR_CHARS; in crates/git-remote-gitlawb/src/main.rs at lines 573-578, replace safe_error_body_excerpt’s loop using MAX_ERROR_BODY_CHARS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/icaptcha-client/src/lib.rs`:
- Around line 67-72: Centralize the duplicated terminal sanitization loop in a
shared gitlawb_core::sanitize::sanitize_terminal_excerpt(s, max_chars) helper,
preserving the existing trim, control/bidi stripping, whitespace collapsing, and
character-cap behavior. In crates/icaptcha-client/src/lib.rs at lines 67-72,
replace sanitize_excerpt’s hand-rolled loop with the helper using MAX_ERR_CHARS;
in crates/git-remote-gitlawb/src/main.rs at lines 573-578, replace
safe_error_body_excerpt’s loop using MAX_ERROR_BODY_CHARS.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2dffc881-3ae8-46f3-b63e-d96da6221710
📒 Files selected for processing (5)
crates/git-remote-gitlawb/src/main.rscrates/gitlawb-core/src/lib.rscrates/gitlawb-core/src/sanitize.rscrates/gl/src/sync.rscrates/icaptcha-client/src/lib.rs
Two terminal-bound error sanitizers filtered only
Cccontrol bytes and letCfbidi/format controls through to the operator's terminal, so a right-to-left override in an attacker-influenceable error line could visually reorder it (Trojan Source, CWE-451).glalready stripped these;git-remote-gitlawb::safe_error_body_excerptandicaptcha-client::sanitize_excerptdid not.Change
gitlawb_core::sanitize::is_bidi_format: the 12 reorderingCfcode points (LRM/RLM/ALM, the LRE..RLO embedding/override range, the LRI..PDI isolate range). All three client crates already depend ongitlawb-core, so this adds no dependency edges and leaves one audited definition instead of three copies that can drift.gl's local copy to the shared predicate.pending_space(they are not whitespace, so stripping leaves no phantom gap).This deliberately diverges from the issue's "port into each crate" wording. Centralizing removes the exact drift that produced the gap: two divergent copies, one incomplete.
Scope
Strips only the bidi-reordering subset, not all of
Cf. Legitimate characters like ZWJ (U+200D) and soft hyphen pass through unchanged; homoglyph and other Trojan-Source vectors are out of scope.Not addressed here (separate class, follow-up): several
glcommands print node-advertised strings (PR/issue titles, agent model, registration errors) straight to the terminal with no sanitizer at all. Same vector, different shape, tracked separately.Tests
Per-class strip coverage (each
Cfclass asserted individually), a must-not-over-strip guard, a transport-path proof throughhandle_connect, and a cap-boundary test. Every strip and preserve assertion was verified load-bearing by mutation: predicate returning false, predicate over-broad, and each of the 12 arms dropped individually all go red; the real predicate is green.Closes #183.
Summary by CodeRabbit