AUR-466 — the deterministic pass distinguishes secret from placeholder, fixture, and constant SQL
Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.
Command
aurumcode review --base <ref> [--fail-on <level>] [--modelo <nome>] [--seguranca]
Same command AUR-435/AUR-442/AUR-462 published. This card does not add a
flag or change the output shape; it narrows what two of --seguranca's
deterministic rules match, in internal/review/rules/security.yml.
The measured defect (2026-08-14)
Run against a real, 55-commit Node repository the tool had never seen, the
pass reported 15 findings of severity error. Every sampled one was
false, and traced to exactly two rules:
security/hardcoded-secretmatched:README.md: a documentation code block,export GEMINI_API_KEY=sua-chave— the value is the literal word "sua-chave" (Portuguese "your key"), a placeholder.lib/setup-snippets.mjs: a help-text string that teaches the same export line, inside a JS string literal.test/tui-smoke.mjs:process.env.OPENAI_API_KEY = 'ttm-smoke-key', a test fixture.test/e2e.mjs:'sk-body-ULTRA-SECRET'— a value a redaction test asserts ON; the fake value is the test's own subject.security/sql-injectionmatchedlib/db.mjs:'SELECT * FROM daily_rollups' + (where.length ? ' WHERE ' + where.join(' AND ') : '')— a query built entirely from string CONSTANTS. The real values are bound later through?placeholders viaparams.push(...), never concatenated into the string. Correct code.
One shared cause: both rules matched the FORM of the line (a keyword
followed by =/+, a quote nearby) and never looked at the VALUE or the
surrounding CONTEXT. With --fail-on error this blocks every pull
request touching these files, forever, for no reason — worse than
finding nothing, because a team correctly disables the gate and the tool
is worth less than zero.
The fix
Both rules keep their id (the AUR-461 mirror in
internal/prompt/rulecatalog.go compares ids in both directions against
this catalog, so no id was added or renamed; only the two pattern:
values changed in place).
security/hardcoded-secret
The value-shape class was [0-9\-] — a hyphen counted as "the digit
half" of the required letter+digit mix, so any hyphenated multi-word
phrase (sua-chave, ttm-smoke-key, ULTRA-SECRET) satisfied it with no
real randomness. The fix is [0-9\-] → [0-9] (all 4 occurrences, both
alternation branches: quoted and unquoted forms): the value must now
contain a real digit.
This is the "low entropy / dictionary word" placeholder signal from the
card's own design notes, made concrete the only way a line-level regexp
can check it without a dictionary: base64/hex/vendor-token secrets are,
statistically, essentially never digit-free, while hand-typed placeholder
words overwhelmingly are. All four measured false positives contain no
digit at all and are now excluded. Every existing true-positive fixture
value in this repository (AURUM-FAKE-KEY-9000-2222,
AURUM-FAKE-PASSWORD-9000-1111, AURUM-FAKE-TOKEN-0000-0001,
AURUM-FAKE-WEBHOOK-0000-0003) contains a digit and keeps matching
unchanged (AC-003).
Deliberately NOT used: an EXAMPLE|SAMPLE|DUMMY|FAKE|TEST|SMOKE
marker-word denylist, one candidate the card's own design notes raise.
Every existing true-positive fixture in this repository is itself named
AURUM-FAKE-...; that denylist would silence the rule's own regression
fixtures. sk-body-ULTRA-SECRET, one of the five measured false
positives, also contains none of those words and even carries a
real-looking sk- vendor prefix — a marker-word list would have missed
it regardless. The digit rule catches it because it is not the marker
words that distinguish it, it is the absence of randomness.
Known limitation, accepted deliberately: a real secret that happens to be digit-free (some base64 alphabets can produce one) is now missed. The project's own fixtures, and every publicly documented vendor key format sampled while designing this fix, contain digits; widening past "requires a digit" back toward "requires a hyphen or a digit" reintroduces the measured false positives.
Post-approval adversarial-review finding (blocking, fixed in this same
card): digit-bearing secrets with digits clustered at one edge were
missed. The first cut above required a specific ORDER: a digit then a
letter (or a letter then a digit), with 4 or more MORE characters after
the SECOND of those two pivot characters. tests/unit/AUR-442.go:307's
own fixture value, AURUM-FAKE-CANARY-VALUE-7777, has all four of its
digits clustered in the last four bytes of the string, so no choice of
pivot character ever leaves four characters trailing after it — the value
plainly contains both a letter and a digit, just not positioned where the
suffix-only requirement could see it. This is a different defect from the
digit-free limitation above: it is digit-BEARING and was still missed,
and it regressed tests/acceptance/AUR-442.sh AC-001 from exit 0 to exit
1 (ScanNeverEchoesTheMatchedValue: "expected exactly one finding, got
[]").
RE2 has no lookahead, so "contains a digit somewhere, in any position"
cannot be expressed as a single check independent of order and position.
The fix widens each of the two pivot orders (digit-then-letter,
letter-then-digit) from one branch each (padding required strictly AFTER
the second pivot) to two each — padding required BEFORE the first pivot,
OR after the second — four branches total instead of two. This resolves
every concrete shape measured or requested: digits clustered at the end
(AURUM-FAKE-CANARY-VALUE-7777), digits clustered at the start
(7777-AURUM-FAKE-CANARY), and a single digit sitting close to one edge
with little trailing room (AURUM-FAKE-CANARY-7B) — all three are now
tests/unit/AUR-466.go cases
(testAUR466BlockerDigitsClusteredAtEndAreFound and its two siblings),
proven red against the pre-fix pattern (that is exactly how the
regression was discovered) and green after. It is still not a fully
general solution: padding split across BOTH sides in amounts each under 4
characters is theoretically still missable, but no such value exists in
this repository's fixtures or in either the original measurement or the
adversarial review that found this defect.
That remaining gap was measured, not left as a bare "theoretical" claim.
The adversarial review's second pass built the boundary case directly:
token = "9000User" -- 8 characters, 4 digits then 4 letters, no padding
on either side -- escapes all four branches, confirmed both by isolated
Go regex and by a from-scratch run of the sibling acceptance. An
exhaustive search over digit/letter split points and total length found
the escaping window is narrow: values of 6 to 8 characters can escape
when the digit/letter split lands close to 4/4 with no padding on either
side; from 9 characters on, no split point escapes any of the four
branches (a 9-character value always has enough room on at least one side
of at least one pivot). A real secret in the 16-64 character range that
this rule is meant to catch always satisfies some branch. The gap is
real, narrow, and bounded to short (<=8 character) values with an almost
exact 4/4 digit/letter split -- left unfixed under this card's deadline,
but precisely scoped rather than merely asserted.
Two items the adversarial review raised, non-blocking, recorded here rather than chased under this deadline:
ttm-0000000000000000000000(digits present, all identical, zero entropy) still matches. Requiring a digit AND a letter AND non-repetition was considered as a cheaper follow-on filter, but its impact on true positives was not measured in the time available, so it is left as a known, accepted residue: this card's digit check answers "is there a digit" (the axis every measured false positive failed on), not "is this value high-entropy" (a materially harder, unmeasured axis).- The "no common credential format is digit-free" premise behind this whole fix was independently checked, not merely assumed: a real AWS access key ID has roughly a 0.16% chance of containing no digit at all across its 16 trailing base-36 characters, and a hex-only (letters-only) secret is not a format any real generator produces deterministically. The one plausible digit-free real-secret shape is a diceware-style passphrase, which is exactly the limitation already declared above. This validates the trade-off rather than changing it.
security/sql-injection
The pattern was (?i)\b(select|insert|update|delete)\b.*["']\s*\+ — it
required a quote-then-+ to exist SOMEWHERE after the SQL verb, but never
looked at what followed that +. The false positive's own second +
(' WHERE ' + where.join(' AND ')) also happens to be followed by a bare
identifier (where), so simply requiring "an identifier after +"
without also restricting the search is not sufficient — it would still
match at the second +.
Two changes fix this together:
.*(unbounded, can skip past any+) becomes[^+]*. Go's RE2 has no lookahead, so this is what pins the match to the FIRST+after the verb — the character class structurally cannot cross a+, so the engine can never reach the line's second+while trying to satisfy this pattern.- The suffix gains
\s*[A-Za-z_$][\w$.]*: that first+must be immediately followed by a bare identifier or dotted access — the same "does the RHS start with a real identifier character, not(or a quote" shape AUR-462 already established for XSS. The measured false positive's first+is followed by(, excluded structurally; the true-positive shape ("..." + name) is followed directly by the identifiername, still matched.
tests/fixtures/review/vuln/content/02/src/db.py:8 and
tests/unit/AUR-435.go's aur435SQLInjectionLine ("SELECT id, name FROM
users WHERE name = '" + name + "'") both still match byte-for-byte
(AC-003).
Known limitation, accepted deliberately: a query built as "SELECT " +
"FROM " + userInput (verb literal concatenated with another literal
FIRST, the tainted value arriving on a LATER +) is missed, because the
pattern only inspects the first + after the verb. No fixture in this
repository takes that shape; widening past the first + reintroduces the
measured false positive.
What is not attempted
- No path allowlist as the mechanism (
test/,lib/are not treated specially). A real secret committed in a test file is still a real secret, and both fixes work purely on value shape / structural position within the line, not on file path. - No severity change. Both rules stay
severity: error. - No AST or semantic analysis. Both fixes are still single-line regular
expressions the security pass applies to added diff lines; no
cross-line state was added to
internal/review/securitypass.go. - Markdown-fence / multi-line-string detection ("is this line inside a ``` block or a string literal") was considered and rejected: the security pass has no cross-line state today, and adding it to detect fencing is exactly the scope creep the digit-based value check avoids. The digit rule already suppresses the README and help-text cases without needing to know they are documentation at all.
Fixture
tests/fixtures/review/vuln/node-placeholder-vs-secret (built by
tests/fixtures/repos/git-demo/build-fixture.sh from this directory's
history.spec, the same git-less, deterministic, loose-object builder
used by every sibling security fixture) plants, in one file
(src/app.js, plus a README.md), all five AC-001 false-positive shapes
and all three AC-002 true-positive shapes side by side, so
tests/unit/AUR-466.go, tests/integration/AUR-466.go,
tests/e2e/AUR-466.sh, and tests/acceptance/AUR-466.sh all agree on the
same line numbers.
Known gap in this delivery
This card's paths (internal/review/rules, tests/acceptance/AUR-466.sh,
tests/unit/AUR-466.go, tests/integration/AUR-466.go,
tests/e2e/AUR-466.sh, docs/specs/AUR-466.md) do not list
tests/fixtures/review/vuln/node-placeholder-vs-secret, which this fix
requires as a committed, git-less fixture (the sealed acceptance profile
carries no git). The card's own text invites exactly this: "Se precisar
de fixture novo em paths, me peça e eu amplio o contrato." The fixture
is included in this delivery; the contract widening was requested but not
confirmed before the 35-minute deadline. A reviewer must either accept
the fixture as in-scope (it is read-only test data, follows the pattern
of every prior sibling security card's fixture, and contains no real
credential) or the card's paths must be amended before done.