auth-review
Reviews authentication and authorization in any codebase — login flows, password storage, session/cookie handling, JWT pitfalls, OAuth/OIDC misconfigurations, IDOR/broken access control, privilege escalation, CSRF protection, route census for unguarded endpoints, trusted-header identity spoofing, check-then-act race conditions (TOCTOU), and WebSocket/SSE authorization. Use when auditing how users log in, how permissions are enforced, how internal services trust each other, or when reviewing auth-related code changes.
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 81
- Forks
- 17
01 前言 Front Matter
- name
- auth-review
- license
- MIT
- description
- Reviews authentication and authorization in any codebase — login flows, password storage, session/cookie handling, JWT pitfalls, OAuth/OIDC misconfigurations, IDOR/broken access control, privilege escalation, CSRF protection, route census for unguarded endpoints, trusted-header identity spoofing, check-then-act race conditions (TOCTOU), and WebSocket/SSE authorization. Use when auditing how users log in, how permissions are enforced, how internal services trust each other, or when reviewing auth-related code changes.
02 资源文件 3
- 许可证LICENSE1.0 KB
- 参考文档SOURCE.md368 B
- 参考文档TRUST.auto.yaml2.1 KB
03 SKILL.md 正文
Authentication & Authorization Review
1 — Map the auth architecture first
Find: login/register/password-reset endpoints, session middleware, role/permission checks, token creation/validation, OAuth client configs. Write a 3-line model: who can authenticate how → what identity looks like → where authorization is enforced. Broken access control is much easier to spot once you know the intended model.
Route census (enumerate, don't guess)
Pattern greps find what they look for; a census finds what nobody remembered. Enumerate EVERY route, then check each one:
rg -n "app\.(get|post|put|patch|delete|all|use)\(" # Express/Koa
rg -n "@app\.route\(|@\w+\.route\(|add_url_rule" # Flask
rg -n "path\(|re_path\(" */urls.py # Django
rg -n "^\s*(get|post|patch|put|delete) |resources " config/routes.rb # Rails
rg -n "@(Get|Post|Put|Patch|Delete|Request)Mapping" # Spring
rg -n "Route::(get|post|any|resource|apiResource)" routes/ # Laravel
rg -n "@(app|router)\.(get|post|put|delete|patch)" # FastAPI
Build a table route → auth middleware → ownership check and fill it by reading each handler. Every row with neither is a finding (Critical for admin/state-changing, High otherwise). Census-only catches:
- routes registered BEFORE the auth middleware (order bugs —
app.use(auth)must precede every guarded route) - the one handler missing
@login_required/authorize/auth:sanctumin an otherwise-guarded file - non-HTTP surfaces that are routes in disguise: queue consumers, cron/background job handlers, webhook receivers, GraphQL resolvers (see
../graphql-security/SKILL.md), event subscribers, RPC/message handlers — census them with the same table - dispatch-style apps (
?action=→ switch) — enumerate the switch arms, not the single URL - case-sensitive middleware paths: on case-insensitive hosts/routers,
app.use('/admin', guard)can be bypassed by/ADMINor/Admin/— check the router's case-sensitivity setting (Express: default sensitive only with regex; some frameworks/filesystems are not) and whether any guarded prefix can be cased around → authz bypass - encoding-sensitive middleware paths (CWE-177): authz guards comparing the RAW, still-encoded request URI (
req.url/req.originalUrl, JavagetRequestURI(),RAW_URI) while the router dispatches on the DECODED path —GET /%61dmin/usersfailsstartsWith('/admin')yet still reaches the admin route → unauthenticated admin access (class incident: Cisco Catalyst SD-WAN Manager CVE-2026-76504, KEV). Safe shape: compare the decoded path the router actually dispatches on (req.path, JavagetServletPath()), or mount the guard on the route itself (app.use('/admin', guard)), so percent-encoding can't split the check from the dispatch:
rg -n "req\.(url|originalUrl)\.(startsWith|endsWith|includes|match)|getRequestURI\(\)\.(startsWith|contains|equals)|RAW_URI" -g '*.js' -g '*.ts' -g '*.java' -g '*.py'
- suffix/contains path matching in auth whitelists (third variant of
the path-matching family: case → encoding → MATCHER STRICTNESS): a
public-path exemption checked with
path.endsWith('/x')orpath.contains('/x')instead of an exact/anchored match frees EVERY colliding route, not just the intended public one (class incident: Kestra CVE-2026-49869, KEV —AuthenticationFilterusedrequest.getPath().endsWith("/configs")to whitelist the public configs endpoint; every namespace-scoped.../configsroute bypassed Basic Auth → unauthenticated API access → RCE). Safe shape: exact match set (new Set(['/configs']).has(path)) or anchored regex (^/configs$) — never suffix/substring tests on a path:
rg -n "endsWith\(\s*['\"]\/|\.contains\(\s*['\"]\/" src/ -g '*.java' -g '*.js' -g '*.ts' -g '*.py' | head
- setup/install/first-run wizard routes:
app.post('/setup/restore'),/install,/api/setup/*are unauthenticated BY DESIGN during first boot — one that stays reachable after initialization is an auth-bypass front door (class incident: ground-station CVE-2026-103244 —setup.restoreinvoked over Socket.IO during setup mode planted admin users and forged session tokens; full takeover). Census them with the route table; every setup route must verify installation state server-side and refuse once installed:
rg -n -i "['\"]/(setup|install|installer)[^'\"]*['\"]" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.rb' -g '*.php' | head -15
- gRPC / protobuf services:
service X { rpc Y (...) }in.protofiles — everyrpcis a route; check per-method auth interceptors and validate request fields like any handler. Same for message-queue consumers (Kafka/Rabbit/SQS handlers), scheduled job entry points, and Netty/WebSocket frame handlers — add them all to the census table
rg -n "rpc \w+\(" -g '*.proto'; rg -n "@GrpcClient|StreamObserver" -g '*.java' -g '*.kt'
rg -n "@(KafkaListener|RabbitListener)|consumer\.|@Scheduled|@CloudFunction|functions\." -g '*.java' -g '*.kt' -g '*.py' -g '*.js' | head
2 — Authentication checks
Password storage
rg -n -i "bcrypt|argon2|scrypt|pbkdf2|md5|sha1|sha256.*password|hashpw|crypt\("
bcrypt/argon2id/pbkdf2with sane params (bcrypt cost ≥ 10, argon2id defaults or better) → OK- MD5/SHA1/SHA-256 for passwords → CRITICAL (unsalted fast hash = instant crack)
- Plaintext or reversible encryption → CRITICAL
Login mechanics
- User enumeration: login and password-reset responses differ for unknown vs wrong-password (timing or body) → MEDIUM
- No rate limiting / lockout on login, reset, and MFA endpoints → HIGH (note where it would live: middleware, API gateway, app code)
- Password reset: predictable token (timestamp, userid,
random.random()), token reused or non-expiring, reset response leaks the token → HIGH/CRITICAL - MFA present? Missing MFA on admin accounts → HIGH if roles exist
LDAP authentication (enterprise directories)
rg -n "SECURITY_AUTHENTICATION|ANONYMOUS" -g '*.py' -g '*.java'
rg -n 'uid="\s*\+\s*\w+|SECURITY_PRINCIPAL.*\+' -g '*.py' -g '*.java'
rg -n '\(sAMAccountName=|\(uid=' -g '*.py' -g '*.java'
rg -n "escape_dn_chars|escape_filter_chars|LdapEncoder" -g '*.py' -g '*.java'
- Anonymous bind offered as an auth path (
authentication=ANONYMOUS,SECURITY_AUTHENTICATIONset tonone) → CRITICAL — everyone "authenticates"; binding ≠ verifying the intended identity - DN injection (CWE-90): bind DN built by concatenation —
"uid=" + user + ",ou=people,..."(Python) orContext.SECURITY_PRINCIPALconcatenation (Java) → CRITICAL: a uid containing,...or+rebinds as a different principal - Filter injection: search filters composed by f-string/format with user terms (
f"(sAMAccountName={name})") → HIGH —*and)(characters forge wildcard/boolean filters (auth bypass + data mining) - Safe shape: escape before composing (
escape_dn_chars()/escape_filter_chars()in ldap3,LdapEncoder.filterEncode/nameEscapein Java) — or constant service DN + post-bind attribute comparison - The DN/filter greps hit BOTH vulnerable and escaped forms — the escape-call context is the triage discriminator (same census discipline as XSS sinks)
Profile changes → account takeover
Email/password-change endpoints are AUTH endpoints — a session alone is not sufficient proof (stolen session, XSS-riding, insider):
rg -n "email\s*=|updateEmail|changeEmail|current_password|currentPassword|change.?password" src/ app/ | head -20
- Email change without current-password confirmation AND without re-verification of the NEW address (verify-before-swap + notice to the old one) → High — a hijacked session silently owns the account: future password resets go to the attacker's address, no signal ever reaches the victim
- Password change without current password → same class, High
- Username/handle change that re-frees an old identifier while reset-by-username flows exist → Medium
- Safe shape:
requireCurrentPassword(or step-up re-auth) + verification mail to the new address before activation + revocation notice to the old one
JWT / tokens
rg -n "jwt\.(sign|decode|verify)|verify\(|algorithms|algorithm|none|HS256|RS256"
algorithm: "none"accepted orjwt.decode(token)without algorithm pinning → CRITICAL- Algorithm confusion (RS256 → HS256):
jwt.verify(token, PUBLIC_KEY, {algorithms: ['HS256', ...]})— attacker signs with the public key as HMAC secret → CRITICAL. Pin asymmetric algorithms only when the key is public; never pass a public key where the library accepts it for HMAC.
rg -n "algorithms.*HS256|verify\(.*public|createPublicKey|jwt\.RSAPublicKey"
- JWKS
kidinjection: token-controlledkid/alg/jku/x5uheader flowing into paths, queries, or fetches ('./keys/' + header.kid,jwks_url + kid) → path traversal / attacker-controlled key material → Critical. Trustkidfor lookup only against a fixed key set.
rg -n "header\.kid|\bkid\b.*\+|jku|x5u"
- PKCE: public clients (SPA/mobile/desktop — anything that can't hold a secret) using auth-code flow without
code_challenge/code_verifier→ code interception, HIGH. Server-side confidential clients may omit it (note, not a finding). - Symmetric signing with weak/default secret (
secret,jwt-secret,changeme) → CRITICAL - No
expvalidation, or noaud/isschecks where multiple services share keys → HIGH - Tokens in localStorage with XSS present elsewhere → MEDIUM (compounding)
- Session tokens: not rotated on login/privilege change → MEDIUM; long-lived non-revocable tokens → HIGH
3 — Authorization checks (IDOR & friends)
Find object access points, then check each for an ownership/scope test:
rg -n "req\.params\.id|findById\(|get\(|\.filter\(.*userId|user\.id|currentUser|req\.user"
Model.findById(req.params.id)returned directly with nowhere userId/ post-fetch ownership check → IDOR, HIGH (CRITICAL for sensitive objects: invoices, messages, PII)- Association (chained) IDOR — downstream handlers fetch by a related id (
?order_id=,payment_id) without verifying ownership THROUGH the join: each endpoint guards its own object, the chain leaks. Systematic method in../flow-security/SKILL.md(class F3) — audit flows, not just routes | - Admin routes: how are they guarded? Missing middleware → CRITICAL. Note function-level checks too (regular user hitting
/admin/*handlers). - Horizontal vs vertical: test reasoning for both — same-role users touching each other's data, and low-role → admin.
- Trusting client-side hints:
is_adminfrom request body/cookie-in-JWT-without-verify, hidden-but-served admin UI → HIGH - authz on derived/divergent state (ToB, Provenance 2026): when a permission decision reads a DERIVED value (cached role, denormalized count/balance, materialized flag) instead of the live source-of-truth record, two failure modes: (a) stale/replicated state authorizes what live state would deny, and (b) missing/zero/default derived state must FAIL CLOSED — a check like
if derived_supply > 0: grant admingrants when the derived value is empty. Trace every authz read to its source-of-truth write; verify the default branch denies.
Mass assignment (generic)
The framework skills cover Laravel/Django/Rails fillable semantics; the generic version bites every ORM:
rg -n "\.create\(\s*req\.body|\.update\(\s*req\.body|Object\.assign\(\s*\w+\s*,\s*req\.body|new \w+\(\s*req\.body|findOneAndUpdate\(\s*\w+,\s*req\.body|bulkCreate\(\s*req\.body"
Request body spread into create/update without an allowlist → attacker sets role, isAdmin, userId, credits, price, email (account takeover via email change). Report High — Critical when the model has role/permission fields. Safe: explicit field destructuring/pick, schema-level allowlist ($fillable, fields), validators configured to strip unknown keys (default validate/zod .strict() — not passthrough). Also check filter/map "sanitizers" that only check types: an allowLIST of keys, not of types, is what counts.
Trusted-header authorization (identity spoofing)
rg -n -i "req\.headers\[\s*['\"]x-|request\.headers\.get\(|getHeader\(\s*\"X-|headers\[\s*'X-|x-forwarded-for|x-forwarded-user|x-real-ip|HTTP_X_"
Using X-User-Id / X-Email / X-Admin / X-Forwarded-For as the identity or authz input means anyone who can reach the service directly is any user → CRITICAL. Valid only when BOTH hold: (1) an edge proxy/gateway strips these headers from inbound traffic, and (2) the app is not directly reachable (check NodePort/LoadBalancer services, ingress annotations, port exposure in compose). Microservice meshes are the classic miss: service B trusts X-User-Id "because only service A calls us" — but anything on the cluster network can. Headers used only for logging (X-Request-Id) are fine — trace the value into an authz decision before flagging.
SSO/OIDC callback host verification (CWE-943). Rocket.Chat GHSL-2026-004/005: an account/SSO service accepted callbacks whose host did not match the configured/allowed one → authentication bypass. Wherever an OIDC/OAuth/SAML/OmniAuth callback or JWKS/discovery URL is resolved from configuration, verify the code checks the exact host/origin (allowlist, not substring/endsWith) and rejects mismatches before trusting the identity assertion → Critical (direct auth bypass).
Authorization predicate on a request-derived principal (sibling of header spoofing — the write side). The capability/role check itself is evaluated against a user object CONSTRUCTED FROM REQUEST DATA instead of the authenticated session principal: user_can(get_user_by('id', $_COOKIE['original_user_id'])), findById(req.cookies.user_id).can(...), hasRole(req.body.role). The check "passes" for whoever the REQUEST names — forging the cookie/param IS the privilege. DevKit Pro CVE-2026-14378 shape: user-switching revert checked manage_options on the cookie-named user instead of current_user_can() → admin takeover. → Critical. The principal for any authz decision must come from the session/token verification path; request data may name a TARGET, never the ACTOR.
rg -n "findById\(\s*req\.(cookies|body|query)|get_user_by\([^)]*\$_(COOKIE|GET|POST|REQUEST)|user_can\s*\(\s*\$" src/ app/ -g '*.js' -g '*.php'
Multi-tenant scoping census (SaaS)
Per-object IDOR checks miss the systematic version: in a multi-tenant app, EVERY data access must carry the tenant filter — one query without it leaks a whole tenant's data to another tenant's users (often via list/export/report endpoints that feel "shared").
rg -n "tenant|org_?id|account_?id|customer_?id|workspace" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.rb' -g '*.php' # tenant model census — size it (wc -l), then read
rg -n "\.(find|where|filter|all|select|query|get)(All)?\(" -g '*.js' -g '*.py' | rg -v "tenant|org_|account_|customer_|user" # unscoped reads — census: disposition EVERY line
Method: list every model access, mark each SCOPED (tenant filter present) / UNSCOPED / GLOBAL-BY-DESIGN (shared catalog). Every UNSCOPED access to tenant-owned data → Critical. Stronger defenses to note when present: DB-level row-level security (RLS), ORM global scopes (Laravel global scope, Django manager), middleware-injected tenant context that queries MUST use. Also check: tenant taken from request body/header instead of session (req.body.tenantId — attacker-controlled scope switch → cross-tenant, Critical), and cache keys missing the tenant prefix (cross-tenant cache bleed).
- Target-tenant vs caller-tenant on credential-issuing endpoints: when
a request names a TARGET (
user_id,email,login_name) and the handler issues enrollment codes / invitations / reset URLs / passkey or passwordless registrations for that target, checking only the CALLER's request tenant (org header, subdomain) while never comparing the TARGET's tenant = cross-tenant takeover: an admin (or any user-write holder) in org A obtains enrollment credentials for org-B users and registers their own authenticator (class incident: ZITADEL CVE-2026-105209 — passkey enrollment codes authorized againstx-zitadel-orgidwhile the target user's organization went unchecked, cross-org ATO, CVSS 9.6). The authz must compare the TARGET's tenant with the caller's tenant (or verify the caller's scope covers the target's tenant) — validating the request's own tenant header proves nothing about the target. Strongest shape: scope the target lookup itself by the caller's session tenant (findOne({ id, org_id: session.org })) rather than fetch-then-check:
rg -n -i "enroll(ment)?.?code|invite.?(code|user)|passwordless" src/ app/ -g '*.js' -g '*.ts' -g '*.py' -g '*.java' | head
Check-then-act races (TOCTOU)
Look for state checks followed by a separate write:
rg -n "if\s*\(.*\b(used|redeemed|approved|active|enabled|stock|balance|remaining)\b|\.findOne\(.*\)\.then|SELECT.*\b(balance|stock|used)\b.*FROM|\.save\(\)"
- Single-use coupons/tokens/reset links:
if (t.used) reject; ... t.used = true→ parallel replay wins every time → High (Critical for payments/refunds/webhooks). Fix: atomic conditional write —findOneAndUpdate({code, used:false}, {$set:{used:true}}),UPDATE ... SET used=1 WHERE code=? AND used=0checking rows affected. - Balance/stock read-modify-write:
user.balance -= amt; user.save()→ double-spend under concurrency. Fix:UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?. - Webhooks/event handlers without idempotency keys or event-id dedup → replayable. Report as one grouped finding with every affected flow listed.
Feature-flag & plan-capability gating
Flags constrain CAPABILITY (what a plan/tenant/role may do), not identity — an authenticated admin can still be the attacker the plan must stop. UI/menu gating is cosmetic; the handler/route is the enforcement point. Incident class (2026-09-23): a trial tenant blasted 1200+ emails through a compose route whose flag was enforced only in the menu and the queued job.
rg -n "Feature::(define|active)|isFeatureEnabled|featureFlags|feature_flags|isEnabled\(|isEnabledFor|\.variation\(" src/ app/ modules/
rg -n "pennant:purge|flushCache|flag.*purge" deploy/ scripts/ .github/
Stack-agnostic census (framework skills carry the concrete greps — e.g. ../laravel-security/SKILL.md Step 8):
- For every flag whose name implies a dangerous capability (import, email, send, sms, export, invite, admin, billing): WHERE is it enforced? Checks only in UI/menu code or only inside async jobs = gated-in-the-wrong-layer → High; Critical when the ungated surface fans out outbound messages (F9,
../flow-security/SKILL.md) - Default-true definitions for dangerous capabilities (
'import': true,"send_email": truein flag config) → High — the capability exists for everyone including trials; per-plan closures/lookups are the safe shape - Flag-store staleness: persisted flag stores (Pennant database, DB-backed Unleash/OpenFeature, a
featurestable) keep stored per-scope values when defaults change — reverting a default changes nothing for already-stored scopes. Deploy scripts must purge/sync; absence → Medium - Public endpoints that TRIGGER OUTBOUND messages (send-verification, subscribe, reset, notify, magic-link, webhook-register): census them with §1's route table; each needs auth-or-signed + throttle (+ captcha where public) → Critical when public AND unthrottled: ID enumeration = mail/SMS bomb needing zero auth and zero flags
One-time codes (verification / login / MFA codes)
A one-time code IS a bearer credential — census every endpoint that PRODUCES or VERIFIES one, then check WHERE the code goes and WHAT ELSE the verifier accepts:
rg -n -i "verification.?code|login.?code|one[-_ ]?time.?code|\botp\b|magic.?code|sms.?code|email.?code" src/ app/ routes/ | head -40
rg -n -i "(master|static|backdoor|debug|test).{0,12}code" src/ app/ config/ db/ seeds/ -g '*.sql' -g '*.java' -g '*.py' -g '*.js' -g '*.ts' -g '*.yml'
- Producer endpoint whose HTTP RESPONSE carries the generated code (
res.json({ ..., code }),put("code", ...), response wrapper, template rendering it) → Critical — unauthenticated takeover of any known identifier: request the code, read it from the response, log in (class incident: CVE-2026-97063, X-SpringBoot — GET /sys/mobile/code and /sys/email/code returned codes; login via /sys/emailOrMobileLogin) - Static/master code the verifier accepts (seed rows, config constants, "test-mode" fallbacks — X-SpringBoot's
172839seed, CVE-2026-97064; entry:../cve-research/vuln-db/entries/2026-09-25-cve-2026-97064.md) → Critical — a public backdoor credential; if it ever shipped, assume it IS used - Code delivered to an address/number taken from the REQUEST instead of the stored account record → High — attacker-controlled delivery channel harvests codes
- Weak hygiene: reusable codes, no expiry, no attempt cap on verify, predictable generators (
Math.random) → Medium→High by brute-force feasibility - Safe shape: CSPRNG code, sent ONLY to the stored channel, response =
{sent: true}(existence-neutral), single-use + short TTL + verify throttle + constant-time compare, NO static fallback in any environment — dev seeds get cloned to prod
Webhook receiver authentication
Webhook endpoints are authenticated by the SENDER's signature, not by your sessions — an unverified receiver is an unauthenticated endpoint with money/entitlement powers. Census every webhook/callback route with §1's table, then:
rg -n "webhook|callback" src/ app/ routes/ | rg -i "post|route" | head -20
rg -n "timingSafeEqual|compare_digest|verify_sig|verifySignature|signature" src/ app/
rg -n "!process\.env\.[A-Z_]*SECRET|WEBHOOK_SECRET" src/ app/ .env.example 2>/dev/null
- Handler parses
req.body/event and ACTS (fulfill, enroll, activate, refund) without verifying the sender's HMAC signature → Critical (forged events = free entitlements; pairs with F4 replay in../flow-security/SKILL.md) - Signature compared with
==/===instead of constant-time (timingSafeEqual,hmac.compare_digest) → Medium (timing oracle) - Signature computed over re-parsed JSON instead of the RAW request bytes → verification fails → commonly "fixed" by deleting the check (drift toward Critical)
- Verification gated on the secret being SET (
if (!WEBHOOK_SECRET) next()/ empty-secret guard that skips the check) → fail-open on missing config → Critical — the Dockhand CVE-2026-53988 shape: null webhook secret guard → unauthenticated stack redeploy, and the redeploy pulls an attacker-controlled docker-compose.yml (privileged bind mounts) = container escape. A missing/empty secret must REJECT (fail closed), never skip verification. - No replay window (timestamp/event-id dedup) → High
- Callback that ESTABLISHES a session (not merely acts): the handler authenticates the CALLER from the payload —
wp_set_auth_cookie()/wp_set_current_user()on a request-supplied user id,req.login(user)whereusercame from a GET/POST param (base64 blob, JSON field). There is no signature to fall back on: identity itself arrives from the attacker, so a forged callback = logged in as anyone → Critical. Divi Membership CVE-2026-19660 shape:process_paypal_callbackoninit, base64paypal_paramGET param, no IPN/signature/ownership/nonce →wp_set_auth_cookie(). Census every session-establishment call and trace where its user comes from:
rg -n "wp_set_auth_cookie\s*\(|wp_set_current_user\s*\(|req\.login\s*\(|\.logIn\s*\(" src/ app/ -g '*.js' -g '*.ts' -g '*.php'
- Safe shape: the callback verifies the sender FIRST and resolves the member/user from the VERIFIED event's stored record (order → user), never from a request parameter; raw-body HMAC + constant-time compare + event-id dedup, all before any side effect
API keys as authentication (service-to-service)
Machine auth has its own failure modes — check every API-key/gateway-token scheme:
rg -n -i "x-api-key|api[_-]?key" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.rb' | head
- Key in URL/query (
?api_key=) — hits logs, referrers, browser history → HIGH (same class as tokens in URLs) - Unscoped keys — one god-key instead of per-service/per-permission keys: lateral movement on any leak → MEDIUM/HIGH
- No rotation story — no expiry, no revocation endpoint, keys older than the repo's history → MEDIUM (operational)
- Constant-time comparison absent (
==on the key) → timing oracle → LOW/MEDIUM (pairs withcrypto-reviewcomparison rules) - Weak key generation —
uuid(),random.random(), timestamp-derived → predictable keys → HIGH (see crypto-review randomness table) - Key = password reuse — same key validates AND encrypts/signs → separation of duties violation → MEDIUM
- Non-secret identifiers as credentials — authenticating a device/user by a value that is public or enumerable (hostname, hardware serial, username, email, machine-id) is authentication theater: anyone who knows the identifier IS the identity (class incident: Fleet CVE-2026-103264 — device API accepted hostnames and hardware serials as tokens alongside UUIDs; unauthenticated actors became any iOS/iPadOS host, MDM migration included). Identifiers are for LOOKUP, never for proof — enroll with a one-time secret or client cert, then authenticate on that:
rg -n -i "findby(hostname|serial|hardware)|by_(hostname|serial)|token\s*={2,3}[^=]*(hostname|serial|machine.?id)" -g '*.js' -g '*.ts' -g '*.py' -g '*.go' -g '*.java'
- Internal keys honored as master auth on every route (UTMStack CVE-2026-82042 class:
InternalApiKeyFilteraccepted theUtm-Internal-Keyheader ==INTERNAL_KEYenv var for ANY endpoint — no path restriction, no constant-time compare, no rate limit, no audit logging — so anyone who obtains the infra key gets full admin API: account creation, user management, rule changes): an internal/service perimeter key that the PUBLIC surface also accepts is a god-key with extra steps — the perimeter that justified the key does not exist at the app. The key must (1) be honored only on internal routes/hosts, (2) compare constant-time, (3) grant a SCOPED service identity (never admin), and (4) be obtainable only by internal callers — if any less-privileged endpoint reveals it, the scheme is Critical regardless of the rest:
rg -n -i "internal[_-]?key|x-internal|internal[_-]?api[_-]?key" src/ app/ -g '*.js' -g '*.ts' -g '*.py' -g '*.java'
4 — Session & CSRF
- Cookie flags on session/auth cookies:
HttpOnly(missing + XSS present → HIGH),Secure(missing → HIGH if prod is HTTPS),SameSite(missing → MEDIUM) - CSRF: state-changing endpoints protected by token/
SameSite=Strict/Origincheck? Both auth-cookie AND no CSRF defense → HIGH - Logout invalidates server-side session? Not just client cookie clear → MEDIUM
- Session fixation: session ID regenerated after login → check
5 — OAuth/OIDC, if present
redirect_urivalidated exactly (not prefix/suffix match) → open redirect_uri = account takeover, CRITICALstateparameter present and verified → missing = CSRF/login CSRF, MEDIUM/HIGH- Token in URL fragment vs query (query leaks via logs/referrers) → MEDIUM
id_tokenvalidated: signature,nonce,aud→ skipping any = CRITICAL- Account linking (external IdP identity → local account) without caller
verification: census every surface that BINDS an external-IdP identity
to a local account (
AddIDPLink,/auth/link,link_account, social connect). A linking call that acts on a caller-SUPPLIED login name / user identifier — without an authenticated session plus a fresh primary factor or proof of control of BOTH identities — lets anyone who knows a victim's login name attach their OWN IdP identity and then sign in as the victim (class incident: ZITADEL CVE-2026-105207 — User Service V2 AddIDPLink + identify-only Login V2 sessions, unauthenticated ATO, CVSS 9.8). → Critical. Safe shape: link only to the session principal, require step-up re-auth, and take the external identity from the IdP's verified assertion (never from request fields):
rg -n -i "add_?idp|idp.?link|link.?(account|identity)|connect.?(account|identity)" src/ app/ -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.php' | head -20
Host-header poisoning in outbound auth/email flows
Password-reset/verification links and OAuth redirect URIs built from Host / X-Forwarded-Host (request-derived) let an attacker poison the victim's link domain (CodeQL class; Django's ALLOWED_HOSTS=['*'] is the classic enabler — see ../django-security/SKILL.md Step 1). Grep link/URL construction in mailers and auth controllers for request-header inputs → HIGH (account takeover via poisoned reset links).
6 — SAML (if present)
rg -n -i "saml|assertion|NameID|acs[ _-]?url|AudienceRecipient|samlp" -g '*.py' -g '*.java' -g '*.rb' -g '*.php' -g '*.js' -g '*.xml' | head -15
The four classic SAML bugs, in observed frequency order:
- Signature wrapping (SWA/XSW): the response's Signature element covers one assertion but the app reads a DIFFERENT one (attacker-injected). Defense: validate the signature over the exact node you consume (
response.get_assertion().validate_signature? no — one_assertion_only + signature-verified read). Grep forone_assertion_only/want_assertions_signedsettings — absent → HIGH/Critical. - Comment injection in NameID:
admin@corp.com<!--atk-->@evil.comparses as admin@corp.com to the IdP-signed value but the SP extracts differently — mitigated by library versions andNameIDformat checks; flag any custom NameID string handling → HIGH. - Unvalidated recipients:
Destination/AudienceRestriction/ACS URL not enforced (or ACS taken from the assertion!) → assertion replay across SPs. Config keys:expected_audience,acs_urlpinned server-side → absent → Critical. - Loose validation knobs:
want_response_signed=False+want_assertions_signed=False, clock skew ±24h (not_before/not_on_or_afterneutered),allow_unsolicitedin prod → each HIGH.
Safe shapes: pinned ACS + audience, both signed flags true, one-assertion-only, tight skew (≤5m), library current (python3-saml / OneLogin / passport-saml families each had historic CVEs — version-check via vuln-db/OSV).
7 — Realtime channels (WebSocket / SSE)
rg -n "io\.on\(|socket\.on\(|new WebSocket|WebSocketServer|@MessageMapping|@SubscribeMapping|STOMP|EventSource|ActionCable|cable\.|channel\.subscribe"
- Handshake auth: no
io.use(authMiddleware)/ token check on connect → CRITICAL (any client connects as anyone) - Subscription authz:
socket.join('room:' + id)/ channel subscribe without ownership check → IDOR over sockets, High — same rule as REST IDOR, different transport - Message handlers are routes: every
socket.on('cmd', ...)doing a state change needs authz; grep the handler bodies exactly like controllers - Command-forwarding handlers are RCE surfaces (UTMStack CVE-2026-82041 class: STOMP
/command/{hostname}destination →processCommand()forwarded arbitrary OS commands over gRPC to agents, no role check, no allowlist — any authenticated user, any role, root/SYSTEM on monitored hosts): a realtime channel whose messages become COMMANDS executed on backends/agents must enforce (1) role check per handler — authentication ≠ authorization, "any logged-in user" is not a policy — and (2) a server-side command allowlist (verb + target scope) before anything reaches the executor. Generic shape: message destination →agent.send({cmd}),grpc.invoke,exec(/spawn(inside the handler → Critical (remote command execution on downstream hosts):
rg -n "socket\.on\(\s*['\"][^'\"]*(command|cmd|exec|run)|@MessageMapping\(\s*['\"][^'\"]*(command|cmd|exec)" src/ app/ -g '*.js' -g '*.ts' -g '*.java'
rg -n "(agent|host)\.(send|execute|invoke)\(" src/ app/ | head
io.origins('*')/ missing origin check on a credentialed socket → Medium (cross-site WebSocket hijacking)- SSE (
EventSourceendpoints) are plain GETs — same authz as any other route
Reporting
For each finding: severity, file:line, one-line exploit story ("authenticated user A reads user B's invoice by changing ?id="), and the concrete fix (ownership scope in query, middleware, flag). Group IDORs by endpoint pattern — they usually cluster.
04 同仓库更多 Skills
recipe-performance
Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold/warm/background-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout/paint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.
mexc-spot-websocket
MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.
mexc-spot-rest
MEXC Spot REST workflows for public data, signed account requests, orders, wallet/admin actions, and secure signing without exposing API keys.
mexc-futures-websocket
MEXC Futures WebSocket workflows for public streams, private account/order/position streams, ping/pong, subscriptions, and depth maintenance.
0xklen-version-and-refresh-sanctions-lists
暂无简介
verify-tests-cover-changed-lines
Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.