Phy Jwt Auth Audit

TotalClaw 作者 PHY041 v1.0.0

JWT 和 OAuth/OIDC 安全审核员。解码任何 JWT 令牌(无需验证)以检查 alg/exp/iss/aud/scope 声明,检测“alg:none”绕过漏洞、过期或未过期令牌、过于宽泛的 OAuth 范围、存储在 localStorage 中的 JWT(XSS 盗窃风险)、URL 参数中的 JWT(日志泄漏)、源代码中缺少颁发者/受众验证、.env 文件中的硬编码令牌以及弱 HMAC 机密。还扫描源文件以查找不安全的令牌处理模式:记录承载令牌、与 == 比较的令牌、通过有效负载中的角色:admin 绕过身份验证。生成包含确切代码位置和修复的严重性排名报告。零外部API——纯本地分析。在“JWT 审计”、“令牌安全”、“身份验证安全”、“alg none”、“OAuth 范围”、“不记名令牌”、“令牌过期”、“/jwt-audit”上触发。

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install totalclaw:phy041~phy-jwt-auth-audit
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Aphy041~phy-jwt-auth-audit/file -o phy-jwt-auth-audit.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/c3839cfa852931f58414e6130b081f0b4d2c00ab
## 概述(中文)

JWT 和 OAuth/OIDC 安全审核员。解码任何 JWT 令牌(无需验证)以检查 alg/exp/iss/aud/scope 声明,检测“alg:none”绕过漏洞、过期或未过期令牌、过于宽泛的 OAuth 范围、存储在 localStorage 中的 JWT(XSS 盗窃风险)、URL 参数中的 JWT(日志泄漏)、源代码中缺少颁发者/受众验证、.env 文件中的硬编码令牌以及弱 HMAC 机密。还扫描源文件以查找不安全的令牌处理模式:记录承载令牌、与 == 比较的令牌、通过有效负载中的角色:admin 绕过身份验证。生成包含确切代码位置和修复的严重性排名报告。零外部API——纯本地分析。在“JWT 审计”、“令牌安全”、“身份验证安全”、“alg none”、“OAuth 范围”、“不记名令牌”、“令牌过期”、“/jwt-audit”上触发。

## 原文

# JWT & Auth Auditor

A developer pastes a JWT into a debug log. The logger ships it to Datadog. An attacker finds it in the logs 6 months later. The token never expires.

