Skip to content

fix: filter structured-text false positives from the generic-secret detector - #29

Merged
nmatt0 merged 1 commit into
masterfrom
fix/generic-secret-precision
Sep 18, 2026
Merged

nmatt0 merged 1 commit into
masterfrom
fix/generic-secret-precision

Conversation

@nmatt0

@nmatt0 nmatt0 commented Sep 18, 2026

Copy link
Copy Markdown
Owner

What

The generic-secret detector (keyword like password/secret/api_key + a high-entropy value) produced almost entirely false positives on firmware that ships a web UI. Its value alphabet and gates (length + entropy + a small wordlist) did not distinguish an opaque credential from structured text — printf/URL format strings, JavaScript object access, and localization strings are all long and diverse enough to clear the entropy gate.

Fix

Add a value-shape filter (generic_value_is_noise) applied to the extracted value, rejecting it when it is:

  1. a format string — % followed by a printf conversion (%s, %02x, …);
  2. an assignment/query shape — an interior = or any & (a trailing run of = is treated as base64 padding);
  3. a no-digit word/identifier chain — after trimming non-alphanumeric ends, the core is only letters and ./_/- separators (dotted member access, snake/camel identifiers, hyphenated/localization words).

Real credential values carry a digit or base64 density and are kept.

Trade-off

Precision-first at the pattern tier: the one accepted false negative (documented in the code) is a purely-alphabetic hardcoded password with no digit.

Validation

Measured before/after across a set of real firmware images:

  • False positives from web/source/localization content drop to zero — e.g. a router web UI 7 → 0, an NVR web UI 32 → 0, a camera web UI 4 → 0.
  • Real values are retained — a device's base64-encoded config credentials 29 → 29, and a JWT-shaped access token still matches.

Tests

test_generic_secret_shape covers the three reject shapes — including the leading/trailing-punctuation and consecutive-separator variants seen in minified JS — plus two must-keep positives (a JWT-shaped token and a digit-bearing password). Full unit suite (1397 checks) and integration suite pass; clean under ASan+UBSan; secrets fuzzer clean.

…etector

The generic-secret rule matches a keyword (password/secret/api_key/...) plus a
high-entropy value, but its value alphabet and gates (length + entropy + a small
wordlist) did not tell an opaque credential apart from structured text. On
firmware with a web UI it produced almost entirely false positives: printf/URL
format strings, JavaScript object access, and localization strings, all of which
are long and diverse enough to clear the entropy gate.

Add a value-shape filter (generic_value_is_noise) that rejects a value which is
(1) a format string ('%' + a printf conversion), (2) an assignment/query shape
(an interior '=' or any '&'; a trailing '=' run is treated as base64 padding),
or (3) a no-digit word/identifier chain (after trimming non-alphanumeric ends,
the core is only letters and '.'/'_'/'-' separators). Real credential values
carry a digit or base64 density and are kept.

Measured across the corpus: false positives from web/source/localization drop to
zero (e.g. a router web UI 7 -> 0, an NVR web UI 32 -> 0), while real values are
retained (a device's base64-encoded config credentials 29 -> 29, a JWT-shaped
access token kept). The one accepted false negative, documented in the code, is
a purely-alphabetic hardcoded password with no digit, at this pattern tier.

Add test_generic_secret_shape covering the three reject shapes (including the
leading/trailing-punctuation and consecutive-separator variants seen in minified
JS) and two must-keep positives.
@nmatt0
nmatt0 merged commit 02ddc22 into master Sep 18, 2026
4 checks passed
@nmatt0
nmatt0 deleted the fix/generic-secret-precision branch September 18, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant