Skip to content

fix(agents): detect C1 control characters in sanitizeModelWarningValue#103380

Closed
lsr911 wants to merge 2 commits into
openclaw:mainfrom
lsr911:fix/model-selection-c1-control-chars
Closed

fix(agents): detect C1 control characters in sanitizeModelWarningValue#103380
lsr911 wants to merge 2 commits into
openclaw:mainfrom
lsr911:fix/model-selection-c1-control-chars

Conversation

@lsr911

@lsr911 lsr911 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

sanitizeModelWarningValue() in src/agents/model-selection-shared.ts builds the value shown in the "providerless model" warning log (Model "<value>" specified without provider. Falling back to ...). It truncates the value at the first control character so attacker-controlled text after a control byte can't ride along into the log. The truncation boundary only covered the C0 range (0x00-0x1f) and DEL (0x7f), not the C1 control range (0x80-0x9f). C1 includes the CSI introducer U+009B (an alternative ANSI escape prefix equivalent to ESC [).

A residual C1 byte — one that survives the preceding stripAnsi() pass, e.g. U+0080 — was therefore not treated as a truncation boundary: stripAnsi/sanitizeForLog removed the byte itself but the visible suffix after it was retained in the warning.

Change

Add (code >= 0x80 && code <= 0x9f) to the boundary predicate so a residual C1 byte truncates the warning value at that position.

Testing / Proof

Added src/agents/model-selection-shared.test.ts driving the real exported resolveConfiguredModelRef providerless-model path (logger mocked) with gpt4<U+0080>EVIL, asserting the warning truncates to Model "gpt4" ... and drops the EVIL suffix (and carries no raw C1 byte).

Before/after on the exact same test, toggling only the one-line predicate:

Before (current main, boundary = C0/DEL only): test fails —

Received: Model "gpt4EVIL" specified without provider. Falling back to "openai/gpt4EVIL". ...

The residual C1 byte is dropped but the attacker suffix EVIL is retained.

After (this change, boundary includes C1): test passes —

$ node scripts/run-vitest.mjs run src/agents/model-selection-shared.test.ts

 Test Files  1 passed (1)
      Tests  2 passed (2)

The warning is truncated to Model "gpt4" ...; the suffix after the C1 byte is dropped.

sanitizeModelWarningValue() detected C0 control characters but not
C1 controls (0x80-0x9f). Add the C1 range to the control boundary
detection. Same pattern as openclaw#103226, openclaw#103274, openclaw#103379.

Co-Authored-By: Claude <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jul 10, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jul 10, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 10, 2026
@clawsweeper

clawsweeper Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 11, 2026, 4:46 AM ET / 08:46 UTC.

Summary
Extends providerless-model warning truncation to the C1 control range and adds focused regression coverage for a residual U+0080 followed by visible suffix text.

PR surface: Source 0, Tests +51. Total +51 across 2 files.

Reproducibility: yes. at source level with high confidence: U+0080 survives ANSI stripping, current main misses it as the truncation boundary, and the shared log sanitizer removes the byte while retaining the following suffix. No real OpenClaw runtime reproduction has been provided.

Review metrics: none identified.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🐚 platinum hermit
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Post a redacted real OpenClaw warning capture showing the visible suffix after U+0080 is absent.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The revised head adds a focused logger-mocked regression, but it still needs a redacted after-fix OpenClaw warning transcript, terminal capture, or runtime log; after adding proof, update the PR body to trigger review or ask a maintainer for @clawsweeper re-review.

Risk before merge

  • [P1] The contributor has not shown the after-fix warning from a real OpenClaw setup, so the observed runtime result remains unverified outside the mocked test.

Maintainer options:

  1. Decide the mitigation before merge
    Keep the narrow C1 boundary extension and regression, then require a redacted terminal or runtime-log capture showing a real providerless-model warning drops the suffix after U+0080 before merge.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] No concrete code repair remains; contributor-supplied real behavior proof and ordinary maintainer review are the remaining actions.

Security
Cleared: The diff strengthens attacker-controlled warning-text containment and introduces no dependency, secret, permission, artifact-download, or code-execution surface.

Review details

Best possible solution:

Keep the narrow C1 boundary extension and regression, then require a redacted terminal or runtime-log capture showing a real providerless-model warning drops the suffix after U+0080 before merge.

Do we have a high-confidence way to reproduce the issue?

Yes at source level with high confidence: U+0080 survives ANSI stripping, current main misses it as the truncation boundary, and the shared log sanitizer removes the byte while retaining the following suffix. No real OpenClaw runtime reproduction has been provided.

Is this the best way to solve the issue?

