jwt-inspector
Decode and security-audit a JSON Web Token — flag alg=none, missing/excessive expiry, symmetric-alg confusion risk, missing claims — and attempt an offline HMAC secret crack against a wordlist to detect weak signing keys. Use when the user asks to "decode this JWT", "is this toke
Install
npx skills add https://github.com/NovaCode37/claude-security-skills/tree/main/skills/jwt-inspector
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install novacode37-claude-security-skills@llmmart
git clone https://github.com/NovaCode37/claude-security-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole novacode37/claude-security-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
JWT Inspector
Decode and audit JSON Web Tokens with no third-party dependencies. It splits the token, decodes header + payload, evaluates them against a set of security checks, and (for HMAC tokens) tries a fast offline crack of the signing secret against a wordlist.
When to use this skill
- "Decode / inspect this JWT."
- "Is this token configured securely?"
- "Does this JWT use a weak/guessable secret?"
- Auditing auth tokens during a security review.
Checks performed
- alg=none (critical) — unsigned, forgeable token.
- Symmetric alg (HS)* — HMAC verification key == signing secret; HS/RS confusion and brute-force risk.
- Missing
exp/ token never expires; excessively long lifetime. iatin the future, missingnbf, missingiss/aud/sub.- Weak HMAC secret (critical) — cracked from a built-in or supplied wordlist.
How to run it
# Decode + audit
python skills/jwt-inspector/inspector.py "<token>"
# Read token from stdin
echo "<token>" | python skills/jwt-inspector/inspector.py -
# Try cracking the HMAC secret with a custom wordlist
python skills/jwt-inspector/inspector.py "<token>" --secret-list rockyou.txt
# JSON output
python skills/jwt-inspector/inspector.py "<token>" --json
# Only fail CI on high/critical (claim-hygiene notes are LOW)
python skills/jwt-inspector/inspector.py "<token>" --min-severity high
Exit codes: 0 clean · 1 issues reported · 2 malformed input.
Every reported issue fails the build. The default reports everything down to
info (including exp-past); raise --min-severity to low/medium/high
to filter advisory notes out of both the report and the exit code.
Recommended workflow for Claude
- Run the inspector and read the decoded payload to understand the token.
- Report findings ordered by severity; explain the impact of each.
- If a secret was cracked, stress that the key is compromised — rotate it and move to an asymmetric algorithm (RS256/ES256) where feasible.
- Never treat a decoded payload as trusted: decoding ≠ verifying. Remind the user that signature verification with the correct key is what matters.
Note
Cracking only runs for HMAC algorithms and only against the provided wordlist — it is a weak-key detector, not a brute-forcer. Only inspect tokens you are authorized to handle.
Files (claude-security-skills)
-
tests
-
test_inspector.py 6.5 KB
import json import os import sys import time import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import inspector def _b64(obj): return inspector.b64url_encode(json.dumps(obj).encode()) def test_b64url_roundtrip(): data = b"hello world!?" assert inspector.b64url_decode(inspector.b64url_encode(data)) == data def test_decode_valid(): token = inspector.sign_hs256({"alg": "HS256", "typ": "JWT"}, {"sub": "1", "exp": int(time.time()) + 60}, "secret") jwt = inspector.decode(token) assert jwt.header["alg"] == "HS256" assert jwt.payload["sub"] == "1" def test_decode_rejects_malformed(): with pytest.raises(ValueError): inspector.decode("not.a.valid.jwt.token") with pytest.raises(ValueError): inspector.decode("only-one-part") def test_alg_none_is_critical(): token = f'{_b64({"alg": "none", "typ": "JWT"})}.{_b64({"sub": "1"})}.' jwt = inspector.decode(token) issues = inspector.audit(jwt) assert any(i.id == "alg-none" and i.severity == "critical" for i in issues) def test_missing_exp_flagged(): token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "1"}, "x") issues = inspector.audit(inspector.decode(token)) assert any(i.id == "exp-missing" for i in issues) def test_expired_token_noted(): token = inspector.sign_hs256({"alg": "HS256"}, {"exp": int(time.time()) - 100}, "x") issues = inspector.audit(inspector.decode(token)) assert any(i.id == "exp-past" for i in issues) def test_excessive_lifetime_flagged(): token = inspector.sign_hs256( {"alg": "HS256"}, {"exp": int(time.time()) + 60 * 60 * 24 * 800}, "x") issues = inspector.audit(inspector.decode(token)) assert any(i.id == "exp-far" for i in issues) def test_symmetric_alg_flagged(): token = inspector.sign_hs256({"alg": "HS256"}, {"exp": int(time.time()) + 60}, "x") issues = inspector.audit(inspector.decode(token)) assert any(i.id == "alg-symmetric" for i in issues) def test_crack_weak_secret(): token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "1", "exp": int(time.time()) + 60}, "secret") result = inspector.inspect(token) assert result["cracked_secret"] == "secret" assert any(i["id"] == "weak-secret" for i in result["issues"]) @pytest.mark.parametrize("secret", ["password123", "P@ssw0rd", "welcome"]) def test_crack_extended_weak_secret_candidates(secret): token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "1", "exp": int(time.time()) + 60}, secret) result = inspector.inspect(token) assert result["cracked_secret"] == secret assert any(issue["id"] == "weak-secret" for issue in result["issues"]) def test_strong_secret_not_cracked(): token = inspector.sign_hs256( {"alg": "HS256"}, {"sub": "1", "exp": int(time.time()) + 60}, "f3Kd9Lm2Qx8Zp1Rt7Vw4Bn6Cs0Hj5-not-in-wordlist") result = inspector.inspect(token) assert result["cracked_secret"] is None def test_inspect_returns_decoded(): token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "abc", "exp": int(time.time()) + 60}, "secret") result = inspector.inspect(token) assert result["payload"]["sub"] == "abc" def test_cli_exit_code_high_issue(capsys): token = f'{_b64({"alg": "none"})}.{_b64({"sub": "1"})}.' assert inspector.main([token]) == 1 def test_cli_bad_input(): assert inspector.main(["garbage"]) == 2 def test_cli_json(capsys): token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "1", "exp": int(time.time()) + 60}, "secret") inspector.main([token, "--json"]) data = json.loads(capsys.readouterr().out) assert "header" in data and "issues" in data def _low_only_token(): """RS256 token whose only findings are LOW claim-hygiene issues.""" now = int(time.time()) header = {"alg": "RS256", "typ": "JWT"} payload = {"sub": "user-1", "iat": now - 60, "exp": now + 3600} return "{}.{}.{}".format(_b64(header), _b64(payload), inspector.b64url_encode(b"sig")) def test_cli_low_only_issues_exit_one(capsys): """Reported LOW issues fail the build too (issue #46).""" rc = inspector.main([_low_only_token(), "--json"]) data = json.loads(capsys.readouterr().out) assert data["issues"] and all(i["severity"] == "low" for i in data["issues"]) assert rc == 1 def test_cli_min_severity_filters_report_and_exit(capsys): rc = inspector.main([_low_only_token(), "--min-severity", "medium", "--json"]) data = json.loads(capsys.readouterr().out) assert data["issues"] == [] assert rc == 0 def test_inspect_min_severity(): token = f'{_b64({"alg": "none"})}.{_b64({"sub": "1"})}.' all_issues = inspector.inspect(token, min_severity="info") high_only = inspector.inspect(token, min_severity="high") assert len(high_only["issues"]) < len(all_issues["issues"]) assert all(i["severity"] in ("critical", "high") for i in high_only["issues"]) def _info_only_token(): """RS256 token with every claim present but expired: one INFO issue.""" now = int(time.time()) header = {"alg": "RS256", "typ": "JWT"} payload = {"iss": "https://issuer.example", "aud": "api", "sub": "user-1", "iat": now - 7200, "nbf": now - 7200, "exp": now - 3600} return "{}.{}.{}".format(_b64(header), _b64(payload), inspector.b64url_encode(b"sig")) def test_cli_info_issue_reported_by_default(capsys): """INFO issues stay in the default report and still fail the build.""" rc = inspector.main([_info_only_token(), "--json"]) data = json.loads(capsys.readouterr().out) assert [i["id"] for i in data["issues"]] == ["exp-past"] assert [i["severity"] for i in data["issues"]] == ["info"] assert rc == 1 def test_cli_min_severity_low_hides_info(capsys): rc = inspector.main([_info_only_token(), "--min-severity", "low", "--json"]) data = json.loads(capsys.readouterr().out) assert data["issues"] == [] assert rc == 0 def test_inspect_reports_info_by_default(): ids = {i["id"] for i in inspector.inspect(_info_only_token())["issues"]} assert "exp-past" in ids
-
-
inspector.py 8.3 KB
from __future__ import annotations import argparse import base64 import hashlib import hmac import json import sys import time from dataclasses import dataclass, asdict def b64url_decode(segment: str) -> bytes: pad = "=" * (-len(segment) % 4) return base64.urlsafe_b64decode(segment + pad) def b64url_encode(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") @dataclass class DecodedJWT: header: dict payload: dict signature_b64: str signing_input: bytes def decode(token: str) -> DecodedJWT: token = token.strip() parts = token.split(".") if len(parts) != 3: raise ValueError( f"Not a well-formed JWT: expected 3 dot-separated parts, got " f"{len(parts)}.") h_b64, p_b64, s_b64 = parts try: header = json.loads(b64url_decode(h_b64)) payload = json.loads(b64url_decode(p_b64)) except (ValueError, UnicodeDecodeError) as exc: raise ValueError(f"Could not decode header/payload: {exc}") from exc if not isinstance(header, dict) or not isinstance(payload, dict): raise ValueError("Header and payload must be JSON objects.") return DecodedJWT(header, payload, s_b64, f"{h_b64}.{p_b64}".encode("ascii")) @dataclass class Issue: id: str severity: str message: str def to_dict(self) -> dict: return asdict(self) WEAK_ALGS = {"none", "hs256", "hs384", "hs512"} def audit(jwt: DecodedJWT) -> list[Issue]: issues: list[Issue] = [] alg = str(jwt.header.get("alg", "")).strip() alg_lower = alg.lower() if alg_lower == "none": issues.append(Issue("alg-none", "critical", "alg=none: token is unsigned and trivially " "forgeable if the server accepts it.")) elif not alg: issues.append(Issue("alg-missing", "high", "No 'alg' in header — ambiguous verification.")) if "typ" not in jwt.header: issues.append(Issue("typ-missing", "low", "Header has no 'typ' field.")) if alg_lower.startswith("hs"): issues.append(Issue("alg-symmetric", "medium", f"{alg} is symmetric (HMAC): the verification key " "is the signing secret. Risk of HS/RS confusion " "and brute-forceable weak secrets.")) payload = jwt.payload now = int(time.time()) if "exp" not in payload: issues.append(Issue("exp-missing", "high", "No 'exp' claim: token never expires.")) else: try: exp = int(payload["exp"]) if exp < now: issues.append(Issue("exp-past", "info", f"Token expired at {_ts(exp)}.")) elif exp - now > 60 * 60 * 24 * 365: issues.append(Issue("exp-far", "medium", "Token lifetime exceeds 1 year — overly " "long-lived.")) except (TypeError, ValueError): issues.append(Issue("exp-malformed", "medium", "'exp' is not a numeric timestamp.")) if "iat" in payload: try: if int(payload["iat"]) > now + 300: issues.append(Issue("iat-future", "medium", "'iat' is in the future (clock skew or " "forged token).")) except (TypeError, ValueError): pass if "nbf" not in payload: issues.append(Issue("nbf-missing", "low", "No 'nbf' (not-before) claim.")) for claim in ("iss", "aud", "sub"): if claim not in payload: issues.append(Issue(f"{claim}-missing", "low", f"No '{claim}' claim — weakens validation.")) return issues def _ts(epoch: int) -> str: return time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime(epoch)) DEFAULT_WEAK_SECRETS = [ "secret", "password", "123456", "changeme", "admin", "jwt", "token", "secretkey", "supersecret", "key", "your-256-bit-secret", "test", "qwerty", "letmein", "default", "root", "private", "s3cr3t", "password123", "secret123", "jwtsecret", "mysecret", "P@ssw0rd", "welcome", ] _HASH_BY_ALG = {"HS256": hashlib.sha256, "HS384": hashlib.sha384, "HS512": hashlib.sha512} def crack_hmac_secret(jwt: DecodedJWT, candidates) -> str | None: alg = str(jwt.header.get("alg", "")).upper() hashfn = _HASH_BY_ALG.get(alg) if not hashfn: return None try: expected = b64url_decode(jwt.signature_b64) except Exception: return None for cand in candidates: key = cand.encode("utf-8") if isinstance(cand, str) else cand sig = hmac.new(key, jwt.signing_input, hashfn).digest() if hmac.compare_digest(sig, expected): return cand if isinstance(cand, str) else cand.decode("utf-8", "ignore") return None def sign_hs256(header: dict, payload: dict, secret: str) -> str: h = b64url_encode(json.dumps(header, separators=(",", ":")).encode()) p = b64url_encode(json.dumps(payload, separators=(",", ":")).encode()) signing_input = f"{h}.{p}".encode() sig = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest() return f"{h}.{p}.{b64url_encode(sig)}" SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4} def filter_by_severity(issues: list[Issue], min_severity: str = "info") -> list[Issue]: threshold = SEV_RANK.get(min_severity, SEV_RANK["info"]) return [i for i in issues if SEV_RANK.get(i.severity, 3) <= threshold] def inspect(token: str, secret_candidates=None, min_severity: str = "info") -> dict: jwt = decode(token) issues = audit(jwt) cracked = None if str(jwt.header.get("alg", "")).upper() in _HASH_BY_ALG: cands = list(secret_candidates) if secret_candidates else DEFAULT_WEAK_SECRETS cracked = crack_hmac_secret(jwt, cands) if cracked is not None: issues.append(Issue("weak-secret", "critical", f"HMAC secret cracked from wordlist: '{cracked}'. " "Anyone can forge valid tokens.")) issues.sort(key=lambda i: SEV_RANK.get(i.severity, 9)) issues = filter_by_severity(issues, min_severity) return { "header": jwt.header, "payload": jwt.payload, "issues": [i.to_dict() for i in issues], "cracked_secret": cracked, } def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser( prog="jwt-inspector", description="Decode and audit a JWT; detect weak HMAC secrets.") p.add_argument("token", help="the JWT, or '-' to read from stdin") p.add_argument("--secret-list", help="wordlist file for HMAC cracking") p.add_argument("--json", action="store_true", help="emit JSON") p.add_argument("--min-severity", default="info", choices=list(SEV_RANK), help="report issues at or above this severity " "(default: info, i.e. report everything)") args = p.parse_args(argv) token = sys.stdin.read() if args.token == "-" else args.token cands = None if args.secret_list: try: with open(args.secret_list, "r", encoding="utf-8", errors="ignore") as fh: cands = [ln.strip() for ln in fh if ln.strip()] except OSError as exc: print(f"error: {exc}", file=sys.stderr) return 2 try: result = inspect(token, cands, args.min_severity) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 2 if args.json: print(json.dumps(result, indent=2)) else: print("== Header =="); print(json.dumps(result["header"], indent=2)) print("\n== Payload =="); print(json.dumps(result["payload"], indent=2)) print("\n== Findings ==") if not result["issues"]: print(" No issues found. [OK]") for i in result["issues"]: print(f" [{i['severity'].upper():<8}] {i['id']}: {i['message']}") return 1 if result["issues"] else 0 if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass raise SystemExit(main()) -
SKILL.md 2.7 KB
--- name: jwt-inspector description: >- Decode and security-audit a JSON Web Token — flag alg=none, missing/excessive expiry, symmetric-alg confusion risk, missing claims — and attempt an offline HMAC secret crack against a wordlist to detect weak signing keys. Use when the user asks to "decode this JWT", "is this token secure?", "audit a JWT", or "check if this token uses a weak secret". license: MIT --- # JWT Inspector Decode and audit JSON Web Tokens with **no third-party dependencies**. It splits the token, decodes header + payload, evaluates them against a set of security checks, and (for HMAC tokens) tries a fast offline crack of the signing secret against a wordlist. ## When to use this skill - "Decode / inspect this JWT." - "Is this token configured securely?" - "Does this JWT use a weak/guessable secret?" - Auditing auth tokens during a security review. ## Checks performed - **alg=none** (critical) — unsigned, forgeable token. - **Symmetric alg (HS*)** — HMAC verification key == signing secret; HS/RS confusion and brute-force risk. - **Missing `exp`** / token never expires; **excessively long** lifetime. - **`iat` in the future**, missing `nbf`, missing `iss`/`aud`/`sub`. - **Weak HMAC secret** (critical) — cracked from a built-in or supplied wordlist. ## How to run it ```bash # Decode + audit python skills/jwt-inspector/inspector.py "<token>" # Read token from stdin echo "<token>" | python skills/jwt-inspector/inspector.py - # Try cracking the HMAC secret with a custom wordlist python skills/jwt-inspector/inspector.py "<token>" --secret-list rockyou.txt # JSON output python skills/jwt-inspector/inspector.py "<token>" --json # Only fail CI on high/critical (claim-hygiene notes are LOW) python skills/jwt-inspector/inspector.py "<token>" --min-severity high ``` **Exit codes:** `0` clean · `1` issues reported · `2` malformed input. Every reported issue fails the build. The default reports everything down to `info` (including `exp-past`); raise `--min-severity` to `low`/`medium`/`high` to filter advisory notes out of both the report and the exit code. ## Recommended workflow for Claude 1. Run the inspector and read the decoded payload to understand the token. 2. Report findings ordered by severity; explain the impact of each. 3. If a secret was cracked, stress that the key is compromised — rotate it and move to an asymmetric algorithm (RS256/ES256) where feasible. 4. Never treat a decoded payload as trusted: decoding ≠ verifying. Remind the user that signature verification with the correct key is what matters. ## Note Cracking only runs for HMAC algorithms and only against the provided wordlist — it is a weak-key *detector*, not a brute-forcer. Only inspect tokens you are authorized to handle.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.