Skip to content

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 10 Sep 21:37
· 51 commits to main since this release
96abd93

v0.9.2 — a userinfo with no password is still a credential

With --llm-anonymize on, postgres://appuser@db:5432/app sent appuser to the model endpoint in the clear. No detector had ever masked a connection string whose userinfo carries a username and nothing else — and single-label hosts like @db, @redis and @rabbit are the ordinary shape in container and compose logs, so this was the common case rather than the corner one.

Found by the sweep. This is the third gap found in one grammar, each by a different accident, so this release does to the credential grammar what v0.9.0 did to detector interference: enumerates it instead of patching it. The userinfo production is specified — RFC 3986 §3.2.1 — so the space of shapes is finite. Twenty-two rows: nineteen run against the real detector chain, three computed from the grammar because no input exists to run them.

The table is what made the fix obvious. The old credential detector was written as two runs with a literal colon between them, but the RFC puts the colon inside the production, not between two of them. Described that way, the rule left three arrangements unreachable — both halves present with one already masked, a password with no username, and a username with no password — and each of the three was found separately, months apart, by a different accident.

What changed: the credential detector becomes one rule over the whole userinfo, and the colon stops being a structural element of the rule set.

://(user)[:](pass)@   ->   ://(userinfo)@
  • the credential grammar is now three rules rather than four. The remaining split — one rule for a pre-masked left half, one for a pre-masked right half — is forced by the detector struct carrying a single capture group and a single tag, and the code now says so. Merged as far as the structure allows, not nearly merged
  • percent-encoding works, and the sweep established why: the classes are negated, so %40 is three ordinary characters and nothing in the package special-cases encoding. Nothing relied on that, and nothing tested it either; two rows now pin it
  • multi-colon userinfos are unchanged, because only the first colon was ever structural and the password class already admitted the rest
  • the fail-closed re-scan now covers this class. A password-less userinfo that ever survives masking mutes the escalation rather than being sent

Two corrections the sweep forced, both in the uncomfortable direction.

The issue as filed said an address literal or a dotted host "rescues" the username by accident, under the wrong tag. Measured, both rescues are partial: they cover only the part of the username that falls inside the rescuing detector's alphabet, which is narrower than the RFC's. svc_user@10.0.0.5 sent _user. us!er@db.acme.com sent !. The defect was worse than the report.

And the widening this fix costs turned out to close something. https://api.acme.com?q=a@b — a query with no path separator, read as a userinfo — sends the host itself in the clear today, because the host detector consumes api.acme.com?q=a@ as context and masks only b. After the merge it is a token and a host, both masked. The over-mask is still an over-mask, and it is now on the same side as a host leak rather than opposite one.

The trade that is purely a cost is smaller and is pinned: on redis://:password@host the colon now disappears inside the placeholder, so the model loses the signal that the absent half was the username. Restore is unaffected — it restores the matched text exactly — so this is fidelity to the model, not correctness.

Measured on the wire, same nine lines through the same capture endpoint, before and after: every closed shape goes from present to zero, while the two shapes this release deliberately does not close still appear. One honest note about that check — the grep understates what main did, because us!er never appears literally in the capture: the partial rescue split it, and only the ! leaked. A pattern-based check could not fully measure the defect it was written for, which is the argument for the table over the greps.

Affected span: v0.4.0 — the release that introduced --llm-anonymize — through v0.9.1. Fifteen tagged releases, counted inclusively from git tag. An earlier draft of this note said sixteen; a disclosure span that overstates itself is the wrong kind of wrong.

Install: download the binary for your OS from the assets below, or go install github.com/maxie7/logscry/cmd/logscry@latest.

Known limitations: three rows of the sweep are open, and the first is the one to know about. #57 — when an earlier detector recognises only part of a userinfo half, the remainder is unreachable to every credential rule, because the tag blocks them and the remainder may lie on either side of it. s3://AKIA…EXAMPLE.prod@bucket sends .prod, and so does the version with a password, which means #46's own fix does not cover it either: an uncovered remainder of an issue that was called closed. It is not fixable by any single pattern while a detector carries one capture group and one tag. #58 — a raw @ inside a userinfo half is illegal per the RFC and common in practice, and the mask stops at the first one, so the true host leaks. It was open at v0.9.2 and is closed in v0.9.3: the credential rules now take the last @ before the authority ends, which is RFC 3986 §3.2's own disambiguation and what net/url already did. #59 — a raw / in a half, where net/url errors out; the honest answer is likely to fail closed and skip the escalation rather than mask it partially, which would be the first place in this package where the answer is to mask less and refuse.

#61, found in the sweep that closed #58 and open from v0.4.0 onward: hosts 2..n of a replica-set or failover DSN — mongodb://user:pass@host1:27017,host2:27017/db — are sent in the clear. It is neither an interference defect nor a grammar gap. The credential and the first host are masked correctly, and the host detector simply stops at the first port colon or comma, so the detector's notion of the value is smaller than the value.

Also open, unchanged from v0.9.1: #54 host disclosure that needs structural work rather than another reordering, #52 a digit in a URL scheme hiding the host on the template field, #51 the current OpenAI project key format matching nothing, #49 an IPv4-mapped IPv6 address masked to its first octet, and #50, latent.

Tracked as #55.