auth-review
Skill2 starsChanged 50 days agoMITView on GitHub
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.
- Reads credentials
2 other projects use a version of this.
Adopted by
Most recently taken up by
- Jinbae10/skillstheir own version, first seen Jul 7, 2026
- descope/skillstheir own version, first seen Apr 24, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- descope/skills★ 18same name
- Jinbae10/skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository23 other files it gives its agents.
- android-code-securitySkill
- config-hardeningSkill
- container-iac-securitySkill
- course-platform-securitySkill
- crypto-reviewSkill
- cve-researchSkill
- data-exposureSkill
- dependency-vulnsSkill
- design-threat-reviewSkill
- django-securitySkill
- dotnet-securitySkill
- flow-securitySkill
- flutter-securitySkill
- graphql-securitySkill
- injection-flawsSkill
- laravel-securitySkill
- llm-securitySkill
- mobile-securitySkill
- rails-securitySkill
- secrets-detectionSkill
- security-auditSkill
- skill-forgeSkill
- spring-securitySkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 3 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 73
No rate limiting / lockout on login, reset, and MFA endpoints → HIGH (note where it would live: middleware, API gateway, app code)
- Very specific: line 88
Filter injection: search filters composed by f-string/format with user terms (f"(sAMAccountName={name})") → HIGH — * and )( characters …
- Distinctive: line 187
Look for state checks followed by a separate write:
- Very specific: line 207
Default-true definitions for dangerous capabilities ('import': true, "send_email": true in flag config) → High — the capability exists for …
- Distinctive: line 240
No replay window (timestamp/event-id dedup) → High
- Very specific: line 322
Message handlers are routes: every socket.on('cmd', ...) doing a state change needs authz.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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)" # FastAPIBuild 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' | head2 — 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 lineMethod: 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' | headCheck-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 -20Host-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 -15The 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/ | headio.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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