Yes. Extending the existing first-control boundary through C1 is the narrowest maintainable fix, preserves model resolution, and the revised head directly covers the previously untested behavior.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against b01affeaee7f.

Label changes

Label justifications:

  • P2: This is a bounded warning/log sanitization bug fix with limited blast radius and no demonstrated urgent outage or blocked user workflow.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The revised head adds a focused logger-mocked regression, but it still needs a redacted after-fix OpenClaw warning transcript, terminal capture, or runtime log; after adding proof, update the PR body to trigger review or ask a maintainer for @clawsweeper re-review.
Evidence reviewed

PR surface:

Source 0, Tests +51. Total +51 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 1 1 0
Tests 1 51 0 +51
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 52 1 +51

What I checked:

  • Current-main gap: At current main, sanitizeModelWarningValue strips ANSI and truncates at C0 or DEL, but does not treat C1 characters as the boundary, so sanitizeForLog removes U+0080 while retaining following visible text. (src/agents/model-selection-shared.ts:155, b01affeaee7f)
  • Shared sanitizer contract: sanitizeForLog already strips ANSI plus C0, DEL, and C1 characters; this PR intentionally adds stronger sink-specific suffix truncation rather than duplicating character removal. (packages/terminal-core/src/ansi.ts:211, b01affeaee7f)
  • Revised regression coverage: The latest head drives resolveConfiguredModelRef with U+0080 plus EVIL and asserts the warning retains only the prefix, addressing the previous review cycle's missing focused coverage. (src/agents/model-selection-shared.test.ts:37, af23a2e6bd0f)
  • Feature provenance: Blame ties the current warning sanitizer and existing providerless-warning test to Vincent Koc's recent current-main commit; older model-selection history also traces to Peter Steinberger's shared-helper refactor. (src/agents/model-selection-shared.ts:155, c58690804f35)
  • Live merge gate: The current head's real-behavior-proof check fails, while the displayed critical quality and security checks pass; the PR body supplies a mocked test run rather than a real runtime warning capture. (af23a2e6bd0f)

Likely related people:

  • vincentkoc: Introduced the current warning-specific sanitizer and its existing providerless-warning coverage, making him the strongest reviewer for this follow-up. (role: introduced behavior and recent area contributor; confidence: high; commits: c58690804f35; files: src/agents/model-selection-shared.ts, src/agents/model-selection.test.ts)
  • steipete: Authored the earlier refactor that established the shared model-selection helper surface and remains relevant for broader model-selection ownership. (role: original shared model-selection contributor; confidence: medium; commits: 310d2db31241; files: src/agents/model-selection-shared.ts, src/agents/model-selection.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-07-10T10:33:46.243Z sha 4b72d62 :: needs real behavior proof before merge. :: none

Add a warning-level regression driving the real resolveConfiguredModelRef
providerless-model path: a value with a residual C1 control byte (U+0080)
followed by visible text truncates the logged warning at the C1 boundary
("gpt4") instead of retaining the attacker-controlled suffix, and a clean
value is preserved. Locks down the new C1 branch in sanitizeModelWarningValue.

Signed-off-by: lsr911 <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added size: S triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed size: XS labels Jul 11, 2026
@lsr911

lsr911 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #104362 — a single sink-wide C1 (0x80–0x9f) pass that folds this sink's fix in with a valid U+009B regression, alongside an audit of every other control-character sink (already-covered / deferred / out-of-scope). This consolidates the isolated C1 PRs per the direction on #103226. Closing in favor of the consolidated review.

@lsr911 lsr911 closed this Jul 11, 2026
lsr911 added a commit to lsr911/openclaw that referenced this pull request Jul 12, 2026
…g sinks

Terminal-control sanitizers guard untrusted text before it reaches a terminal
or log (CWE-117). The canonical range is C0 (0x00-0x1f), DEL (0x7f), and C1
(0x80-0x9f) - the range already used by the shared sanitizeForLog and by
renderTerminalBufferText (openclaw#103274). C1 includes the 8-bit CSI introducer U+009B.

Deliberate sink-wide pass: closes the C1 gap in twelve terminal/log output
sinks, each keeping its own strip/escape/replace/truncate/reject action and
gaining a focused valid-U+009B regression test. The PR body audits every other
control-char sink (already-covered, deferred, or out-of-scope).

Consolidates the isolated PRs openclaw#103379 openclaw#103380 openclaw#103381 openclaw#103382 openclaw#103383 openclaw#103402

Signed-off-by: lsr911 <[email protected]>
openclaw#103405 and re-lands the sanitizeForConsole fix from openclaw#103226 cleanly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant