Repository navigation
v0.20.0 — the report said *** and showed the value
Two secret leaks. Both were in the part of the tool that exists to keep a secret out of what leaves your machine, and both were silent — which is why this is a minor rather than a patch.
The report said *** and showed the value
{{env.*}} values were redacted from every text channel. The HTML report then embedded the failure screenshot as a base64 data: URI, and nothing masks pixels:
report text contains the secret in the clear? no (masked)
report embeds a PNG as a data URI? YES
size of the embedded PNG: 26 KB
That 26 KB showed the value twice: in the field it was typed into, and in the page's own status line. A type="password" field renders as dots and is safe by accident; an email in a profile header, a token in a "copy this" box, a one-time code echoed into a confirmation are not.
What makes it worse than a file on disk is what the file is for. The report is self-contained precisely so it can be attached to a pull request, uploaded as a CI artifact and forwarded to someone who was not there. A screenshot is captured only on failure — exactly when a report gets shared. .blastproof/reports/ is git-ignored, which protects the repository and does nothing for the copy in the artifact bucket.
When anything in a run references {{env.*}}, the report now links to each failure screenshot instead of carrying it. The PNG stays where it always was, and on that path the file is never read, so no later edit can put the bytes back by accident. A run that referenced no {{env.*}} value is unchanged and still opens offline with its images.
The condition is the run's mask, not the failed test's own placeholders: a login credential is typed once and then echoed by the pages of every test that follows. That costs most authenticated suites the inline image, and the link is one click from it.
Our own CI guide was part of the leak. It recommended uploading report.html and .blastproof/reports/, which is where the unmasked PNGs live. The example now uploads the report alone and says what that directory contains.
The mask compared byte-for-byte
The second leak needs no screenshot. SecretsMask replaced by escaped literal, case-sensitively, so the guarantee held only while the string on the page was byte-identical to the one in the environment. Applications reformat what they echo — ours does:
| supplied | the page showed | before | now |
|---|---|---|---|
HUNTER2 |
Unknown promo code "HUNTER2". |
"***" |
"***" |
hunter2 |
Unknown promo code "HUNTER2". |
leaked | "***" + warning |
Hunter2 |
Unknown promo code "HUNTER2". |
leaked | "***" + warning |
The leaking run and the masked one printed the same score, the same exit code and the same output. Nobody could tell them apart without reading the report by eye.
The comparison is now the value case-insensitively, with runs of whitespace matched as runs of whitespace. That is not a new rule: it is the normalization blastproof already applies in the other direction, when deciding whether a value the model typed came from the page.
It is deliberately the whole rule. There is no catalogue of application-side transforms, and a value your app base64-encodes, hashes or truncates is still not recognised — pinned by a test, so the boundary is checked rather than assumed. An enumeration never closes, and one that implies completeness stops people being careful, which is worse than a limit stated plainly.
What ends is the silence where detection is possible. When a redaction is found only by the widened comparison, the run says so once per variable:
warning: the application returned PROBE_SECRET in a different form than the value supplied
(case or spacing). It was redacted, but a form this tool cannot recognise — an encoding,
a hash — would not have been. Check what that page does with the value.
It names the variable and neither form of the value, and it changes no verdict: a page's formatting is not a test failure.
One cost worth stating. Wider masking redacts a little more of a page, and redacted text cannot be asserted on. The advice in the README has not changed — do not write a step that asserts on an {{env.*}} value — it just matters slightly more.
Verified before tagging
Both leaks were reproduced against examples/demo-app with a real browser, and both fixes were then confirmed by an agentic run with a real model: the report carries no image and its link opens the PNG; the lowercase run warns, the uppercase one does not, neither leaks the value into stdout, stderr or the report, and both reach the same verdict.
Each fix was mutated to check the tests can see it: three mutations on the report path turn 3, 1 and 4 tests red; four on the mask turn 2, 4, 7 and 2 red.
The full dogfood ran on the merge base — 8 of 8, Score 100, 95 model calls against a real browser and a real model. Worth recording honestly: that suite uses no {{env.*}} value anywhere, so it proves nothing regressed on the common path and is not what validates either fix.
Also
examples/demo-app always rendered its discount row, because a display: flex rule beat the hidden attribute. Not shipped code, but it is what this project tests itself against, and a fixture that puts false evidence of success on the page produced exactly that — a wrong PASS, now filed as #112.