Skip to content

Secret suffix leaks into masked_preview/context_line for generic_high_entropy_assignment rule #2

Description

@kimdzhekhon

Summary

For the generic_high_entropy_assignment rule, builtin_scan passes the whole regex match (keyword + operator + quotes + secret) to mask() instead of the actual secret capture group. Because mask() keeps a few characters at the tail of whatever string it's given, the tail of the whole match — which is "...end-of-secret + closing quote" — ends up exposed in plaintext in masked_preview and context_line.

This directly violates the tool's core safety guarantee documented in README.md:

Nothing in this tool ever prints, logs, or serializes a raw secret value.

Since masked_preview/context_line are fed straight into the LLM prompt (persona review) and into report.md, this means real trailing characters of scanned secrets can end up in an LLM prompt and in a checked-in/shared report file.

Where

  • src/scanners.rs:210-213 — the rule regex has two capture groups: group 1 = keyword/operator (api_key, token, etc.), group 2 = the actual quoted secret value:
    re: r#"(?i)(api[_-]?key|secret|token|password|passwd|access[_-]?key)\s*[:=]\s*['"]([A-Za-z0-9_\-/+=]{20,})['"]"#,
  • src/scanners.rs builtin_scan (~line 276-280):
    for (re, rule) in &compiled {
        for m in re.find_iter(line) {
            line_matches.push((rule, m.as_str())); // whole match, not the capture group
        }
    }
    m.as_str() returns the whole match, not m.get(2) (the real secret). This whole-match string is later passed to mask() for masked_preview and to mask_line_all() for context_line.

Impact

  • Trailing characters of the actual secret value leak in plaintext into masked_preview / context_line.
  • masked_preview's len=N reports the whole match's length (keyword + operator + quotes + secret), not the secret's own length — inconsistent with the scheme documented in README.md ("longer values keep clamp(n/5, 3, 6) characters at each end ... e.g. AKIA****...****3F2Q (len=20)").
  • Every other rule in rules() has no capture groups (the whole match is the secret), so this only affects generic_high_entropy_assignment — but that's the fallback rule most likely to fire on arbitrary KEY = "..."-shaped code, so it's not a narrow edge case.

Suggested fix

Use the rule's designated secret capture group (group 2 for generic_high_entropy_assignment, whole match for every other rule) as the value passed to mask(), mask_line_all(), and fingerprint() — not the whole regex match.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions