fix(claudewatcher): scrub credentials embedded in URL userinfo
The scrubber had a rule for `postgres://user:pass@` and none for the
general `scheme://user:secret@host` form — which is exactly what a git
credential store holds:
https://<user>:<token>@git.d-ma.be
Verified against all 13 DefaultRules with an invented value: it matched
none of them. Not authorization-header, not bearer-token, and not
homelab-env-token, which needs a `_TOKEN=`-style prefix a bare URL does
not have. So such a turn was ingested rather than dropped, against the
stated intent in this file's own doc comment:
Fail-closed by design: we'd rather lose signal than ingest
credentials into a public-readable brain.
It failed closed for the shapes it named and open for this one.
How it surfaced: a test suite in mathias/infra executed a script that
configures git while inheriting the operator's real $HOME, read koala's
live ~/.git-credentials, and printed two live values. ~/.claude/projects
is this watcher's own input, so the scrubber was the last line of defence
for that mistake. A sweep of that tree found 12 occurrences of 2 distinct
credential-shaped URLs across 4 transcripts in 3 sessions — so the shape
had been passing for a while, not just that once. Both credentials are
being rotated (infra#466).
The generalisable point is not "do not read the credential file". That
mistake will recur. It is that this shape must fail closed, because the
scrubber is what decides whether such a mistake is survivable or
permanent.
Ordering: the new rule sits after postgres-uri-with-password so a DSN
still reports the more specific name, the same convention as
authorization-header before bearer-token.
Both directions are tested, because a rule that dropped every URL with a
colon would discard most of the signal this watcher exists to keep. The
{8,} floor on the secret half keeps `https://host:443/path` and
`https://user:@host` out; the `[^:\s/@]+` user half stops a host:port
pair being read as userinfo. Must-not-scrub cases include a host with a
port, a password-less DSN, a colon in a query string, and the literal
printf format string `https://oauth2:%s@%s`.
Closes #142
Refs mathias/infra#466
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,26 @@ var DefaultRules = []Rule{
|
||||
// JSON, so both segments begin with "eyJ".
|
||||
{Name: "jwt", RE: regexp.MustCompile(`eyJ[A-Za-z0-9_\-]{8,}\.eyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}`)},
|
||||
{Name: "postgres-uri-with-password", RE: regexp.MustCompile(`postgres(?:ql)?://[^:\s/]+:[^@\s/]+@`)},
|
||||
// Any credential embedded in a URL's userinfo, which is what a git credential
|
||||
// store holds: `https://<user>:<token>@host`. The postgres rule above is the
|
||||
// same shape narrowed to one scheme, and is kept ahead of this one only so a
|
||||
// DSN reports the more specific name -- same ordering convention as
|
||||
// authorization-header before bearer-token.
|
||||
//
|
||||
// Added after this shape passed ALL rules and was therefore ingested rather
|
||||
// than dropped (hyperguild#142). It reached ~/.claude/projects -- this
|
||||
// watcher's own input -- because a test suite in mathias/infra executed a
|
||||
// script that configures git while inheriting the operator's real $HOME, read
|
||||
// koala's live credential store, and printed it. Two credentials were rotated
|
||||
// (infra#466). The scrubber is the control that decides whether that class of
|
||||
// mistake is survivable, and for this shape it was failing open.
|
||||
//
|
||||
// The {8,} floor on the secret half is what keeps `https://host:443/path` and
|
||||
// `https://user:@host` out; the `[^:\s/@]+` user half stops a host:port pair
|
||||
// being read as userinfo. Both directions are covered by
|
||||
// TestScrub_URLUserinfoCredential, because a rule that dropped every URL with
|
||||
// a colon in it would discard most of the signal this watcher exists to keep.
|
||||
{Name: "url-userinfo-credential", RE: regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9+.\-]*://[^:\s/@]+:[^@\s/]{8,}@`)},
|
||||
{Name: "private-key", RE: regexp.MustCompile(`-----BEGIN[^-]*PRIVATE KEY-----`)},
|
||||
{Name: "ssh-key", RE: regexp.MustCompile(`ssh-(?:rsa|ed25519|ecdsa)\s+[A-Za-z0-9+/=]{40,}`)},
|
||||
{Name: "github-pat", RE: regexp.MustCompile(`\b(?:ghp|gho|ghu|ghr|gha)_[A-Za-z0-9]{30,}\b`)},
|
||||
|
||||
Reference in New Issue
Block a user