Skip to content

fix(insurance): keep the API key out of every field fmt can print - #331

Merged
cristim merged 2 commits into
mainfrom
fix/325-insurance-key-format
Oct 10, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/325-insurance-key-format

Conversation

@cristim

@cristim cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member

Closes #325. Follow-up for the remaining Config.APIKey exposure: #330.

Cause

fmt does not call Format on unexported struct fields, and for %s/%q it dereferences a *Client and prints every field, including cfg.APIKey. A Client or Config held by value in an unexported field printed the key under every verb.

Change

Client no longer stores a Config or any string containing the key: it keeps baseURL, orgID (a UUID identifier), hc, and two closures (setAuth writes x-api-key per request; redact masks an echoed key in vendor messages, no-op on an empty key). Client.Format is removed as redundant (deletion probe: TestConfigAndClient_NeverFormatTheKey passes without it). Config.Format stays; its doc now says a caller-held Config in an unexported field still prints raw (#330). NewClient still copies the caller's http.Client (noRedirect := *hc) and adds no headers to it; only setAuth writes the header, per request. Public API unchanged. Hex/quoted echoes of the key in vendor messages stay out of scope, as before.

Pre-fix control (56db459 client.go + the new test): 48 failing subtests, including

  • TestClientKeyNeverPrintedWherever/unexported_*Client/%s and /%q (the issue's reproducer; &unexported_*Client/%s and /%q too)
  • .../unexported_Client_value/{Sprint,Sprintln,%v,%+v,%#v,%s,%q,%x,...}
  • TestClientHoldsNoStringContainingKey (reflection walk finds Client*.cfg.APIKey)
    After the change all pass.

Evidence (offline)

  • Matrix: 10 holder shapes (unexported pointer and value, pointer holder, exported, nested, slice, map, []any, wrapped error) x Sprint Sprintln %v %+v %#v %s %q %x %X %d %T %p, forbidding the key, its hex, base64 and quoted forms; plus json.Marshal.
  • Reflection walk over every field of Client (unexported included) asserts no string or byte slice contains the key, independent of fmt behavior.
  • Mutations: re-adding a key copy as a Client field fails 56 subtests including the walk; redact returning its input fails TestClient_ErrorMessageIsSanitized. A package-level variable holding the key is not reachable from any Client value, so it is not a printable path and no Client test can see it (not claimed). Dropping the empty-key guard in redact is unreachable (NewClient rejects a blank key) and survives; kept as a no-op guard on request of the review.
  • go vet, golangci-lint, go test ./... in pkg pass.

Consumers

Nothing outside pkg/insurance uses Client fields (rg over platform, cli, mcp and their worktrees: only NewClient/*insurance.Client). After merge, platform, cli and mcp each bump their pkg pin; they can then drop the atomic.Pointer and own-Format workarounds in a later cleanup. Until then keep the client in locals or atomic.Pointer.

🤖 Generated with Claude Code

A *Client in an unexported struct field printed the key under %s and %q, and a Client or Config held by value leaked under every verb, because fmt never calls Format on unexported fields. Client now keeps the key only inside setAuth and redact closures and the org ID as a plain identifier. Client.Format is removed as redundant; Config.Format stays.

Closes #325

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim cristim added triaged Item has been triaged urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/s Hours type/bug Defect labels Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 2 billable files and costs up to $0.50.

  • 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 53 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 88 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: f461463c-af4a-475f-8273-14680686c199

📥 Commits

Reviewing files that changed from the base of the PR and between d9ead42 and 798d18c.


📒 Files selected for processing (2)
  • pkg/insurance/client.go
  • pkg/insurance/key_leak_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 10, 2026

Copy link
Copy Markdown
Member Author

Gate review (security: credential redaction) at 84a9a5c: content verdict CLEAN, no blocking findings.

  • Pre-fix control reproduced: 56db459 client.go + this PR's key_leak_test.go gives 48 failing subtests plus the parent test (49 FAIL lines), including unexported *Client %s/%q (the issue) and unexported Client by value under all verbs; the reflection walk fails on Client.cfg.APIKey. At head: pkg/insurance 225 passed, go vet clean (git archive, GOWORK=off -mod=readonly, go1.26.9).
  • Mutations at head: re-add a key copy as a Client field fails the matrix (and walk); redact returning its input fails TestClient_ErrorMessageIsSanitized; setAuth not writing the header fails TestClient_Comparison_RequestAndDecode; empty-key guard removed survives (unreachable, see below).
  • Closure capture is not reachable by fmt, json or reflection (checked on a scratch struct with func fields: no leak under %v %+v %#v %s %q %x %d, json.Marshal).
  • Config.Format retained with the corrected caveat; Client.Format removed and TestConfigAndClient_NeverFormatTheKey passes. NewClient still copies the caller's http.Client (noRedirect := *hc) and adds no header to it; only setAuth writes x-api-key per request. Redaction order is unchanged (control-char strip, redact, truncate).
  • Rulings: (1) keep the empty-key guard in redact: ReplaceAll with an empty old string would splice "[redacted]" between every rune, it is the same guard the old cleanMessage had (moved, not added), one line. (2) The package-level-key mutation is correctly disclosed as outside what a Client test can see; not a finding. (3) PR body is honest, with one nit: "public API unchanged" omits that the exported method Client.Format is removed (Client stops implementing fmt.Formatter); no in-repo or consumer caller, so not breaking in practice.
    Merge pending: branch is BEHIND main (go queue serialized behind fix(aws/recommendations): fail the detail on an invalid average-instances or SP utilization #327); update-branch once then re-gate.

@cristim

cristim commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Gate evidence at 798d18c (after one update-branch): client.go and key_leak_test.go byte-identical to the reviewed 84a9a5c (content verdict CLEAN above); go test ./insurance ok at this SHA (git archive, go1.26.9, GOWORK=off); CI 15/15 pass; mergeStateStatus CLEAN. Merging with --match-head-commit.

@cristim
cristim merged commit e8af37b into main Oct 10, 2026
15 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/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(insurance): the API key is printed by fmt %s when a *insurance.Client is held in an unexported struct field

1 participant