$npx -y skills add briiirussell/cybersecurity-skills --skill owasp-auditAudit application source code against the OWASP Top 10 (2021) vulnerability categories — broken access control, cryptographic failures, injection, insecure design, security misconfiguration, vulnerable components, authentication failures, data integrity, logging failures, SSRF. U
| 1 | # OWASP Audit — Source Code Security Review |
| 2 | |
| 3 | Perform a systematic security audit of application source code against the OWASP Top 10 (2021). |
| 4 | |
| 5 | ## Scope the Audit |
| 6 | |
| 7 | 1. Identify the project's language, framework, and architecture |
| 8 | 2. Map entry points (routes, API handlers, form processors) |
| 9 | 3. Identify data flows (user input → processing → storage → output) |
| 10 | 4. Locate authentication and authorization boundaries |
| 11 | |
| 12 | ## Audit Checklist |
| 13 | |
| 14 | Work through each category systematically. For each, grep for known vulnerability patterns, then read flagged files for deeper analysis. |
| 15 | |
| 16 | ### A01: Broken Access Control |
| 17 | - Missing authorization checks on endpoints or routes |
| 18 | - IDOR — user-controlled IDs without ownership verification |
| 19 | - **Auth-check ordering.** Verify the authorization check runs *before* any branch that can reveal whether the resource exists, what state it's in, or any other resource-specific metadata. Returning 404 for "not found", 400 for "wrong state", and 401 for "not authenticated" is itself a leak — an attacker enumerates resource IDs and learns states without ever passing the auth gate. Recommended response shape: uniform 404 for everything an unprivileged caller should not see. |
| 20 | - **Framework RPC surfaces that don't appear as routes.** Server actions and equivalents are publicly-exposed RPCs that file scans miss. Enumerate and audit each one for auth + ownership: |
| 21 | - Next.js: every exported function in a file with `'use server'` |
| 22 | - Remix / React Router: every `action` / `loader` export |
| 23 | - tRPC: every procedure |
| 24 | - GraphQL: every resolver |
| 25 | - Rails: non-resource controller actions |
| 26 | - **IDOR via foreign keys in mutation payloads.** Form posts a foreign-key UUID (`categoryId`, `projectId`, `teamId`, `organizationId`) → server validates ownership of the primary record but blindly accepts the FK → ORM relation join later surfaces another tenant's data. Look for `formData.get("<id>")` / `body.<id>` passed straight to insert/update without a preceding `findFirst({ where: { id, userId } })`. For ORM relation joins (Drizzle `with:`, Prisma `include`, ActiveRecord `includes`), trace whether the join target is filtered by the same tenant/ownership predicate as the parent query. |
| 27 | - Missing CSRF protections on state-changing requests |
| 28 | - Role checks only on the frontend, not enforced server-side |
| 29 | - Open redirect via post-auth return-to parameter — `?from=`, `?next=`, `?returnTo=`, `?continue=`, `?redirect=` passed unsanitized to `redirect()` / `Response.redirect()`. Restrict to same-origin paths under the expected scope, normalize (`new URL(target, "http://localhost").pathname`) to defeat traversal like `/admin/../foo`. Also reject control bytes in the path before redirect: tab/newline/null (`\t`, `\n`, `\0`) — URL parsers strip these and collapse `/\tevil` into protocol-relative `//evil`; null bytes can turn the redirect into a 500. Reject any byte in `[\x00-\x1F\x7F]`, any backslash, and any percent-encoded slash/backslash (`%2f`, `%5c`). |
| 30 | - Grep for: direct object references, missing auth middleware, user ID from request params, `redirect(.*from`, `redirect(.*next`, `redirect(.*returnTo` |
| 31 | |
| 32 | ### A02: Cryptographic Failures |
| 33 | - Hardcoded secrets, API keys, or passwords in source |
| 34 | - Weak hashing (MD5, SHA1 for passwords instead of bcrypt/argon2/scrypt) |
| 35 | - For bcrypt, also check the cost factor. OWASP 2024 guidance is ≥ 12 (cost 10 ≈ 10ms / 100 hashes/sec/core for an attacker) |
| 36 | - **Type coercion in cryptographic-verification paths.** Numeric parsing (`parseInt`, `Number`, `parseFloat`) silently produces `NaN` for garbage input, and `NaN` compares as `false` for both `<` and `>`. A timestamp-freshness check `if (Math.abs(now - parsed) > tolerance) return false` *fails to reject* `NaN` — because `NaN > tolerance` is `false`. Grep for: `parseInt|parseFloat|Number\(.*\)` inside `verifySignature` / `validateToken` / signed-cookie / JWT-claim code. Each numeric extraction must be followed by `if (!Number.isFinite(parsed)) return false` before any inequality. Same family: `parseInt('0x123', 10) === 0`, `parseInt('1e10', 10) === 1`, `parseFloat('Infinity') === Infinity`. |
| 37 | - Sensitive data in logs, URLs, or localStorage |
| 38 | - Missing encryption at rest or in transit |
| 39 | - **Before recommending `VERIFY_PEER` for a TLS |