[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GPECM9V7H4A406715REEXB":3,"skill-same:01M6GPECM9V7H4A406715REEXB":85},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":16,"sourceHtmlUrl":17,"categories":18,"keywords":19,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":6,"ownerHtmlUrl":53,"ownerBlogUrl":54,"ownerDescription":55,"updatedAt":56,"install":57,"resources":70},"01M6GPECM9V7H4A406715REEXB","auth-review",null,"Reviews authentication and authorization in any codebase — login flows, password storage, session\u002Fcookie handling, JWT pitfalls, OAuth\u002FOIDC misconfigurations, IDOR\u002Fbroken access control, privilege escalation, CSRF protection, route census for unguarded endpoints, trusted-header identity spoofing, check-then-act race conditions (TOCTOU), and WebSocket\u002FSSE 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.","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",81,"MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fashrafiucse-auth-review\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fashrafiucse-auth-review\u002FSKILL.md",[],[20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"auth","auth review","review","log","when","log in","login","csrf protection","pitfalls","reviews","privilege escalation","flows","act","route","broken","access","how","csrf","toctou","permissions","authentication","code","storage","2026-11-10 10:12:30",false,"---\nname: auth-review\ndescription: Reviews authentication and authorization in any codebase — login flows, password storage, session\u002Fcookie handling, JWT pitfalls, OAuth\u002FOIDC misconfigurations, IDOR\u002Fbroken access control, privilege escalation, CSRF protection, route census for unguarded endpoints, trusted-header identity spoofing, check-then-act race conditions (TOCTOU), and WebSocket\u002FSSE 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.\nlicense: MIT\n---\n\n# Authentication & Authorization Review\n\n## 1 — Map the auth architecture first\n\nFind: login\u002Fregister\u002Fpassword-reset endpoints, session middleware, role\u002Fpermission checks, token creation\u002Fvalidation, 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.\n\n### Route census (enumerate, don't guess)\n\nPattern greps find what they look for; a census finds what nobody remembered. Enumerate EVERY route, then check each one:\n\n```bash\nrg -n \"app\\.(get|post|put|patch|delete|all|use)\\(\"            # Express\u002FKoa\nrg -n \"@app\\.route\\(|@\\w+\\.route\\(|add_url_rule\"              # Flask\nrg -n \"path\\(|re_path\\(\" *\u002Furls.py                             # Django\nrg -n \"^\\s*(get|post|patch|put|delete) |resources \" config\u002Froutes.rb  # Rails\nrg -n \"@(Get|Post|Put|Patch|Delete|Request)Mapping\"             # Spring\nrg -n \"Route::(get|post|any|resource|apiResource)\" routes\u002F     # Laravel\nrg -n \"@(app|router)\\.(get|post|put|delete|patch)\"             # FastAPI\n```\n\nBuild a table `route → auth middleware → ownership check` and fill it by reading each handler. Every row with **neither** is a finding (Critical for admin\u002Fstate-changing, High otherwise). Census-only catches:\n- routes registered BEFORE the auth middleware (order bugs — `app.use(auth)` must precede every guarded route)\n- the one handler missing `@login_required` \u002F `authorize` \u002F `auth:sanctum` in an otherwise-guarded file\n- non-HTTP surfaces that are routes in disguise: queue consumers, cron\u002Fbackground job handlers, webhook receivers, GraphQL resolvers (see `..\u002Fgraphql-security\u002FSKILL.md`), event subscribers, RPC\u002Fmessage handlers — census them with the same table\n- dispatch-style apps (`?action=` → switch) — enumerate the switch arms, not the single URL\n- **case-sensitive middleware paths**: on case-insensitive hosts\u002Frouters, `app.use('\u002Fadmin', guard)` can be bypassed by `\u002FADMIN` or `\u002FAdmin\u002F` — check the router's case-sensitivity setting (Express: default sensitive only with regex; some frameworks\u002Ffilesystems are not) and whether any guarded prefix can be cased around → authz bypass\n- **encoding-sensitive middleware paths** (CWE-177): authz guards comparing the RAW, still-encoded request URI (`req.url` \u002F `req.originalUrl`, Java `getRequestURI()`, `RAW_URI`) while the router dispatches on the DECODED path — `GET \u002F%61dmin\u002Fusers` fails `startsWith('\u002Fadmin')` 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`, Java `getServletPath()`), or mount the guard on the route itself (`app.use('\u002Fadmin', guard)`), so percent-encoding can't split the check from the dispatch:\n```bash\nrg -n \"req\\.(url|originalUrl)\\.(startsWith|endsWith|includes|match)|getRequestURI\\(\\)\\.(startsWith|contains|equals)|RAW_URI\" -g '*.js' -g '*.ts' -g '*.java' -g '*.py'\n```\n- **suffix\u002Fcontains path matching in auth whitelists** (third variant of\n  the path-matching family: case → encoding → MATCHER STRICTNESS): a\n  public-path exemption checked with `path.endsWith('\u002Fx')` or\n  `path.contains('\u002Fx')` instead of an exact\u002Fanchored match frees EVERY\n  colliding route, not just the intended public one (class incident:\n  Kestra CVE-2026-49869, KEV — `AuthenticationFilter` used\n  `request.getPath().endsWith(\"\u002Fconfigs\")` to whitelist the public\n  configs endpoint; every namespace-scoped `...\u002Fconfigs` route bypassed\n  Basic Auth → unauthenticated API access → RCE). Safe shape: exact\n  match set (`new Set(['\u002Fconfigs']).has(path)`) or anchored regex\n  (`^\u002Fconfigs$`) — never suffix\u002Fsubstring tests on a path:\n```bash\nrg -n \"endsWith\\(\\s*['\\\"]\\\u002F|\\.contains\\(\\s*['\\\"]\\\u002F\" src\u002F -g '*.java' -g '*.js' -g '*.ts' -g '*.py' | head\n```\n- **setup\u002Finstall\u002Ffirst-run wizard routes**: `app.post('\u002Fsetup\u002Frestore')`, `\u002Finstall`, `\u002Fapi\u002Fsetup\u002F*` 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.restore` invoked 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:\n```bash\nrg -n -i \"['\\\"]\u002F(setup|install|installer)[^'\\\"]*['\\\"]\" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.rb' -g '*.php' | head -15\n```\n- **gRPC \u002F protobuf services**: `service X { rpc Y (...) }` in `.proto` files — every `rpc` is a route; check per-method auth interceptors and validate request fields like any handler. Same for **message-queue consumers** (Kafka\u002FRabbit\u002FSQS handlers), **scheduled job entry points**, and **Netty\u002FWebSocket frame handlers** — add them all to the census table\n```bash\nrg -n \"rpc \\w+\\(\" -g '*.proto'; rg -n \"@GrpcClient|StreamObserver\" -g '*.java' -g '*.kt'\nrg -n \"@(KafkaListener|RabbitListener)|consumer\\.|@Scheduled|@CloudFunction|functions\\.\" -g '*.java' -g '*.kt' -g '*.py' -g '*.js' | head\n```\n\n## 2 — Authentication checks\n\n### Password storage\n```bash\nrg -n -i \"bcrypt|argon2|scrypt|pbkdf2|md5|sha1|sha256.*password|hashpw|crypt\\(\"\n```\n- `bcrypt`\u002F`argon2id`\u002F`pbkdf2` with sane params (bcrypt cost ≥ 10, argon2id defaults or better) → OK\n- MD5\u002FSHA1\u002FSHA-256 for passwords → **CRITICAL** (unsalted fast hash = instant crack)\n- Plaintext or reversible encryption → **CRITICAL**\n\n### Login mechanics\n- User enumeration: login and password-reset responses differ for unknown vs wrong-password (timing or body) → MEDIUM\n- No rate limiting \u002F lockout on login, reset, and MFA endpoints → HIGH (note where it would live: middleware, API gateway, app code)\n- Password reset: predictable token (timestamp, userid, `random.random()`), token reused or non-expiring, reset response leaks the token → HIGH\u002FCRITICAL\n- MFA present? Missing MFA on admin accounts → HIGH if roles exist\n\n### LDAP authentication (enterprise directories)\n\n```bash\nrg -n \"SECURITY_AUTHENTICATION|ANONYMOUS\" -g '*.py' -g '*.java'\nrg -n 'uid=\"\\s*\\+\\s*\\w+|SECURITY_PRINCIPAL.*\\+' -g '*.py' -g '*.java'\nrg -n '\\(sAMAccountName=|\\(uid=' -g '*.py' -g '*.java'\nrg -n \"escape_dn_chars|escape_filter_chars|LdapEncoder\" -g '*.py' -g '*.java'\n```\n\n- Anonymous bind offered as an auth path (`authentication=ANONYMOUS`, `SECURITY_AUTHENTICATION` set to `none`) → **CRITICAL** — everyone \"authenticates\"; binding ≠ verifying the intended identity\n- **DN injection** (CWE-90): bind DN built by concatenation — `\"uid=\" + user + \",ou=people,...\"` (Python) or `Context.SECURITY_PRINCIPAL` concatenation (Java) → **CRITICAL**: a uid containing `,...` or `+` rebinds as a different principal\n- **Filter injection**: search filters composed by f-string\u002Fformat with user terms (`f\"(sAMAccountName={name})\"`) → HIGH — `*` and `)(` characters forge wildcard\u002Fboolean filters (auth bypass + data mining)\n- Safe shape: escape before composing (`escape_dn_chars()` \u002F `escape_filter_chars()` in ldap3, `LdapEncoder.filterEncode\u002FnameEscape` in Java) — or constant service DN + post-bind attribute comparison\n- The DN\u002Ffilter greps hit BOTH vulnerable and escaped forms — the escape-call context is the triage discriminator (same census discipline as XSS sinks)\n\n### Profile changes → account takeover\n\nEmail\u002Fpassword-change endpoints are AUTH endpoints — a session alone is not sufficient proof (stolen session, XSS-riding, insider):\n\n```bash\nrg -n \"email\\s*=|updateEmail|changeEmail|current_password|currentPassword|change.?password\" src\u002F app\u002F | head -20\n```\n\n- 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\n- Password change without current password → same class, High\n- Username\u002Fhandle change that re-frees an old identifier while reset-by-username flows exist → Medium\n- Safe shape: `requireCurrentPassword` (or step-up re-auth) + verification mail to the new address before activation + revocation notice to the old one\n\n### JWT \u002F tokens\n```bash\nrg -n \"jwt\\.(sign|decode|verify)|verify\\(|algorithms|algorithm|none|HS256|RS256\"\n```\n- `algorithm: \"none\"` accepted or `jwt.decode(token)` without algorithm pinning → **CRITICAL**\n- **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.\n```bash\nrg -n \"algorithms.*HS256|verify\\(.*public|createPublicKey|jwt\\.RSAPublicKey\" \n```\n- **JWKS `kid` injection**: token-controlled `kid`\u002F`alg`\u002F`jku`\u002F`x5u` header flowing into paths, queries, or fetches (`'.\u002Fkeys\u002F' + header.kid`, `jwks_url + kid`) → path traversal \u002F attacker-controlled key material → **Critical**. Trust `kid` for lookup only against a fixed key set.\n```bash\nrg -n \"header\\.kid|\\bkid\\b.*\\+|jku|x5u\" \n```\n- **PKCE**: public clients (SPA\u002Fmobile\u002Fdesktop — anything that can't hold a secret) using auth-code flow without `code_challenge`\u002F`code_verifier` → code interception, HIGH. Server-side confidential clients may omit it (note, not a finding).\n- Symmetric signing with weak\u002Fdefault secret (`secret`, `jwt-secret`, `changeme`) → CRITICAL\n- No `exp` validation, or no `aud`\u002F`iss` checks where multiple services share keys → HIGH\n- Tokens in localStorage with XSS present elsewhere → MEDIUM (compounding)\n- Session tokens: not rotated on login\u002Fprivilege change → MEDIUM; long-lived non-revocable tokens → HIGH\n\n## 3 — Authorization checks (IDOR & friends)\n\nFind object access points, then check each for an ownership\u002Fscope test:\n```bash\nrg -n \"req\\.params\\.id|findById\\(|get\\(|\\.filter\\(.*userId|user\\.id|currentUser|req\\.user\"\n```\n- `Model.findById(req.params.id)` returned directly with no `where userId` \u002F post-fetch ownership check → **IDOR**, HIGH (CRITICAL for sensitive objects: invoices, messages, PII)\n- **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 `..\u002Fflow-security\u002FSKILL.md` (class F3) — audit flows, not just routes |\n- Admin routes: how are they guarded? Missing middleware → CRITICAL. Note *function-level* checks too (regular user hitting `\u002Fadmin\u002F*` handlers).\n- Horizontal vs vertical: test reasoning for both — same-role users touching each other's data, and low-role → admin.\n- Trusting client-side hints: `is_admin` from request body\u002Fcookie-in-JWT-without-verify, hidden-but-served admin UI → HIGH\n- **authz on derived\u002Fdivergent state** (ToB, Provenance 2026): when a permission decision reads a DERIVED value (cached role, denormalized count\u002Fbalance, materialized flag) instead of the live source-of-truth record, two failure modes: (a) stale\u002Freplicated state authorizes what live state would deny, and (b) missing\u002Fzero\u002Fdefault derived state must FAIL CLOSED — a check like `if derived_supply > 0: grant admin` grants when the derived value is empty. Trace every authz read to its source-of-truth write; verify the default branch denies.\n\n### Mass assignment (generic)\n\nThe framework skills cover Laravel\u002FDjango\u002FRails fillable semantics; the generic version bites every ORM:\n```bash\nrg -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\"\n```\nRequest body spread into create\u002Fupdate without an allowlist → attacker sets `role`, `isAdmin`, `userId`, `credits`, `price`, `email` (account takeover via email change). Report **High** — Critical when the model has role\u002Fpermission fields. Safe: explicit field destructuring\u002Fpick, schema-level allowlist (`$fillable`, `fields`), validators configured to strip unknown keys (default `validate`\u002Fzod `.strict()` — not passthrough). Also check filter\u002Fmap \"sanitizers\" that only check types: an allowLIST of keys, not of types, is what counts.\n\n### Trusted-header authorization (identity spoofing)\n```bash\nrg -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_\"\n```\nUsing `X-User-Id` \u002F `X-Email` \u002F `X-Admin` \u002F `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\u002Fgateway strips these headers from inbound traffic, and (2) the app is not directly reachable (check NodePort\u002FLoadBalancer 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.\n\n**SSO\u002FOIDC callback host verification (CWE-943).** Rocket.Chat GHSL-2026-004\u002F005: an account\u002FSSO service accepted callbacks whose host did not match the configured\u002Fallowed one → authentication bypass. Wherever an OIDC\u002FOAuth\u002FSAML\u002FOmniAuth callback or JWKS\u002Fdiscovery URL is resolved from configuration, verify the code checks the **exact** host\u002Forigin (allowlist, not substring\u002F`endsWith`) and rejects mismatches before trusting the identity assertion → Critical (direct auth bypass).\n\n**Authorization predicate on a request-derived principal** (sibling of header spoofing — the write side). The capability\u002Frole 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\u002Fparam 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\u002Ftoken verification path; request data may name a TARGET, never the ACTOR.\n```bash\nrg -n \"findById\\(\\s*req\\.(cookies|body|query)|get_user_by\\([^)]*\\$_(COOKIE|GET|POST|REQUEST)|user_can\\s*\\(\\s*\\$\" src\u002F app\u002F -g '*.js' -g '*.php'\n```\n\n### Multi-tenant scoping census (SaaS)\n\nPer-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\u002Fexport\u002Freport endpoints that feel \"shared\").\n```bash\nrg -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\nrg -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\n```\nMethod: list every model access, mark each SCOPED (tenant filter present) \u002F UNSCOPED \u002F 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\u002Fheader instead of session (`req.body.tenantId` — attacker-controlled scope switch → cross-tenant, Critical), and cache keys missing the tenant prefix (cross-tenant cache bleed).\n- **Target-tenant vs caller-tenant on credential-issuing endpoints**: when\n  a request names a TARGET (`user_id`, `email`, `login_name`) and the\n  handler issues enrollment codes \u002F invitations \u002F reset URLs \u002F passkey or\n  passwordless registrations for that target, checking only the CALLER's\n  request tenant (org header, subdomain) while never comparing the\n  TARGET's tenant = cross-tenant takeover: an admin (or any\n  user-write holder) in org A obtains enrollment credentials for org-B\n  users and registers their own authenticator (class incident: ZITADEL\n  CVE-2026-105209 — passkey enrollment codes authorized against\n  `x-zitadel-orgid` while the target user's organization went unchecked,\n  cross-org ATO, CVSS 9.6). The authz must compare the TARGET's tenant\n  with the caller's tenant (or verify the caller's scope covers the\n  target's tenant) — validating the request's own tenant header proves\n  nothing about the target. Strongest shape: scope the target lookup itself\n  by the caller's session tenant (`findOne({ id, org_id: session.org })`)\n  rather than fetch-then-check:\n```bash\nrg -n -i \"enroll(ment)?.?code|invite.?(code|user)|passwordless\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java' | head\n```\n### Check-then-act races (TOCTOU)\n\nLook for state checks followed by a **separate** write:\n```bash\nrg -n \"if\\s*\\(.*\\b(used|redeemed|approved|active|enabled|stock|balance|remaining)\\b|\\.findOne\\(.*\\)\\.then|SELECT.*\\b(balance|stock|used)\\b.*FROM|\\.save\\(\\)\"\n```\n- Single-use coupons\u002Ftokens\u002Freset links: `if (t.used) reject; ... t.used = true` → parallel replay wins every time → **High** (Critical for payments\u002Frefunds\u002Fwebhooks). Fix: atomic conditional write — `findOneAndUpdate({code, used:false}, {$set:{used:true}})`, `UPDATE ... SET used=1 WHERE code=? AND used=0` checking rows affected.\n- Balance\u002Fstock read-modify-write: `user.balance -= amt; user.save()` → double-spend under concurrency. Fix: `UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?`.\n- Webhooks\u002Fevent handlers without idempotency keys or event-id dedup → replayable. Report as one grouped finding with every affected flow listed.\n\n### Feature-flag & plan-capability gating\n\nFlags constrain CAPABILITY (what a plan\u002Ftenant\u002Frole may do), not identity — an authenticated admin can still be the attacker the plan must stop. UI\u002Fmenu gating is cosmetic; the handler\u002Froute 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.\n\n```bash\nrg -n \"Feature::(define|active)|isFeatureEnabled|featureFlags|feature_flags|isEnabled\\(|isEnabledFor|\\.variation\\(\" src\u002F app\u002F modules\u002F\nrg -n \"pennant:purge|flushCache|flag.*purge\" deploy\u002F scripts\u002F .github\u002F\n```\n\nStack-agnostic census (framework skills carry the concrete greps — e.g. `..\u002Flaravel-security\u002FSKILL.md` Step 8):\n\n- 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\u002Fmenu code or only inside async jobs = **gated-in-the-wrong-layer** → High; Critical when the ungated surface fans out outbound messages (F9, `..\u002Fflow-security\u002FSKILL.md`)\n- Default-true definitions for dangerous capabilities (`'import': true`, `\"send_email\": true` in flag config) → High — the capability exists for everyone including trials; per-plan closures\u002Flookups are the safe shape\n- **Flag-store staleness**: persisted flag stores (Pennant database, DB-backed Unleash\u002FOpenFeature, a `features` table) keep stored per-scope values when defaults change — reverting a default changes nothing for already-stored scopes. Deploy scripts must purge\u002Fsync; absence → Medium\n- 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\u002FSMS bomb needing zero auth and zero flags\n\n### One-time codes (verification \u002F login \u002F MFA codes)\n\nA 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:\n\n```bash\nrg -n -i \"verification.?code|login.?code|one[-_ ]?time.?code|\\botp\\b|magic.?code|sms.?code|email.?code\" src\u002F app\u002F routes\u002F | head -40\nrg -n -i \"(master|static|backdoor|debug|test).{0,12}code\" src\u002F app\u002F config\u002F db\u002F seeds\u002F -g '*.sql' -g '*.java' -g '*.py' -g '*.js' -g '*.ts' -g '*.yml'\n```\n\n- 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 \u002Fsys\u002Fmobile\u002Fcode and \u002Fsys\u002Femail\u002Fcode returned codes; login via \u002Fsys\u002FemailOrMobileLogin)\n- Static\u002Fmaster code the verifier accepts (seed rows, config constants, \"test-mode\" fallbacks — X-SpringBoot's `172839` seed, CVE-2026-97064; entry: `..\u002Fcve-research\u002Fvuln-db\u002Fentries\u002F2026-09-25-cve-2026-97064.md`) → **Critical** — a public backdoor credential; if it ever shipped, assume it IS used\n- Code delivered to an address\u002Fnumber taken from the REQUEST instead of the stored account record → High — attacker-controlled delivery channel harvests codes\n- Weak hygiene: reusable codes, no expiry, no attempt cap on verify, predictable generators (`Math.random`) → Medium→High by brute-force feasibility\n- 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\n\n### Webhook receiver authentication\n\nWebhook endpoints are authenticated by the SENDER's signature, not by your sessions — an unverified receiver is an unauthenticated endpoint with money\u002Fentitlement powers. Census every webhook\u002Fcallback route with §1's table, then:\n\n```bash\nrg -n \"webhook|callback\" src\u002F app\u002F routes\u002F | rg -i \"post|route\" | head -20\nrg -n \"timingSafeEqual|compare_digest|verify_sig|verifySignature|signature\" src\u002F app\u002F\nrg -n \"!process\\.env\\.[A-Z_]*SECRET|WEBHOOK_SECRET\" src\u002F app\u002F .env.example 2>\u002Fdev\u002Fnull\n```\n\n- Handler parses `req.body`\u002Fevent and ACTS (fulfill, enroll, activate, refund) without verifying the sender's HMAC signature → **Critical** (forged events = free entitlements; pairs with F4 replay in `..\u002Fflow-security\u002FSKILL.md`)\n- Signature compared with `==`\u002F`===` instead of constant-time (`timingSafeEqual`, `hmac.compare_digest`) → Medium (timing oracle)\n- Signature computed over re-parsed JSON instead of the RAW request bytes → verification fails → commonly \"fixed\" by deleting the check (drift toward Critical)\n- **Verification gated on the secret being SET** (`if (!WEBHOOK_SECRET) next()` \u002F 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\u002Fempty secret must REJECT (fail closed), never skip verification.\n- No replay window (timestamp\u002Fevent-id dedup) → High\n- **Callback that ESTABLISHES a session** (not merely acts): the handler authenticates the CALLER from the payload — `wp_set_auth_cookie()`\u002F`wp_set_current_user()` on a request-supplied user id, `req.login(user)` where `user` came from a GET\u002FPOST 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_callback` on `init`, base64 `paypal_param` GET param, no IPN\u002Fsignature\u002Fownership\u002Fnonce → `wp_set_auth_cookie()`. Census every session-establishment call and trace where its user comes from:\n```bash\nrg -n \"wp_set_auth_cookie\\s*\\(|wp_set_current_user\\s*\\(|req\\.login\\s*\\(|\\.logIn\\s*\\(\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.php'\n```\n- Safe shape: the callback verifies the sender FIRST and resolves the member\u002Fuser 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\n\n### API keys as authentication (service-to-service)\n\nMachine auth has its own failure modes — check every API-key\u002Fgateway-token scheme:\n```bash\nrg -n -i \"x-api-key|api[_-]?key\" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.rb' | head\n```\n- **Key in URL\u002Fquery** (`?api_key=`) — hits logs, referrers, browser history → HIGH (same class as tokens in URLs)\n- **Unscoped keys** — one god-key instead of per-service\u002Fper-permission keys: lateral movement on any leak → MEDIUM\u002FHIGH\n- **No rotation story** — no expiry, no revocation endpoint, keys older than the repo's history → MEDIUM (operational)\n- **Constant-time comparison absent** (`==` on the key) → timing oracle → LOW\u002FMEDIUM (pairs with `crypto-review` comparison rules)\n- **Weak key generation** — `uuid()`, `random.random()`, timestamp-derived → predictable keys → HIGH (see crypto-review randomness table)\n- **Key = password reuse** — same key validates AND encrypts\u002Fsigns → separation of duties violation → MEDIUM\n- **Non-secret identifiers as credentials** — authenticating a device\u002Fuser 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\u002FiPadOS host, MDM migration included). Identifiers are for LOOKUP, never for proof — enroll with a one-time secret or client cert, then authenticate on that:\n```bash\nrg -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'\n```\n- **Internal keys honored as master auth on every route** (UTMStack CVE-2026-82042 class: `InternalApiKeyFilter` accepted the `Utm-Internal-Key` header == `INTERNAL_KEY` env 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\u002Fservice 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\u002Fhosts, (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:\n```bash\nrg -n -i \"internal[_-]?key|x-internal|internal[_-]?api[_-]?key\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java'\n```\n\n## 4 — Session & CSRF\n\n- Cookie flags on session\u002Fauth cookies: `HttpOnly` (missing + XSS present → HIGH), `Secure` (missing → HIGH if prod is HTTPS), `SameSite` (missing → MEDIUM)\n- CSRF: state-changing endpoints protected by token\u002F`SameSite=Strict`\u002F`Origin` check? Both auth-cookie AND no CSRF defense → HIGH\n- Logout invalidates server-side session? Not just client cookie clear → MEDIUM\n- Session fixation: session ID regenerated after login → check\n\n## 5 — OAuth\u002FOIDC, if present\n\n- `redirect_uri` validated exactly (not prefix\u002Fsuffix match) → open redirect_uri = account takeover, CRITICAL\n- `state` parameter present and verified → missing = CSRF\u002Flogin CSRF, MEDIUM\u002FHIGH\n- Token in URL fragment vs query (query leaks via logs\u002Freferrers) → MEDIUM\n- `id_token` validated: signature, `nonce`, `aud` → skipping any = CRITICAL\n- **Account linking (external IdP identity → local account) without caller\n  verification**: census every surface that BINDS an external-IdP identity\n  to a local account (`AddIDPLink`, `\u002Fauth\u002Flink`, `link_account`, social\n  connect). A linking call that acts on a caller-SUPPLIED login name \u002F\n  user identifier — without an authenticated session plus a fresh primary\n  factor or proof of control of BOTH identities — lets anyone who knows a\n  victim's login name attach their OWN IdP identity and then sign in as the\n  victim (class incident: ZITADEL CVE-2026-105207 — User Service V2\n  AddIDPLink + identify-only Login V2 sessions, unauthenticated ATO,\n  CVSS 9.8). → Critical. Safe shape: link only to the session principal,\n  require step-up re-auth, and take the external identity from the IdP's\n  verified assertion (never from request fields):\n```bash\nrg -n -i \"add_?idp|idp.?link|link.?(account|identity)|connect.?(account|identity)\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.php' | head -20\n```\n\n### Host-header poisoning in outbound auth\u002Femail flows\n\nPassword-reset\u002Fverification links and OAuth redirect URIs built from `Host` \u002F `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 `..\u002Fdjango-security\u002FSKILL.md` Step 1). Grep link\u002FURL construction in mailers and auth controllers for request-header inputs → HIGH (account takeover via poisoned reset links).\n## 6 — SAML (if present)\n\n```bash\nrg -n -i \"saml|assertion|NameID|acs[ _-]?url|AudienceRecipient|samlp\" -g '*.py' -g '*.java' -g '*.rb' -g '*.php' -g '*.js' -g '*.xml' | head -15\n```\n\nThe four classic SAML bugs, in observed frequency order:\n- **Signature wrapping (SWA\u002FXSW)**: 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 for `one_assertion_only` \u002F `want_assertions_signed` settings — absent → HIGH\u002FCritical.\n- **Comment injection in NameID**: `admin@corp.com\u003C!--atk-->@evil.com` parses as admin@corp.com to the IdP-signed value but the SP extracts differently — mitigated by library versions and `NameID` format checks; flag any custom NameID string handling → HIGH.\n- **Unvalidated recipients**: `Destination`\u002F`AudienceRestriction`\u002FACS URL not enforced (or ACS taken from the assertion!) → assertion replay across SPs. Config keys: `expected_audience`, `acs_url` pinned server-side → absent → Critical.\n- **Loose validation knobs**: `want_response_signed=False` + `want_assertions_signed=False`, clock skew ±24h (`not_before`\u002F`not_on_or_after` neutered), `allow_unsolicited` in prod → each HIGH.\n\nSafe shapes: pinned ACS + audience, both signed flags true, one-assertion-only, tight skew (≤5m), library current (python3-saml \u002F OneLogin \u002F passport-saml families each had historic CVEs — version-check via vuln-db\u002FOSV).\n\n## 7 — Realtime channels (WebSocket \u002F SSE)\n\n```bash\nrg -n \"io\\.on\\(|socket\\.on\\(|new WebSocket|WebSocketServer|@MessageMapping|@SubscribeMapping|STOMP|EventSource|ActionCable|cable\\.|channel\\.subscribe\"\n```\n\n- **Handshake auth**: no `io.use(authMiddleware)` \u002F token check on connect → **CRITICAL** (any client connects as anyone)\n- **Subscription authz**: `socket.join('room:' + id)` \u002F channel subscribe without ownership check → IDOR over sockets, **High** — same rule as REST IDOR, different transport\n- **Message handlers are routes**: every `socket.on('cmd', ...)` doing a state change needs authz; grep the handler bodies exactly like controllers\n- **Command-forwarding handlers are RCE surfaces** (UTMStack CVE-2026-82041 class: STOMP `\u002Fcommand\u002F{hostname}` destination → `processCommand()` forwarded arbitrary OS commands over gRPC to agents, no role check, no allowlist — any authenticated user, any role, root\u002FSYSTEM on monitored hosts): a realtime channel whose messages become COMMANDS executed on backends\u002Fagents 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(`\u002F`spawn(` inside the handler → **Critical** (remote command execution on downstream hosts):\n```bash\nrg -n \"socket\\.on\\(\\s*['\\\"][^'\\\"]*(command|cmd|exec|run)|@MessageMapping\\(\\s*['\\\"][^'\\\"]*(command|cmd|exec)\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.java'\nrg -n \"(agent|host)\\.(send|execute|invoke)\\(\" src\u002F app\u002F | head\n```\n- `io.origins('*')` \u002F missing origin check on a credentialed socket → Medium (cross-site WebSocket hijacking)\n- SSE (`EventSource` endpoints) are plain GETs — same authz as any other route\n\n## Reporting\n\nFor 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.\n","# Authentication & Authorization Review\n\n## 1 — Map the auth architecture first\n\nFind: login\u002Fregister\u002Fpassword-reset endpoints, session middleware, role\u002Fpermission checks, token creation\u002Fvalidation, 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.\n\n### Route census (enumerate, don't guess)\n\nPattern greps find what they look for; a census finds what nobody remembered. Enumerate EVERY route, then check each one:\n\n```bash\nrg -n \"app\\.(get|post|put|patch|delete|all|use)\\(\"            # Express\u002FKoa\nrg -n \"@app\\.route\\(|@\\w+\\.route\\(|add_url_rule\"              # Flask\nrg -n \"path\\(|re_path\\(\" *\u002Furls.py                             # Django\nrg -n \"^\\s*(get|post|patch|put|delete) |resources \" config\u002Froutes.rb  # Rails\nrg -n \"@(Get|Post|Put|Patch|Delete|Request)Mapping\"             # Spring\nrg -n \"Route::(get|post|any|resource|apiResource)\" routes\u002F     # Laravel\nrg -n \"@(app|router)\\.(get|post|put|delete|patch)\"             # FastAPI\n```\n\nBuild a table `route → auth middleware → ownership check` and fill it by reading each handler. Every row with **neither** is a finding (Critical for admin\u002Fstate-changing, High otherwise). Census-only catches:\n- routes registered BEFORE the auth middleware (order bugs — `app.use(auth)` must precede every guarded route)\n- the one handler missing `@login_required` \u002F `authorize` \u002F `auth:sanctum` in an otherwise-guarded file\n- non-HTTP surfaces that are routes in disguise: queue consumers, cron\u002Fbackground job handlers, webhook receivers, GraphQL resolvers (see `..\u002Fgraphql-security\u002FSKILL.md`), event subscribers, RPC\u002Fmessage handlers — census them with the same table\n- dispatch-style apps (`?action=` → switch) — enumerate the switch arms, not the single URL\n- **case-sensitive middleware paths**: on case-insensitive hosts\u002Frouters, `app.use('\u002Fadmin', guard)` can be bypassed by `\u002FADMIN` or `\u002FAdmin\u002F` — check the router's case-sensitivity setting (Express: default sensitive only with regex; some frameworks\u002Ffilesystems are not) and whether any guarded prefix can be cased around → authz bypass\n- **encoding-sensitive middleware paths** (CWE-177): authz guards comparing the RAW, still-encoded request URI (`req.url` \u002F `req.originalUrl`, Java `getRequestURI()`, `RAW_URI`) while the router dispatches on the DECODED path — `GET \u002F%61dmin\u002Fusers` fails `startsWith('\u002Fadmin')` 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`, Java `getServletPath()`), or mount the guard on the route itself (`app.use('\u002Fadmin', guard)`), so percent-encoding can't split the check from the dispatch:\n```bash\nrg -n \"req\\.(url|originalUrl)\\.(startsWith|endsWith|includes|match)|getRequestURI\\(\\)\\.(startsWith|contains|equals)|RAW_URI\" -g '*.js' -g '*.ts' -g '*.java' -g '*.py'\n```\n- **suffix\u002Fcontains path matching in auth whitelists** (third variant of\n  the path-matching family: case → encoding → MATCHER STRICTNESS): a\n  public-path exemption checked with `path.endsWith('\u002Fx')` or\n  `path.contains('\u002Fx')` instead of an exact\u002Fanchored match frees EVERY\n  colliding route, not just the intended public one (class incident:\n  Kestra CVE-2026-49869, KEV — `AuthenticationFilter` used\n  `request.getPath().endsWith(\"\u002Fconfigs\")` to whitelist the public\n  configs endpoint; every namespace-scoped `...\u002Fconfigs` route bypassed\n  Basic Auth → unauthenticated API access → RCE). Safe shape: exact\n  match set (`new Set(['\u002Fconfigs']).has(path)`) or anchored regex\n  (`^\u002Fconfigs$`) — never suffix\u002Fsubstring tests on a path:\n```bash\nrg -n \"endsWith\\(\\s*['\\\"]\\\u002F|\\.contains\\(\\s*['\\\"]\\\u002F\" src\u002F -g '*.java' -g '*.js' -g '*.ts' -g '*.py' | head\n```\n- **setup\u002Finstall\u002Ffirst-run wizard routes**: `app.post('\u002Fsetup\u002Frestore')`, `\u002Finstall`, `\u002Fapi\u002Fsetup\u002F*` 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.restore` invoked 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:\n```bash\nrg -n -i \"['\\\"]\u002F(setup|install|installer)[^'\\\"]*['\\\"]\" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.rb' -g '*.php' | head -15\n```\n- **gRPC \u002F protobuf services**: `service X { rpc Y (...) }` in `.proto` files — every `rpc` is a route; check per-method auth interceptors and validate request fields like any handler. Same for **message-queue consumers** (Kafka\u002FRabbit\u002FSQS handlers), **scheduled job entry points**, and **Netty\u002FWebSocket frame handlers** — add them all to the census table\n```bash\nrg -n \"rpc \\w+\\(\" -g '*.proto'; rg -n \"@GrpcClient|StreamObserver\" -g '*.java' -g '*.kt'\nrg -n \"@(KafkaListener|RabbitListener)|consumer\\.|@Scheduled|@CloudFunction|functions\\.\" -g '*.java' -g '*.kt' -g '*.py' -g '*.js' | head\n```\n\n## 2 — Authentication checks\n\n### Password storage\n```bash\nrg -n -i \"bcrypt|argon2|scrypt|pbkdf2|md5|sha1|sha256.*password|hashpw|crypt\\(\"\n```\n- `bcrypt`\u002F`argon2id`\u002F`pbkdf2` with sane params (bcrypt cost ≥ 10, argon2id defaults or better) → OK\n- MD5\u002FSHA1\u002FSHA-256 for passwords → **CRITICAL** (unsalted fast hash = instant crack)\n- Plaintext or reversible encryption → **CRITICAL**\n\n### Login mechanics\n- User enumeration: login and password-reset responses differ for unknown vs wrong-password (timing or body) → MEDIUM\n- No rate limiting \u002F lockout on login, reset, and MFA endpoints → HIGH (note where it would live: middleware, API gateway, app code)\n- Password reset: predictable token (timestamp, userid, `random.random()`), token reused or non-expiring, reset response leaks the token → HIGH\u002FCRITICAL\n- MFA present? Missing MFA on admin accounts → HIGH if roles exist\n\n### LDAP authentication (enterprise directories)\n\n```bash\nrg -n \"SECURITY_AUTHENTICATION|ANONYMOUS\" -g '*.py' -g '*.java'\nrg -n 'uid=\"\\s*\\+\\s*\\w+|SECURITY_PRINCIPAL.*\\+' -g '*.py' -g '*.java'\nrg -n '\\(sAMAccountName=|\\(uid=' -g '*.py' -g '*.java'\nrg -n \"escape_dn_chars|escape_filter_chars|LdapEncoder\" -g '*.py' -g '*.java'\n```\n\n- Anonymous bind offered as an auth path (`authentication=ANONYMOUS`, `SECURITY_AUTHENTICATION` set to `none`) → **CRITICAL** — everyone \"authenticates\"; binding ≠ verifying the intended identity\n- **DN injection** (CWE-90): bind DN built by concatenation — `\"uid=\" + user + \",ou=people,...\"` (Python) or `Context.SECURITY_PRINCIPAL` concatenation (Java) → **CRITICAL**: a uid containing `,...` or `+` rebinds as a different principal\n- **Filter injection**: search filters composed by f-string\u002Fformat with user terms (`f\"(sAMAccountName={name})\"`) → HIGH — `*` and `)(` characters forge wildcard\u002Fboolean filters (auth bypass + data mining)\n- Safe shape: escape before composing (`escape_dn_chars()` \u002F `escape_filter_chars()` in ldap3, `LdapEncoder.filterEncode\u002FnameEscape` in Java) — or constant service DN + post-bind attribute comparison\n- The DN\u002Ffilter greps hit BOTH vulnerable and escaped forms — the escape-call context is the triage discriminator (same census discipline as XSS sinks)\n\n### Profile changes → account takeover\n\nEmail\u002Fpassword-change endpoints are AUTH endpoints — a session alone is not sufficient proof (stolen session, XSS-riding, insider):\n\n```bash\nrg -n \"email\\s*=|updateEmail|changeEmail|current_password|currentPassword|change.?password\" src\u002F app\u002F | head -20\n```\n\n- 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\n- Password change without current password → same class, High\n- Username\u002Fhandle change that re-frees an old identifier while reset-by-username flows exist → Medium\n- Safe shape: `requireCurrentPassword` (or step-up re-auth) + verification mail to the new address before activation + revocation notice to the old one\n\n### JWT \u002F tokens\n```bash\nrg -n \"jwt\\.(sign|decode|verify)|verify\\(|algorithms|algorithm|none|HS256|RS256\"\n```\n- `algorithm: \"none\"` accepted or `jwt.decode(token)` without algorithm pinning → **CRITICAL**\n- **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.\n```bash\nrg -n \"algorithms.*HS256|verify\\(.*public|createPublicKey|jwt\\.RSAPublicKey\" \n```\n- **JWKS `kid` injection**: token-controlled `kid`\u002F`alg`\u002F`jku`\u002F`x5u` header flowing into paths, queries, or fetches (`'.\u002Fkeys\u002F' + header.kid`, `jwks_url + kid`) → path traversal \u002F attacker-controlled key material → **Critical**. Trust `kid` for lookup only against a fixed key set.\n```bash\nrg -n \"header\\.kid|\\bkid\\b.*\\+|jku|x5u\" \n```\n- **PKCE**: public clients (SPA\u002Fmobile\u002Fdesktop — anything that can't hold a secret) using auth-code flow without `code_challenge`\u002F`code_verifier` → code interception, HIGH. Server-side confidential clients may omit it (note, not a finding).\n- Symmetric signing with weak\u002Fdefault secret (`secret`, `jwt-secret`, `changeme`) → CRITICAL\n- No `exp` validation, or no `aud`\u002F`iss` checks where multiple services share keys → HIGH\n- Tokens in localStorage with XSS present elsewhere → MEDIUM (compounding)\n- Session tokens: not rotated on login\u002Fprivilege change → MEDIUM; long-lived non-revocable tokens → HIGH\n\n## 3 — Authorization checks (IDOR & friends)\n\nFind object access points, then check each for an ownership\u002Fscope test:\n```bash\nrg -n \"req\\.params\\.id|findById\\(|get\\(|\\.filter\\(.*userId|user\\.id|currentUser|req\\.user\"\n```\n- `Model.findById(req.params.id)` returned directly with no `where userId` \u002F post-fetch ownership check → **IDOR**, HIGH (CRITICAL for sensitive objects: invoices, messages, PII)\n- **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 `..\u002Fflow-security\u002FSKILL.md` (class F3) — audit flows, not just routes |\n- Admin routes: how are they guarded? Missing middleware → CRITICAL. Note *function-level* checks too (regular user hitting `\u002Fadmin\u002F*` handlers).\n- Horizontal vs vertical: test reasoning for both — same-role users touching each other's data, and low-role → admin.\n- Trusting client-side hints: `is_admin` from request body\u002Fcookie-in-JWT-without-verify, hidden-but-served admin UI → HIGH\n- **authz on derived\u002Fdivergent state** (ToB, Provenance 2026): when a permission decision reads a DERIVED value (cached role, denormalized count\u002Fbalance, materialized flag) instead of the live source-of-truth record, two failure modes: (a) stale\u002Freplicated state authorizes what live state would deny, and (b) missing\u002Fzero\u002Fdefault derived state must FAIL CLOSED — a check like `if derived_supply > 0: grant admin` grants when the derived value is empty. Trace every authz read to its source-of-truth write; verify the default branch denies.\n\n### Mass assignment (generic)\n\nThe framework skills cover Laravel\u002FDjango\u002FRails fillable semantics; the generic version bites every ORM:\n```bash\nrg -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\"\n```\nRequest body spread into create\u002Fupdate without an allowlist → attacker sets `role`, `isAdmin`, `userId`, `credits`, `price`, `email` (account takeover via email change). Report **High** — Critical when the model has role\u002Fpermission fields. Safe: explicit field destructuring\u002Fpick, schema-level allowlist (`$fillable`, `fields`), validators configured to strip unknown keys (default `validate`\u002Fzod `.strict()` — not passthrough). Also check filter\u002Fmap \"sanitizers\" that only check types: an allowLIST of keys, not of types, is what counts.\n\n### Trusted-header authorization (identity spoofing)\n```bash\nrg -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_\"\n```\nUsing `X-User-Id` \u002F `X-Email` \u002F `X-Admin` \u002F `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\u002Fgateway strips these headers from inbound traffic, and (2) the app is not directly reachable (check NodePort\u002FLoadBalancer 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.\n\n**SSO\u002FOIDC callback host verification (CWE-943).** Rocket.Chat GHSL-2026-004\u002F005: an account\u002FSSO service accepted callbacks whose host did not match the configured\u002Fallowed one → authentication bypass. Wherever an OIDC\u002FOAuth\u002FSAML\u002FOmniAuth callback or JWKS\u002Fdiscovery URL is resolved from configuration, verify the code checks the **exact** host\u002Forigin (allowlist, not substring\u002F`endsWith`) and rejects mismatches before trusting the identity assertion → Critical (direct auth bypass).\n\n**Authorization predicate on a request-derived principal** (sibling of header spoofing — the write side). The capability\u002Frole 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\u002Fparam 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\u002Ftoken verification path; request data may name a TARGET, never the ACTOR.\n```bash\nrg -n \"findById\\(\\s*req\\.(cookies|body|query)|get_user_by\\([^)]*\\$_(COOKIE|GET|POST|REQUEST)|user_can\\s*\\(\\s*\\$\" src\u002F app\u002F -g '*.js' -g '*.php'\n```\n\n### Multi-tenant scoping census (SaaS)\n\nPer-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\u002Fexport\u002Freport endpoints that feel \"shared\").\n```bash\nrg -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\nrg -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\n```\nMethod: list every model access, mark each SCOPED (tenant filter present) \u002F UNSCOPED \u002F 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\u002Fheader instead of session (`req.body.tenantId` — attacker-controlled scope switch → cross-tenant, Critical), and cache keys missing the tenant prefix (cross-tenant cache bleed).\n- **Target-tenant vs caller-tenant on credential-issuing endpoints**: when\n  a request names a TARGET (`user_id`, `email`, `login_name`) and the\n  handler issues enrollment codes \u002F invitations \u002F reset URLs \u002F passkey or\n  passwordless registrations for that target, checking only the CALLER's\n  request tenant (org header, subdomain) while never comparing the\n  TARGET's tenant = cross-tenant takeover: an admin (or any\n  user-write holder) in org A obtains enrollment credentials for org-B\n  users and registers their own authenticator (class incident: ZITADEL\n  CVE-2026-105209 — passkey enrollment codes authorized against\n  `x-zitadel-orgid` while the target user's organization went unchecked,\n  cross-org ATO, CVSS 9.6). The authz must compare the TARGET's tenant\n  with the caller's tenant (or verify the caller's scope covers the\n  target's tenant) — validating the request's own tenant header proves\n  nothing about the target. Strongest shape: scope the target lookup itself\n  by the caller's session tenant (`findOne({ id, org_id: session.org })`)\n  rather than fetch-then-check:\n```bash\nrg -n -i \"enroll(ment)?.?code|invite.?(code|user)|passwordless\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java' | head\n```\n### Check-then-act races (TOCTOU)\n\nLook for state checks followed by a **separate** write:\n```bash\nrg -n \"if\\s*\\(.*\\b(used|redeemed|approved|active|enabled|stock|balance|remaining)\\b|\\.findOne\\(.*\\)\\.then|SELECT.*\\b(balance|stock|used)\\b.*FROM|\\.save\\(\\)\"\n```\n- Single-use coupons\u002Ftokens\u002Freset links: `if (t.used) reject; ... t.used = true` → parallel replay wins every time → **High** (Critical for payments\u002Frefunds\u002Fwebhooks). Fix: atomic conditional write — `findOneAndUpdate({code, used:false}, {$set:{used:true}})`, `UPDATE ... SET used=1 WHERE code=? AND used=0` checking rows affected.\n- Balance\u002Fstock read-modify-write: `user.balance -= amt; user.save()` → double-spend under concurrency. Fix: `UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?`.\n- Webhooks\u002Fevent handlers without idempotency keys or event-id dedup → replayable. Report as one grouped finding with every affected flow listed.\n\n### Feature-flag & plan-capability gating\n\nFlags constrain CAPABILITY (what a plan\u002Ftenant\u002Frole may do), not identity — an authenticated admin can still be the attacker the plan must stop. UI\u002Fmenu gating is cosmetic; the handler\u002Froute 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.\n\n```bash\nrg -n \"Feature::(define|active)|isFeatureEnabled|featureFlags|feature_flags|isEnabled\\(|isEnabledFor|\\.variation\\(\" src\u002F app\u002F modules\u002F\nrg -n \"pennant:purge|flushCache|flag.*purge\" deploy\u002F scripts\u002F .github\u002F\n```\n\nStack-agnostic census (framework skills carry the concrete greps — e.g. `..\u002Flaravel-security\u002FSKILL.md` Step 8):\n\n- 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\u002Fmenu code or only inside async jobs = **gated-in-the-wrong-layer** → High; Critical when the ungated surface fans out outbound messages (F9, `..\u002Fflow-security\u002FSKILL.md`)\n- Default-true definitions for dangerous capabilities (`'import': true`, `\"send_email\": true` in flag config) → High — the capability exists for everyone including trials; per-plan closures\u002Flookups are the safe shape\n- **Flag-store staleness**: persisted flag stores (Pennant database, DB-backed Unleash\u002FOpenFeature, a `features` table) keep stored per-scope values when defaults change — reverting a default changes nothing for already-stored scopes. Deploy scripts must purge\u002Fsync; absence → Medium\n- 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\u002FSMS bomb needing zero auth and zero flags\n\n### One-time codes (verification \u002F login \u002F MFA codes)\n\nA 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:\n\n```bash\nrg -n -i \"verification.?code|login.?code|one[-_ ]?time.?code|\\botp\\b|magic.?code|sms.?code|email.?code\" src\u002F app\u002F routes\u002F | head -40\nrg -n -i \"(master|static|backdoor|debug|test).{0,12}code\" src\u002F app\u002F config\u002F db\u002F seeds\u002F -g '*.sql' -g '*.java' -g '*.py' -g '*.js' -g '*.ts' -g '*.yml'\n```\n\n- 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 \u002Fsys\u002Fmobile\u002Fcode and \u002Fsys\u002Femail\u002Fcode returned codes; login via \u002Fsys\u002FemailOrMobileLogin)\n- Static\u002Fmaster code the verifier accepts (seed rows, config constants, \"test-mode\" fallbacks — X-SpringBoot's `172839` seed, CVE-2026-97064; entry: `..\u002Fcve-research\u002Fvuln-db\u002Fentries\u002F2026-09-25-cve-2026-97064.md`) → **Critical** — a public backdoor credential; if it ever shipped, assume it IS used\n- Code delivered to an address\u002Fnumber taken from the REQUEST instead of the stored account record → High — attacker-controlled delivery channel harvests codes\n- Weak hygiene: reusable codes, no expiry, no attempt cap on verify, predictable generators (`Math.random`) → Medium→High by brute-force feasibility\n- 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\n\n### Webhook receiver authentication\n\nWebhook endpoints are authenticated by the SENDER's signature, not by your sessions — an unverified receiver is an unauthenticated endpoint with money\u002Fentitlement powers. Census every webhook\u002Fcallback route with §1's table, then:\n\n```bash\nrg -n \"webhook|callback\" src\u002F app\u002F routes\u002F | rg -i \"post|route\" | head -20\nrg -n \"timingSafeEqual|compare_digest|verify_sig|verifySignature|signature\" src\u002F app\u002F\nrg -n \"!process\\.env\\.[A-Z_]*SECRET|WEBHOOK_SECRET\" src\u002F app\u002F .env.example 2>\u002Fdev\u002Fnull\n```\n\n- Handler parses `req.body`\u002Fevent and ACTS (fulfill, enroll, activate, refund) without verifying the sender's HMAC signature → **Critical** (forged events = free entitlements; pairs with F4 replay in `..\u002Fflow-security\u002FSKILL.md`)\n- Signature compared with `==`\u002F`===` instead of constant-time (`timingSafeEqual`, `hmac.compare_digest`) → Medium (timing oracle)\n- Signature computed over re-parsed JSON instead of the RAW request bytes → verification fails → commonly \"fixed\" by deleting the check (drift toward Critical)\n- **Verification gated on the secret being SET** (`if (!WEBHOOK_SECRET) next()` \u002F 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\u002Fempty secret must REJECT (fail closed), never skip verification.\n- No replay window (timestamp\u002Fevent-id dedup) → High\n- **Callback that ESTABLISHES a session** (not merely acts): the handler authenticates the CALLER from the payload — `wp_set_auth_cookie()`\u002F`wp_set_current_user()` on a request-supplied user id, `req.login(user)` where `user` came from a GET\u002FPOST 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_callback` on `init`, base64 `paypal_param` GET param, no IPN\u002Fsignature\u002Fownership\u002Fnonce → `wp_set_auth_cookie()`. Census every session-establishment call and trace where its user comes from:\n```bash\nrg -n \"wp_set_auth_cookie\\s*\\(|wp_set_current_user\\s*\\(|req\\.login\\s*\\(|\\.logIn\\s*\\(\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.php'\n```\n- Safe shape: the callback verifies the sender FIRST and resolves the member\u002Fuser 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\n\n### API keys as authentication (service-to-service)\n\nMachine auth has its own failure modes — check every API-key\u002Fgateway-token scheme:\n```bash\nrg -n -i \"x-api-key|api[_-]?key\" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.rb' | head\n```\n- **Key in URL\u002Fquery** (`?api_key=`) — hits logs, referrers, browser history → HIGH (same class as tokens in URLs)\n- **Unscoped keys** — one god-key instead of per-service\u002Fper-permission keys: lateral movement on any leak → MEDIUM\u002FHIGH\n- **No rotation story** — no expiry, no revocation endpoint, keys older than the repo's history → MEDIUM (operational)\n- **Constant-time comparison absent** (`==` on the key) → timing oracle → LOW\u002FMEDIUM (pairs with `crypto-review` comparison rules)\n- **Weak key generation** — `uuid()`, `random.random()`, timestamp-derived → predictable keys → HIGH (see crypto-review randomness table)\n- **Key = password reuse** — same key validates AND encrypts\u002Fsigns → separation of duties violation → MEDIUM\n- **Non-secret identifiers as credentials** — authenticating a device\u002Fuser 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\u002FiPadOS host, MDM migration included). Identifiers are for LOOKUP, never for proof — enroll with a one-time secret or client cert, then authenticate on that:\n```bash\nrg -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'\n```\n- **Internal keys honored as master auth on every route** (UTMStack CVE-2026-82042 class: `InternalApiKeyFilter` accepted the `Utm-Internal-Key` header == `INTERNAL_KEY` env 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\u002Fservice 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\u002Fhosts, (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:\n```bash\nrg -n -i \"internal[_-]?key|x-internal|internal[_-]?api[_-]?key\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java'\n```\n\n## 4 — Session & CSRF\n\n- Cookie flags on session\u002Fauth cookies: `HttpOnly` (missing + XSS present → HIGH), `Secure` (missing → HIGH if prod is HTTPS), `SameSite` (missing → MEDIUM)\n- CSRF: state-changing endpoints protected by token\u002F`SameSite=Strict`\u002F`Origin` check? Both auth-cookie AND no CSRF defense → HIGH\n- Logout invalidates server-side session? Not just client cookie clear → MEDIUM\n- Session fixation: session ID regenerated after login → check\n\n## 5 — OAuth\u002FOIDC, if present\n\n- `redirect_uri` validated exactly (not prefix\u002Fsuffix match) → open redirect_uri = account takeover, CRITICAL\n- `state` parameter present and verified → missing = CSRF\u002Flogin CSRF, MEDIUM\u002FHIGH\n- Token in URL fragment vs query (query leaks via logs\u002Freferrers) → MEDIUM\n- `id_token` validated: signature, `nonce`, `aud` → skipping any = CRITICAL\n- **Account linking (external IdP identity → local account) without caller\n  verification**: census every surface that BINDS an external-IdP identity\n  to a local account (`AddIDPLink`, `\u002Fauth\u002Flink`, `link_account`, social\n  connect). A linking call that acts on a caller-SUPPLIED login name \u002F\n  user identifier — without an authenticated session plus a fresh primary\n  factor or proof of control of BOTH identities — lets anyone who knows a\n  victim's login name attach their OWN IdP identity and then sign in as the\n  victim (class incident: ZITADEL CVE-2026-105207 — User Service V2\n  AddIDPLink + identify-only Login V2 sessions, unauthenticated ATO,\n  CVSS 9.8). → Critical. Safe shape: link only to the session principal,\n  require step-up re-auth, and take the external identity from the IdP's\n  verified assertion (never from request fields):\n```bash\nrg -n -i \"add_?idp|idp.?link|link.?(account|identity)|connect.?(account|identity)\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.go' -g '*.php' | head -20\n```\n\n### Host-header poisoning in outbound auth\u002Femail flows\n\nPassword-reset\u002Fverification links and OAuth redirect URIs built from `Host` \u002F `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 `..\u002Fdjango-security\u002FSKILL.md` Step 1). Grep link\u002FURL construction in mailers and auth controllers for request-header inputs → HIGH (account takeover via poisoned reset links).\n## 6 — SAML (if present)\n\n```bash\nrg -n -i \"saml|assertion|NameID|acs[ _-]?url|AudienceRecipient|samlp\" -g '*.py' -g '*.java' -g '*.rb' -g '*.php' -g '*.js' -g '*.xml' | head -15\n```\n\nThe four classic SAML bugs, in observed frequency order:\n- **Signature wrapping (SWA\u002FXSW)**: 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 for `one_assertion_only` \u002F `want_assertions_signed` settings — absent → HIGH\u002FCritical.\n- **Comment injection in NameID**: `admin@corp.com\u003C!--atk-->@evil.com` parses as admin@corp.com to the IdP-signed value but the SP extracts differently — mitigated by library versions and `NameID` format checks; flag any custom NameID string handling → HIGH.\n- **Unvalidated recipients**: `Destination`\u002F`AudienceRestriction`\u002FACS URL not enforced (or ACS taken from the assertion!) → assertion replay across SPs. Config keys: `expected_audience`, `acs_url` pinned server-side → absent → Critical.\n- **Loose validation knobs**: `want_response_signed=False` + `want_assertions_signed=False`, clock skew ±24h (`not_before`\u002F`not_on_or_after` neutered), `allow_unsolicited` in prod → each HIGH.\n\nSafe shapes: pinned ACS + audience, both signed flags true, one-assertion-only, tight skew (≤5m), library current (python3-saml \u002F OneLogin \u002F passport-saml families each had historic CVEs — version-check via vuln-db\u002FOSV).\n\n## 7 — Realtime channels (WebSocket \u002F SSE)\n\n```bash\nrg -n \"io\\.on\\(|socket\\.on\\(|new WebSocket|WebSocketServer|@MessageMapping|@SubscribeMapping|STOMP|EventSource|ActionCable|cable\\.|channel\\.subscribe\"\n```\n\n- **Handshake auth**: no `io.use(authMiddleware)` \u002F token check on connect → **CRITICAL** (any client connects as anyone)\n- **Subscription authz**: `socket.join('room:' + id)` \u002F channel subscribe without ownership check → IDOR over sockets, **High** — same rule as REST IDOR, different transport\n- **Message handlers are routes**: every `socket.on('cmd', ...)` doing a state change needs authz; grep the handler bodies exactly like controllers\n- **Command-forwarding handlers are RCE surfaces** (UTMStack CVE-2026-82041 class: STOMP `\u002Fcommand\u002F{hostname}` destination → `processCommand()` forwarded arbitrary OS commands over gRPC to agents, no role check, no allowlist — any authenticated user, any role, root\u002FSYSTEM on monitored hosts): a realtime channel whose messages become COMMANDS executed on backends\u002Fagents 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(`\u002F`spawn(` inside the handler → **Critical** (remote command execution on downstream hosts):\n```bash\nrg -n \"socket\\.on\\(\\s*['\\\"][^'\\\"]*(command|cmd|exec|run)|@MessageMapping\\(\\s*['\\\"][^'\\\"]*(command|cmd|exec)\" src\u002F app\u002F -g '*.js' -g '*.ts' -g '*.java'\nrg -n \"(agent|host)\\.(send|execute|invoke)\\(\" src\u002F app\u002F | head\n```\n- `io.origins('*')` \u002F missing origin check on a credentialed socket → Medium (cross-site WebSocket hijacking)\n- SSE (`EventSource` endpoints) are plain GETs — same authz as any other route\n\n## Reporting\n\nFor 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.\n",{"name":5,"license":15,"description":7},"main","3df1d0d67b30ff15236561f8c0f9f06d33f42dfd","skills\u002Fchains\u002Fashrafiucse-auth-review",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-14 02:27:51",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill auth-review",{"tool":63,"command":64},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill auth-review",{"tool":66,"command":67},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill auth-review","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fashrafiucse-auth-review\u002FSKILL.md",true,[71,75,81],{"path":72,"resourceType":72,"resourceTypeDesc":73,"sizeBytes":74,"sourceHtmlUrl":16},"LICENSE","许可证",1067,{"path":76,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":79,"sourceHtmlUrl":80},"SOURCE.md","REFERENCE","参考文档",368,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fashrafiucse-auth-review\u002FSOURCE.md",{"path":82,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":83,"sourceHtmlUrl":84},"TRUST.auto.yaml",2197,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fashrafiucse-auth-review\u002FTRUST.auto.yaml",[86,114,137,155,168,178],{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":90,"sourceHtmlUrl":91,"categories":92,"keywords":93,"publishedAt":43},"01M712C436EQYE43342Q4E7GG1","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\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113],"performance","recipe performance","recipe","mm","sentry","harness","app","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":118,"licenseFileUrl":119,"sourceHtmlUrl":120,"categories":121,"keywords":122,"publishedAt":43},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[123,124,125,126,127,128,129,130,131,132,133,134,135,136],"mexc spot websocket","spot","websocket","api","workflows","public","listen","streams","depth","keys","for","api keys","maintenance","protobuf",{"skillNo":138,"name":139,"nameZh":6,"description":140,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":118,"licenseFileUrl":141,"sourceHtmlUrl":142,"categories":143,"keywords":144,"publishedAt":43},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[145,124,146,147,148,149,150,134,133,132,151,152,153,128,154,127,126],"mexc spot rest","REST","admin","signing","Wallet","requests","actions","secure","data","orders",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":118,"licenseFileUrl":159,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":43},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[163,164,125,165,166,135,133,167,131,130,128,127],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":169,"name":170,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":171,"sourceHtmlUrl":172,"categories":173,"keywords":174,"publishedAt":43},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[175,176,177],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":182,"sourceHtmlUrl":183,"categories":184,"keywords":185,"publishedAt":43},"01M6H0Z28Q3R8J0S799GJQWXM3","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.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[186,187,188,189,190,191,192,193,194,41,195],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","use","patch","share"]