Skip to content

fix(scan/jwks): don't let token whitespace or one bad key file silently produce wrong results - #291

Merged
hahwul merged 3 commits into
mainfrom
hahwul/bughunt-scan-jwks
Sep 2, 2026
Merged

fix(scan/jwks): don't let token whitespace or one bad key file silently produce wrong results#291
hahwul merged 3 commits into
mainfrom
hahwul/bughunt-scan-jwks

Conversation

@hahwul

@hahwul hahwul commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Autonomous bug-hunt of the vulnerability scanner + JWKS operations. Found and fixed two real, demonstrable robustness/correctness bugs where surrounding whitespace on a token (a trailing newline is extremely common when reading from a file or pipe) or a single bad key file silently produced wrong security results — the most dangerous kind of failure for a scanner.

Bug 1 — Trailing whitespace on the token silently defeats detection

A JWT/JWE compact serialization never contains whitespace, but tokens routinely arrive with a trailing newline ($(cat token.txt), piped input) or stray spaces (copy-paste). The scanner and JWKS verify/rotate used the token verbatim, so that whitespace landed in the signature segment and broke base64url/HMAC verification.

Concrete repro (token …HwNoQUqm… = {"sub":"a"} signed with the weak secret secret):

$ jwt-hack scan "$TOKEN"          # clean   -> Weak Secret: Uses weak secret: 'secret'  (CRITICAL)
$ jwt-hack scan "$TOKEN\n"        # +newline -> Weak Secret: No common secret found     (FALSE NEGATIVE)
$ jwt-hack scan "  $TOKEN  "      # +spaces  -> "Scan failed: token header is not base64url" (hard fail)

$ jwt-hack jwks verify "$TOKEN\n" --jwks-file oct.json   # -> INVALID (valid key reported invalid)
$ jwt-hack jwks rotate "$TOKEN\n" --keys-dir ./keys/     # -> 0 of 1 keys verified

A single trailing byte silently flips the most critical finding (weak secret) from CRITICAL to "clean", and makes JWKS verification report valid keys as invalid.

Fix: trim surrounding whitespace once at the entry of scan_token, verify_with_jwks, and test_key_rotation. Trimming a compact token is always safe (it has no internal whitespace).

Bug 2 — One unreadable/binary key file aborts the whole rotation batch

test_key_rotation read each key with std::fs::read_to_string(path)?. A single non-UTF-8 key file in the --keys-dir (a DER key, a raw binary HMAC secret) propagated its error and aborted the entire batch, so every other key — including ones that would verify the token — was never tested.

Concrete repro (a directory with one binary .key and one valid good.txt):

$ jwt-hack jwks rotate "$TOKEN" --keys-dir ./keys/
✗ Key rotation test failed: … stream did not contain valid UTF-8   # good.txt never tested

Fix: read each key as raw bytes; on an unreadable file, record a per-key error and continue instead of aborting. A non-UTF-8 key is verified as raw HMAC secret bytes (a JWK oct secret is arbitrary bytes) rather than discarded — so a binary HMAC key now verifies its own token, which was previously impossible.

Tests added

  • cmd::scan::tests::test_scan_token_trims_surrounding_whitespace — padded token yields the same findings (weak secret detected) as the clean one.
  • jwks::tests::test_verify_with_jwks_trims_surrounding_whitespace — whitespace-padded token still verifies against its key.
  • jwks::tests::test_key_rotation_binary_key_does_not_abort_batch — a binary key no longer aborts; a good key after it is still tested.
  • jwks::tests::test_key_rotation_verifies_binary_hmac_secret — a binary HMAC secret verifies its own token.

Verification

  • cargo test — all pass (397 + 164 + integration), including the 4 new tests.
  • cargo fmt --check — clean.
  • cargo clippy --all-targets — no new warnings.
  • Scope: only src/cmd/scan.rs and src/jwks/mod.rs changed. No changes to the JSON/HTML report contract, Severity serialization, or any shared/contract file. Data lines remain structurally intact.

Intentionally left out (minor, to avoid churn)

  • JWE alg:"dir" is matched case-sensitively while the JWE none check is case-insensitive — a alg:"DIR" (RFC-invalid) direct-encryption token isn't flagged. Minor, low severity.
  • The credit-card PII heuristic passes Luhn for degenerate all-zero strings ("0000000000000000"). Honest "credit-card-shaped" heuristic; real cards never start with 0. Left as-is.

…skipped

A token routinely arrives with a trailing newline (read from a file or
pipe) or surrounding spaces (copy-paste). Because scan_token used the
token verbatim, that whitespace landed in the signature segment and
silently broke HMAC verification: the weak-secret check then reported
"No common secret found" for a token whose secret IS weak — a false
negative on the single most critical finding — while a leading space
made the whole scan hard-fail on base64 decoding.

Trim the token once at the start of scan_token so every downstream check
and the generated attack payloads see the canonical token. A compact
JWT/JWE serialization never contains surrounding whitespace, so the trim
is always safe. Adds a regression test asserting a whitespace-padded
token yields the same findings (weak secret detected) as the clean one.
… bad keys

Two robustness bugs in the JWKS operations:

1. A trailing newline on the token corrupted its base64url signature
   segment, so jwks verify and jwks rotate wrongly reported valid keys
   as INVALID. Trim the token in verify_with_jwks and test_key_rotation.

2. test_key_rotation read each key with read_to_string(path)?, so a
   single unreadable or non-UTF-8 key file (a DER key, a raw HMAC secret)
   aborted the ENTIRE batch — every other key, including ones that would
   verify the token, was left untested. Read the key as raw bytes, record
   a per-key error and continue instead of aborting, and verify a
   non-UTF-8 key as raw HMAC secret bytes (a JWK 'oct' secret is arbitrary
   bytes) rather than discarding it.

Adds regression tests: a binary key no longer aborts the batch, a binary
HMAC secret verifies its own token, and a whitespace-padded token still
verifies against its key.
@codecov-commenter

codecov-commenter commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.61702% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/jwks/mod.rs 90.90% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

The JWE Direct Encryption check compared alg == "dir" exactly while the
JWE 'none' check next to it used eq_ignore_ascii_case. A case-varied alg
such as "DIR" — exactly the parser-confusion shape this tool exists to
flag — was therefore silently missed. Match 'dir' case-insensitively to
mirror the 'none' check. Adds a regression test covering both casings.
@hahwul
hahwul merged commit 8c17eff into main Sep 2, 2026
9 checks passed
@hahwul
hahwul deleted the hahwul/bughunt-scan-jwks branch September 2, 2026 00:33
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.

2 participants