Skip to content

fix(auth-audit): placeholder secrets, shell-default/Windows refs, secret-manager URIs — from a 463-config public scan - #104

Merged
ernestprovo23 merged 6 commits into
mainfrom
ernest/auth-audit-placeholders
Sep 5, 2026
Merged

fix(auth-audit): placeholder secrets, shell-default/Windows refs, secret-manager URIs — from a 463-config public scan#104
ernestprovo23 merged 6 commits into
mainfrom
ernest/auth-audit-placeholders

Conversation

@ernestprovo23

@ernestprovo23 ernestprovo23 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Turns the orphaned auth-audit placeholder work (uncommitted on the laptop since the 463-config public scan) into a reviewed change, tightened so the downgrade path cannot hide a real credential.

Why

Running mcp-warden auth audit over a 463-config public corpus showed 74 % of WRD-AUTH-TOKEN-IN-CONFIG hits were template fill-me-ins (<your-api-key>, YOUR KEY GOES HERE, changeme, xxx) and several correct reference shapes were reported as committed credentials. False highs are what get a gate switched off.

What

  • WRD-AUTH-PLACEHOLDER-SECRET (low) — a shipped config that cannot work is still a finding, but not a leak.
  • Reference forms no longer flagged as literals: ${VAR:-default} / ${VAR:?msg}, %VAR% (Windows), secret-manager URIs (op://, vault://, awssm://, gcpsm://, azkv://, secretref://, keyring://, pass://), credential-file paths.
  • docs/AGENT_GATES.md rule table + a "Placeholders are a separate rule" section; CHANGELOG [Unreleased]; DOCUMENTATION_INDEX.

Tightened during review (vs the original WIP)

Concern in the WIP Fix Negative test
Placeholder words matched as substrings (hereadherence, fillfillmore) whole-token matching (split on non-alphanumerics); multi-word phrases matched as consecutive tokens; bare < dropped in favour of a <slot> rule test_placeholder_words_match_whole_tokens_not_substrings
len < 12 and no digit swallowed short real secrets with punctuation short-bare-word = ^[a-z][a-z_-]{0,10}$ — a digit or any other punctuation disqualifies (hunter2!, p@ssword stay high) test_short_real_secret_shapes_are_not_downgraded_to_placeholder
A vendor-shaped token containing a placeholder word (ghp_example…) would be downgraded vendor/entropy scan runs first and gates the classification — any WRD-SEC-* hit is a credential, never a placeholder, never a "reference" test_vendor_shaped_secret_is_never_downgraded_even_if_it_contains_placeholder_words
Bearer <token> aB3x… (slot beside a literal) counted as placeholder a bracketed slot counts only when nothing but scheme words remains around it test_angle_slot_beside_a_real_literal_is_still_a_credential
_PATH_LIKE accepted any value starting with / — a base64 blob (/9j/4AAQ…) vanished as a "path" bare absolute paths need ≥ 2 directory segments of path-safe chars test_base64_blob_starting_with_slash_is_not_a_credential_path
Snippet redaction of the new finding unproven asserted test_placeholder_finding_is_redacted_like_every_other_snippet

doctor inherits all of this unchanged (doctor_funnel imports _is_auth_key / _looks_like_secret_ref); its 65 tests pass.

Verification (dse-builder-01, py3.12)

  • ruff check . clean
  • Full suite with coverage: 1094 passed / 2 skipped / 8 failed, TOTAL 87 % (floor 80). The 8 are the known builder artefact (bare python = system 3.12 without mcp); re-run with the venv on PATH → test_precommit + test_signing 52 passed / 1 skipped. CI's setup-python is unaffected.
  • Targeted: test_auth_audit + test_doctor* 97 passed.

Note: src/mcp_warden/auth_audit.py is 355 LOC (was 314) — over the 300-LOC guideline before this change; not split here to keep the diff reviewable.

Security review remediation (CSO RED → fixes, second commit)

Finding Bypass string Fix Test
F1 ${VAR:-D} accepted any default as a reference ${TOKEN:-aB3xK9mQ7pL2wR5tY8vN4jH6} _REF_WITH_DEFAULT: D must be empty, itself a reference, or itself a placeholder, else the value is a literal; ${VAR:?msg} stays a reference test_f1_shell_default_is_a_literal_unless_empty_reference_or_placeholder
F2 one placeholder word laundered the rest (any()) example-aB3xK9, your-key-<real> every token must be a strong placeholder or filler word; hard floor — 16+ chars with a digit and any non-placeholder token is never downgraded test_f2_placeholder_needs_every_token_to_be_filler_and_has_a_hard_floor
F3 any ≤12-char alpha run counted as a scheme word correcthorse ${TOKEN} closed set bearer|token|basic|apikey|negotiate|digest (case-insensitive) for both the reference and <slot> remainder test_f3_only_a_closed_set_of_scheme_words_may_sit_beside_a_reference
F4 default credentials downgraded as "short bare words" admin, letmein, changeit, postgres, … _DEFAULT_CREDENTIALS deny-list subtracted from the downgrade (changeme stays a strong placeholder) test_f4_default_credentials_are_never_downgraded
F5 ~/, ./, C:\ branches accepted one segment; op://<token> was a "URI" ~/<real>, op://<real>, /a/b/<real> every locator branch needs ≥ 2 segments and rejects any mixed-case alphanumeric segment ≥ 16 chars test_f5_locators_need_two_segments_and_no_token_shaped_segment
F6 ... matched as a substring abc...<real> anchored: whole value or tail only test_f6_ellipsis_is_anchored
Docs over-claimed "cannot hide a real secret" docs/AGENT_GATES.md now states the true bound: vendor pattern or entropy (24+ chars at ≥ 4.0 bits/char, ≥ 80 % alnum) is guaranteed; shorter/low-entropy keys rely on the bounds above
Negative twin requested every excused prefix + aB3xK9mQ7pL2wR5tY8vN4jH6 test_negative_twin_every_benign_prefix_plus_a_real_token_is_still_high

F7 (doctor absent) was a stale-checkout artefact — doctor is on main and its 65 tests pass against this branch.

Verification after remediation (builder, venv on PATH): ruff clean · full suite with coverage 1109 passed / 2 skipped / 0 failed, TOTAL 88 % · targeted test_auth_audit + test_doctor* 104 passed. auth_audit.py is now 393 LOC — a split into auth_refs.py is the obvious follow-up.

Re-verification remediation (CSO pass 3, N1–N5)

Finding Bypass string (sub-threshold: not caught by vendor/entropy) Fix Test
N1 nested default not checked ${T:-${U:-hunter2!}}, ${T:-${U:-9f8e7d6c5b4a}} default checked with _looks_like_secret_ref recursively, then placeholder test_f1_…
N2 := not an expansion form ${TOKEN:=hunter2!} _REF_WITH_DEFAULT / _SECRET_REF accept :?[-+=] test_f1_…
N3 token-shaped segment required mixed case ~/.config/9f8e7d6c5b4a3e2d1c0b9a8f, ~/.config/GHSAT0AAAAAABCDEFGHIJ case-blind: alphanumeric-only segment, 20+ chars or 16+ at ≥ 3.5 bits/char (shannon_entropy); dotted/underscored file names (application_default_credentials.json) stay locators test_f5_…
N4 short-bare-word heuristic + deny-list qwerty, welcome, dragon, iloveyou inverted to an allowlist {basic, bearer, token, apikey, api-key, digest, negotiate, oauth, none}; _DEFAULT_CREDENTIALS deleted test_f4_short_bare_words_are_placeholders_only_from_a_closed_allowlist
N5 code stricter than the doc your-api-key-goes-here-12345, YOUR_API_KEY_1234567890 hard floor applies only when a non-filler token is present → these stay low test_f2_…
Test evidence F1/F2/F5/negative-twin now use 9f8e7d6c5b4a (12-hex) and 9f8e7d6c5b4a3e2d1c0b9a8f (24-hex, H ≈ 3.92) so each fails on the pre-fix code; one REAL case kept and labelled as the entropy-path control test_negative_twin_…

Must-pass confirmed: ${TOKEN:-9f8e7d6c5b4a} HIGH · your-key-9f8e7d6c5b4a HIGH · ~/9f8e7d6c5b4a HIGH · the 7 benign corpus shapes still [].

Verification after pass 3 (builder, venv on PATH): ruff clean · full suite with coverage 1119 passed / 2 skipped / 0 failed, TOTAL 88 % · targeted test_auth_audit + test_doctor* 114 passed. auth_audit.py is 399 LOC.

Final-pass fold-in (CSO GREEN notes NF-1 / NF-3 / coverage)

Note String Fix Test
NF-1 mixed-case low-entropy segment /etc/Passw0rdPassw0rd _tokenish_segment: 16+ mixed-case alphanumeric is token-shaped regardless of entropy (OR with the ≥ 20 / ≥ 3.5-bit clauses) test_f5_…
NF-3 fragment / :? message op://a/b#9f8e7d6c5b4a3e2d1c0b9a8f, ${API_KEY:?9f8e7d6c5b4a3e2d1c0b9a8f} _text_carries_a_token() over alphanumeric runs in the fragment and the :? message; #key, #credential, :?required, :?set API_KEY in your shell stay references test_f5_…, test_f1_…
Coverage: entropy branch ~/.config/9f8e7d6c5b4a3e2d (16 hex, H ≈ 3.75) → HIGH test_f5_…
Coverage: low-entropy dir ~/.config/aaaabbbbccccdddd/keys.json, ~/.config/my-app/keys.json → locator test_f5_…
Coverage: F6 tail rule correcthorsebatterystaple... → HIGH test_f6_ellipsis_is_anchored
Doc AGENT_GATES.md: hyphens are file-name separators; NF-2 recorded as a known residual (a token that itself contains a separator, e.g. 9f8e-7d6c-…, reads as a file name) — tracked as a follow-up

Verification after fold-in (builder, venv on PATH): ruff clean · full suite with coverage 1119 passed / 2 skipped / 0 failed, TOTAL 88 % · targeted test_auth_audit + test_doctor* 114 passed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lu4ZLyWWL7TXMGYpNFU5Dc

…ret-manager URIs — from a 463-config public scan

Running auth audit over a 463-config public corpus showed 74% of
WRD-AUTH-TOKEN-IN-CONFIG hits were template fill-me-ins, and several correct
reference shapes were reported as committed credentials.

- New WRD-AUTH-PLACEHOLDER-SECRET (low) for obvious fill-me-ins. Built so it
  cannot hide a real secret: vendor/entropy hits are never downgraded, words
  match whole tokens (adherenceTokenValue is not `here`), the short-bare-word
  rule rejects digits/punctuation (hunter2! stays high), and a <slot> beside a
  literal is still a credential.
- References no longer flagged: ${VAR:-default}/${VAR:?msg}, %VAR%,
  op:// vault:// awssm:// gcpsm:// azkv:// secretref:// keyring:// pass://,
  and credential-file paths (bare absolute paths need >= 2 directories so a
  base64 blob starting with / is still a literal).
- Vendor scan runs once per value and gates the classification.
- docs/AGENT_GATES.md rule table + rationale, CHANGELOG, DOCUMENTATION_INDEX.
…(F1–F6)

- F1 ${VAR:-D} / ${VAR:+D} is a reference only when D is empty, itself a
  reference, or itself a placeholder; otherwise D is a literal. ${VAR:?msg}
  stays a reference.
- F2 placeholder requires every token to be a placeholder or filler word, plus
  a hard floor: a 16+ char value with a digit and any non-placeholder token is
  never downgraded.
- F3 only a closed scheme set (bearer|token|basic|apikey|negotiate|digest) may
  sit beside a reference or <slot>; `correcthorse ${T}` is a literal.
- F4 default credentials (admin, password, letmein, changeit, postgres, root,
  ...) never take the short-bare-word downgrade.
- F5 every locator branch (op://, ~/, ./, ../, /, C:\) needs >= 2 segments and
  no mixed-case alphanumeric segment of 16+ chars.
- F6 `...` is anchored (whole value or tail).
- docs/AGENT_GATES.md states the true bound: vendor pattern or entropy
  (24+ chars at >= 4.0 bits/char) is guaranteed; shorter/low-entropy keys rely
  on the bounds above. Each bound is pinned by a test with the bypass string,
  plus a negative-twin test (every excused prefix + a real token stays high).
- N1 a ${VAR:-D} default is checked recursively: ${T:-${U:-hunter2!}} and
  ${T:-${U:-9f8e7d6c5b4a}} are literals.
- N2 ${VAR:=D} is an expansion form too; a non-benign default is a literal.
- N3 token-shaped locator segments are case-blind: alphanumeric-only and
  20+ chars, or 16+ at >= 3.5 bits/char (shannon). ~/.config/<24 hex> and
  ~/.config/GHSAT0AAAAAABCDEFGHIJ are literals; file names with dots or
  underscores (application_default_credentials.json) stay locators.
- N4 the short-bare-word downgrade is an allowlist of scheme/type slots
  (basic, bearer, token, apikey/api-key, digest, negotiate, oauth, none);
  _DEFAULT_CREDENTIALS deleted — admin, qwerty, welcome, letmein are high.
- N5 the 16+/digit hard floor applies only when a non-filler token is present:
  your-api-key-goes-here-12345 and YOUR_API_KEY_1234567890 stay low.
- Tests now use sub-threshold literals (9f8e7d6c5b4a, 24-hex at H~3.92) so each
  assertion fails on the pre-fix code; one REAL case kept as the entropy-path
  control. docs/AGENT_GATES.md + CHANGELOG describe the allowlist rule.
…verage)

- NF-1 a 16+ char mixed-case alphanumeric segment is token-shaped even below
  3.5 bits/char: /etc/Passw0rdPassw0rd is a literal.
- NF-3 a URI fragment and a ${VAR:?msg} message are checked for token-shaped
  runs: op://a/b#<24hex> and ${API_KEY:?<24hex>} are literals; #key and
  :?required stay references.
- Coverage: ~/.config/9f8e7d6c5b4a3e2d (entropy branch), a 16-char low-entropy
  directory name (locator), correcthorsebatterystaple... (F6 tail rule).
- docs/AGENT_GATES.md: hyphens are file-name separators too; NF-2 (a token
  containing a separator) recorded as a known residual, tracked as a follow-up.
@ernestprovo23
ernestprovo23 merged commit 89b78cb into main Sep 5, 2026
11 checks passed
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