fix(secrets): the scanner printed the credential it found
The report truncated each line to 120 characters and redacted nothing, so a
credential shorter than the budget was printed whole -- into the terminal
scrollback, the CI log, and wherever that log is shipped. It applied to every
mode, which meant a real leak caught by the pre-commit hook was also a real leak
printed to a terminal. The comment above it claimed the match was never echoed
in full; it was corrected to describe the behaviour in the previous commit, and
this changes the behaviour instead.
The match is now masked before truncation. \001 is the substitution delimiter,
as a real control byte rather than the literal backslash-zero-zero-one a
double-quoted "\001" produces -- that first attempt made sed take `\` as its
delimiter and silently substitute nothing, which looked exactly like working
code. These patterns contain both / and |, so either would end the expression
early.
Widening the JWT pattern was part of the same fix, not a separate improvement.
Masking removes exactly what the pattern matched, so `eyJ[A-Za-z0-9_-]{10,}`
redacted the header and printed the payload and signature next to it -- and
those are the token. It now matches all three segments. A pattern that
under-matches is a pattern that half-prints the secret.
Verified in --built and --staged: a planted JWT and a user:pass@host URL are
each reported with file and line, and neither planted value appears anywhere in
the output.
closes #11
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e000f53883
commit
0e9b5c482a
|
|
@ -68,6 +68,11 @@ cd "$(git rev-parse --show-toplevel 2>/dev/null)" || {
|
|||
|
||||
say() { printf 'secrets: %s\n' "$*" >&2; }
|
||||
|
||||
# The delimiter for the masking substitution below. A real control byte, because
|
||||
# these patterns contain both `/` and `|` and either would end the expression
|
||||
# early. It cannot occur in a pattern and it cannot occur in source text.
|
||||
MASK_D=$'\001'
|
||||
|
||||
MODE="staged"
|
||||
BUILT_DIR=""
|
||||
ALLOW=()
|
||||
|
|
@ -92,7 +97,10 @@ done
|
|||
# and it is far too eager to run against source, where it matches ordinary
|
||||
# base64. In a bundle it is worth the noise.
|
||||
BUILT_PATTERNS=(
|
||||
'eyJ[A-Za-z0-9_-]{10,}'
|
||||
# All three segments, not just the header. Masking removes exactly what the
|
||||
# pattern matched, so a pattern that stops at the first dot redacts `eyJ...`
|
||||
# and prints the payload and signature beside it -- which is the token.
|
||||
'eyJ[A-Za-z0-9_-]{10,}(\.[A-Za-z0-9_-]+){0,2}'
|
||||
'\bservice_role\b'
|
||||
'\bapikey["'"'"'[:space:]]*[:=]'
|
||||
)
|
||||
|
|
@ -225,13 +233,18 @@ for pattern in "${PATTERNS[@]}"; do
|
|||
done
|
||||
[ -n "$skip" ] && continue
|
||||
|
||||
# The line is truncated to 120 characters, which bounds how much of a long
|
||||
# value reaches the terminal. It does NOT redact the match: a credential
|
||||
# shorter than the budget is printed whole, and a scanner that prints the
|
||||
# secret it found has published it to the scrollback, the CI log, and
|
||||
# wherever that log is shipped. Masking the matched span is the real fix and
|
||||
# is tracked separately -- until then, treat this output as sensitive.
|
||||
printf ' %.120s…\n' "$hit"
|
||||
# The match is masked, then the line is truncated. Truncation alone was not
|
||||
# enough and used to be all there was: it bounds how much of a LONG value
|
||||
# reaches the terminal and prints a short one whole, so the scanner
|
||||
# published the very thing it was built to find — to the scrollback, the CI
|
||||
# log, and wherever that log is shipped.
|
||||
#
|
||||
# \001 as the delimiter, because these patterns contain both `/` and `|`
|
||||
# and either would end the expression early. It cannot occur in a pattern
|
||||
# and it cannot occur in the text of a source file.
|
||||
masked="$(printf '%s' "$hit" | sed -E "s${MASK_D}${pattern}${MASK_D}[redacted]${MASK_D}g" 2>/dev/null)"
|
||||
[ -n "$masked" ] || masked="[a line matching a credential pattern, unprintable]"
|
||||
printf ' %.120s…\n' "$masked"
|
||||
found=$((found + 1))
|
||||
done < <(printf '%s\n' "$CONTENT" | grep -nEI "$pattern" 2>/dev/null | head -20)
|
||||
done
|
||||
|
|
|
|||
Loading…
Reference in New Issue