Skip to content

mask() reveals up to 67% of a secret in plaintext for lengths 9-19 (violates design-spec §3) #5

Description

@kimdzhekhon

Summary

mask() in src/scanners.rs can reveal up to 67% of a secret's characters in plaintext for secrets 9-19 characters long — far more than the tool's own documented safety contract.

docs/design-spec.md §3 ("Safety requirements (unique to this tool)") states:

Matched strings are always handled masked (sk-ab12****...****ef34, exposing only the first 4 and last 4 characters).

Root cause

fn mask(raw: &str) -> String {
    let n = raw.chars().count();
    if n <= 8 {
        return "*".repeat(n.max(1));
    }
    let keep = (n / 5).clamp(3, 6);
    ...
}

For n just above the n <= 8 full-mask cutoff, n / 5 rounds down to 0 or 1, so the lower clamp bound of 3 dominates — keep is forced to 3 regardless of how small n actually is. That means 2*3=6 characters are always shown in plaintext (3 head + 3 tail) even for a 9-character secret, where that's most of the value.

Proof (measured, not guessed)

Added a temporary test iterating mask() over lengths 9..=20 and printing the actual reveal ratio:

n=9  keep=3 revealed=6/9  (67%) masked=abc****...****ghi (len=9)
n=10 keep=3 revealed=6/10 (60%) masked=abc****...****hij (len=10)
n=11 keep=3 revealed=6/11 (55%)
n=12 keep=3 revealed=6/12 (50%)
n=13 keep=3 revealed=6/13 (46%)
n=14 keep=3 revealed=6/14 (43%)
n=15 keep=3 revealed=6/15 (40%)
...

For all lengths 9-19, more than 40% of the secret is shown; at n=9 it's 67% — only the 3 middle characters are actually hidden.

Why this matters

Built-in rules (aws_access_key_id, github_token, etc.) all require raw-secret lengths of 15+ characters, so builtin-sourced candidates mostly avoid the worst of this range (though 15-19 still reveal 32-40%). But gitleaks/trufflehog (external tools, when installed) can report arbitrary-length secrets from their own rulesets — short passwords, legacy tokens, etc. — and masked_preview/context_line for those go straight into report.md and the LLM prompt context (promptctx::shared_context) unmodified. A 9-13 character secret masked by this function leaks the majority of its characters into a report that may itself get committed, shared, or pasted into chat — defeating the stated purpose of masking (design-spec.md §3: "to prevent the tool itself from becoming a leak vector").

Fix

Change the lower clamp bound from 3 to 1: let keep = (n / 5).clamp(1, 6);. This keeps the existing ~40% ceiling that already applies at n>=15 (unchanged), while capping the worst case at n=9 down to 22% (2 of 9 characters). Minimal, single-token diff.

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