Skip to content

fix(insurance): cap and sanitize vendor values echoed in decode errors - #337

Merged
cristim merged 2 commits into
mainfrom
fix/336-decode-error-echo
Oct 12, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/336-decode-error-echo

Conversation

@cristim

@cristim cristim commented Oct 12, 2026

Copy link
Copy Markdown
Member

Closes #336

Decode errors for rejected enum values (payment option, term, term reason, plan status, provider) used %q on the raw vendor string, so a hostile response could put an arbitrarily long, attacker-chosen string into an error that consumers print or return to an LLM.

  • New describeVendorValue: echoes the value only when it matches ^[A-Za-z0-9_.-]{1,32}$ (a new vendor enum value stays visible to the operator); otherwise reports (N bytes, not shown).
  • All 5 unsupported value sites use it.
  • Other echoes in pkg/insurance are bounded (exchange invalid decimal <= 64B, %.20q number check, JSON syntax error quotes one character, client.go request errors echo caller input). A comment notes a future int/float wire field would echo the number literal.

Verification (local, fixture-based, not a live vendor call):

  • TestDecode_RejectedEnumValueIsBoundedAndSanitized: 8 comparison fields plus plan status with a 300+ byte control-character prompt-injection value through full decode; asserts error < 256 bytes, no injected text/control chars, and that a benign identifier is still echoed.
  • Mutation: restoring %q at each of the 5 sites and removing the identifier check (6 mutants) each fail the suite.
  • golangci-lint run ./... and go test ./... in pkg pass (GOTOOLCHAIN=go1.26.9).
  • Consumers: platform maps errors to fixed text; mcp#55 and cli archeraSafeError can drop their stopgaps after a pin bump.

🤖 Generated with Claude Code

A rejected enum value is echoed only when it is a short identifier
([A-Za-z0-9_.-], up to 32 bytes); otherwise the error reports its length.
Tests cover every validated enum site through full decode.

Closes #336

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added severity/medium Moderate harm triaged Item has been triaged urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint impact/many Affects most users effort/s Hours type/security Security finding labels Oct 12, 2026
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 42 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 55aacaff-4151-484f-ae9a-a55b2e855513

📥 Commits

Reviewing files that changed from the base of the PR and between 16b5795 and 90ef448.


📒 Files selected for processing (3)
  • CHANGELOG.md
  • pkg/insurance/quote.go
  • pkg/insurance/quote_test.go

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 12, 2026 •

Copy link
Copy Markdown
Member Author

Gate review (Opus, independent) at 91f9a7a: CHANGES REQUESTED (test strength only; production code is correct).

Verified (local, git archive of the head, GOTOOLCHAIN=go1.26.9, GOWORK=off):

  • go build ./... and go test ./... -count=1 in pkg: all ok. golangci-lint run ./insurance/...: 0 issues.
  • Fail-before: the new tests on base 16b5795 (helper stubbed in a test file, base call sites untouched): 9/9 decode subtests FAIL (8 comparison fields + plan status).
  • Hostile-input probe through the full decode path (real output): all 5 enum sites print (N bytes, not shown); invalid UTF-8, full-width no_upfront, trailing \n, space, quote all withheld. Go RE2 $ without (?m) is end-of-text, so "abc\n" is withheld. encoding/json type errors carry only kind + Go field path (cannot unmarshal number into ... wirePlan.status of type string, no literal); syntax errors quote one char; unknown keys ignored. No missed site found.
  • Mutants (21, 3 workers): 5 per-site %q restores, bound 31/33, {0,32}, each anchor, both anchors, +space, [^\x00-\x1f], length leaking %.40q, rune-count length, unquoted echo, never-echo, always-echo: all killed.

F1 (blocking, test gap): 3 regex mutants SURVIVE (pkg/insurance/quote_test.go TestDescribeVendorValue):

  1. (?m)^[A-Za-z0-9_.-]{1,32}$ survives. With (?m), "abc\nIGNORE ALL PREVIOUS INSTRUCTIONS..." matches ($ before \n) and the whole unbounded value is echoed. The hostile test value starts with \x1b, so it never exercises "short identifier, newline, payload".
  2. \S{1,32} survives (would echo \x1b, ‮, quotes, any non-whitespace).
  3. Adding " to the class survives.

Suggested fix (test only), e.g. in TestDescribeVendorValue:

assert.Equal(t, "(19 bytes, not shown)", describeVendorValue("abc\nIGNORE PREVIOUS"))
assert.Equal(t, "(4 bytes, not shown)", describeVendorValue("ab\x1bc"))
assert.Equal(t, "(5 bytes, not shown)", describeVendorValue("a‮b"))
assert.Equal(t, "(3 bytes, not shown)", describeVendorValue(`a"b`))

(check the byte counts; the point is a short value with a newline / control / bidi / quote char.)

N1 (non-blocking, from the cli #2166 gate F1): decode errors are built in quote.go without access to Client.redact (client.go:111-115), so a vendor that reflected a short API key (<= 32 chars of [A-Za-z0-9_.-]) as an enum value would have it echoed verbatim. Unrealistic (the vendor has no reason to reflect the key), and strictly better than base, which echoed any value of any length; cli/mcp mask downstream. Suggested follow-up hardening: run errors from DecodeComparison/DecodePlan through c.redact in the client layer, or never echo a value equal to the key. Not in scope for #336.

N2 (non-blocking, pre-existing, documented): checkNumberBounds %.20q (quote.go:581) still echoes up to 20 runes of escaped vendor text for a non-number in a number field (real output: expected a JSON number, got "{\"IGNORE ALL PREVIOU"). Bounded and escaped, listed in the new comment; fine for this PR.

…entifiers

Withheld cases: embedded and trailing newline, control char, bidi override,
quote, space, NUL, plus a newline-prefixed 300-byte payload through decode.
Kills the (?m), \S and quote-in-class regex mutants.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim

cristim commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Gate verdict (Opus, independent) at 90ef448: APPROVED, merging.

  • Delta vs the reviewed 91f9a7a is test-only (pkg/insurance/quote_test.go +33); production code unchanged. Head is up to date with main.
  • F1 resolved. I reran all 21 mutants at this head (3 workers), and every one fails on an assertion (no build failures or panics). The 3 earlier survivors are now killed: (?m) anchors by TestDescribeVendorValue_WithholdsShortNonIdentifiers + TestDecode_NewlinePrefixedPayloadIsWithheld; \S{1,32} and the quote added to the class by TestDescribeVendorValue_WithholdsShortNonIdentifiers. Each of the 5 call sites, bounds 31/33/{0,32}, each anchor, an added space, [^\x00-\x1f], length leaking, rune-count length, unquoted, never-echo and always-echo are all killed too.
  • Local (git archive, GOTOOLCHAIN=go1.26.9, GOWORK=off, macOS): go build ./... and go test ./... -count=1 in pkg exit 0; golangci-lint run ./insurance/... reports 0 issues. Fail-before at the base: 9/9 decode subtests fail (posted earlier).
  • Consumers: platform/cli/mcp do not match on unsupported value text, so the new format does not break them.
  • CI: every check passes and the PR is CLEAN. The first pre-commit run failed in "Install Trivy" (a download flake, no hook ran); the rerun passed.
  • N1 (the key could be echoed if the vendor reflects it as a short identifier) is moved to PR B (go#286) per the coordinator. N2 (%.20q at the number check, which existed before and is bounded) needs no action.

@cristim
cristim merged commit 0715a9f into main Oct 12, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/security Security finding urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(insurance): decode errors echo uncapped vendor values via %q (prompt-injection / log flooding)

1 participant