This skill decodes JWTs without verifying them (which is the point — you need to inspect them even when you don't have the secret), checks their claims against security best practices, scans your codebase for insecure token handling, and finds the OAuth scopes that give more access than necessary.

**Zero external API — all analysis runs locally. Works with any JWT/OAuth provider.**

---

## Trigger Phrases

- "JWT audit", "token security check"
- "auth security", "authentication review"
- "alg:none vulnerability", "JWT algorithm"
- "OAuth scopes", "bearer token check"
- "token expiry", "no exp claim"
- "JWT in localStorage", "token in URL"
- "/jwt-audit"

---

## How to Provide Input

```bash
# Option 1: Decode and audit a specific JWT token
/jwt-audit eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Option 2: Audit source code for insecure token handling
/jwt-audit --scan src/

# Option 3: Audit .env files for hardcoded tokens
/jwt-audit --env-scan

# Option 4: Audit a specific auth file
/jwt-audit src/middleware/auth.ts

# Option 5: Full audit (token decode + code scan + env scan)
/jwt-audit eyJhbG... --scan . --env-scan

# Option 6: Check OAuth scopes in API calls
/jwt-audit --check-scopes

# Option 7: CI mode (exit 1 on critical findings)
/jwt-audit --scan src/ --ci --max-critical 0
```

---

## Step 1: Decode and Analyze JWT Claims

```python
import base64
import json
import time
from dataclasses import dataclass, field
from typing import Any, Optional


@dataclass
class JwtFinding:
    severity: str       # CRITICAL / HIGH / MEDIUM / LOW / INFO
    claim: str          # which claim is affected
    issue: str
    detail: str
    fix: str


def decode_jwt_unsafe(token: str) -> tuple[dict, dict, str]:
    """
    Decode a JWT token WITHOUT verifying the signature.
    Returns (header, payload, signature_b64).
    Safe for inspection purposes — never use for auth decisions.
    """
    parts = token.strip().split('.')
    if len(parts) != 3:
        raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}")

    def b64_decode(s: str) -> dict:
        # JWT uses URL-safe base64 without padding
        padding = 4 - len(s) % 4
        if padding != 4:
            s += '=' * padding
        raw = base64.urlsafe_b64decode(s)
        return json.loads(raw)

    header = b64_decode(parts[0])
    payload = b64_decode(parts[1])
    signature = parts[2]

    return header, payload, signature


def analyze_jwt_claims(token: str) -> list[JwtFinding]:
    """Full security analysis of a JWT token's claims and header."""
    findings = []

    try:
        header, payload, sig = decode_jwt_unsafe(token)
    except Exception as e:
        return [JwtFinding(
            severity='CRITICAL', claim='format',
            issue='Invalid JWT format', detail=str(e),
            fix='Ensure token is a valid JWT (3 base64url parts separated by dots)'
        )]

    now = int(time.time())

    # ── Algorithm checks ─────────────────────────────────────────────────────

    alg = header.get('alg', '')

    if alg.lower() == 'none':
        findings.append(JwtFinding(
            severity='CRITICAL', claim='alg',
            issue='Algorithm "none" — signature verification disabled',
            detail=(
                'alg:none means the JWT has no signature. Any payload can be crafted '
                'and will be accepted by a vulnerable server. This is CVE-2015-9235.'
            ),
            fix=(
                'Server must reject tokens with alg:none. '
                'In jsonwebtoken: jwt.verify(token, secret, { algorithms: ["HS256"] })'
            ),
        ))

    elif alg.startswith('HS') and len(sig) < 32:
        findings.append(JwtFinding(
            severity='HIGH', claim='alg',
            issue=f'Algorithm {alg} with suspiciously short signature',
            detail='Short signature may indicate a weak or guessable HMAC secret.',
            fix='Use a minimum 256-bit (32-byte) random secret for HMAC-SHA256',
        ))

    elif alg == 'RS256' or alg == 'ES256':
        findings.append(JwtFinding(
            severity='INFO', claim='alg',
            issue=f'Algorithm: {alg} (asymmetric — good)',
            detail='RSA/ECDSA signature — cannot be forged without the private key.',
            fix='Ensure public key is loaded from a trusted source, not from the JWT header itself.',
        ))

    # Algorithm confusion: HS256 when server expects RS256
    if alg == 'HS256':
        findings.append(JwtFinding(
            severity='MEDIUM', claim='alg',
            issue='HS256 — verify server rejects RS256→HS256 algorithm confusion',
            detail=(
                'If the server also supports RS256, an attacker may forge tokens using '
                'the public key as the HMAC secret (CVE-2016-10555 pattern).'
            ),
            fix=(
                'Explicitly specify allowed algorithms on verify: '
                'jwt.verify(token, secret, { algorithms: ["HS256"] })'
            ),
        ))

    # ── Expiry checks ────────────────────────────────────────────────────────

    exp = payload.get('exp')
    iat = payload.get('iat')
    nbf = payload.get('nbf')

    if exp is None:
        findings.append(JwtFinding(
            severity='HIGH', claim='exp',
            issue='No expiry (exp) claim — token is valid forever',
            detail=(
                'Without exp, a stolen token can be used indefinitely. '
                'OWASP API Security Top 10 A2: Broken Authentication.'
            ),
            fix='Add exp claim: { exp: Math.floor(Date.now()/1000) + (60*60) } // 1 hour',
        ))
    else:
        ttl = exp - now
        if ttl < 0:
            findings.append(JwtFinding(
                severity='HIGH', claim='exp',
                issue=f'Token EXPIRED {abs(ttl)//3600}h {(abs(ttl)%3600)//60}m ago',
                detail=f'exp={exp}, current time={now}. Using an expired token is a security risk.',
                fix='Generate a fresh token. Check if your token refresh logic is working.',
            ))
        elif ttl > 86400 * 30:  # > 30 days
            findings.append(JwtFinding(
                severity='MEDIUM', claim='exp',
                issue=f'Token expires in {ttl//86400} days — very long-lived',
                detail='Long-lived tokens increase the window of exposure after theft.',
                fix=(
                    'Use short-lived access tokens (≤1 hour) + refresh tokens. '
                    'For JWTs: exp should be 15min–1hr for sensitive endpoints.'
                ),
            ))
        else:
            findings.append(JwtFinding(
                severity='INFO', claim='exp',
                issue=f'Token expires in {ttl//3600}h {(ttl%3600)//60}m',
                detail=f'exp={exp}',
                fix='',
            ))

    # ── Issuer / Audience ────────────────────────────────────────────────────

    iss = payload.get('iss')
    aud = payload.get('aud')

    if not iss:
        findings.append(JwtFinding(
            severity='MEDIUM', claim='iss',
            issue='Missing issuer (iss) claim',
            detail='Without iss, tokens from different issuers (auth providers) are indistinguishable.',
            fix='Add iss claim and validate it on the server: verify(token, key, { issuer: "https://auth.myapp.com" })',
        ))

    if not aud:
        findings.append(JwtFinding(