owasp-audit

Audit 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. Use when the user men

By briiirussell · 392 installs

npx skills add briiirussell/cybersecurity-skills --skill owasp-audit

Source repository · Upstream listing

OWASP Audit — Source Code Security Review Perform a systematic security audit of application source code against the OWASP Top 10 (2021). Scope the Audit 1. Identify the project's language, framework, and architecture 2. Map entry points (routes, API handlers, form processors) 3. Identify data flows (user input → processing → storage → output) 4. Locate authentication and authorization boundaries Audit Checklist Work through each category systematically. For each, grep for known vulnerability patterns, then read flagged files for deeper analysis. A01: Broken Access Control Missing authorization checks on endpoints or routes IDOR — user controlled IDs without ownership verification 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. 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: Next.js: every exported function in a file with 'use server' Remix / React Router: every action / loader export tRPC: every procedure GraphQL: every resolver Rails: non resource controller actions 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. Missing CSRF protections on state changing requests Role checks only on the frontend, not enforced server side 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 ). Grep for: direct object references, missing auth middleware, user ID from request params, redirect(. from , redirect(. next , redirect(. returnTo A02: Cryptographic Failures Hardcoded secrets, API keys, or passwords in source Weak hashing (MD5, SHA1 for passwords instead of bcrypt/argon2/scrypt) For bcrypt, also check the cost factor. OWASP 2024 guidance is ≥ 12 (cost 10 ≈ 10ms / 100 hashes/sec/core for an attacker) 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 . Sensitive data in logs, URLs, or localStorage Missing encryption at rest or in transit Before recommending VERIFY PEER for a TLS connection, identify the cert issuer at the deployment target. Many managed services ship self signed cert chains at lower tiers (Heroku Redis Mini/Hobby, some ElastiCache configurations, Supabase legacy) — VERIFY PEER fails there without an explicit ca file: pin. When VERIFY PEER is genuinely infeasible, present three remediation options in priority order: 1. Upgrade the plan or pin the CA bundle — restores cert verification 2. Accept the risk explicitly — leave VERIFY NONE with (a) an in line comment at every call site, (b) a documented compensating control (private network, internal only routing), (c) a follow up issue tracking re verification conditions 3. Restrict the network path — private subnet / VPC peering / no public exposure Never quietly recommend VERIFY PEER without checking that the cert chain at the deployment target is verifiable. Grep for generic secret names AND known provider key prefixes: Generic: password , secret , api key , private key , MD5 , SHA1 , base64 Stripe: sk live , sk test , rk live , whsec GitHub: ghp , gho , ghu , ghs , ghr AWS: AKIA[0 9A Z]{16} , ASIA[0 9A Z]{16} Google Cloud: AIza[0 9A Za z\ ]{35} , service account JSON ( "type": "service account" ) Slack: xox[baprs] , xoxe.xoxp OpenAI / Anthropic: sk , sk ant Vercel: vercel blob rw Run via git ls files xargs grep lE 'sk live ghp AKIA[0 9A Z]{16} sk ant ' 2 /dev/null so binaries and gitignored files don't pollute output. Include non source file extensions in the sweep. Rails cable.yml / database.yml / storage.yml , Kubernetes manifests, and Vercel / Netlify deploy configs routinely contain TLS or cert config that a source only sweep misses. Concrete sweep for VERIFY NONE / VERIFY PEER: A03: Injection SQL injection: raw queries with string concatenation, missing parameterized queries NoSQL injection: unsanitized user input in MongoDB/Convex queries Command injection: exec() , spawn() , system() with user input XSS: unescaped user input in HTML, dangerouslySetInnerHTML , v html . Inline script breakout via JSON.stringify . Any <script type="application/ld+json" or <script window. DATA = ...</script that interpolates server data through JSON.stringify is vulnerable — JSON.stringify does NOT escape < , , & , U+2028, or U+2029. A stored title containing </script <script alert(1)</script will break out. The "internal only object" framing only saves you when every field is guaranteed never to come from user editable input. Grep for: application/ld+json , html: JSON.stringify , window. + JSON.stringify Fix: wrap with an escape helper that replaces < &\u2028\u2029 with their \uXXXX Unicode escapes before injecting. Rails ERB sinks: raw() , .html safe , <%== , sanitize with a permissive allowlist, and simple format on user input. Grep for these alongside dangerouslySetInnerHTML / v html . Sanitizer choice. When remediating an HTML/SVG XSS sink, the fix MUST use a vetted parser based sanitizer (DOMPurify / isomorphic dompurify / sanitize html for JS; bleach for Python). Reject regex based sanitizers in code review. If unavoidable, a regex sanitizer must: Treat [/\s] (not just \s ) as the attribute name separator — HTML accepts / between tag name and first attribute: <img/onerror=… Strip both SVG and HTML namespace dangerous elements ( <img , <body , <video , <iframe ) — HTML elements instantiate even in SVG rendering contexts Include a final fallback pass that strips any on = regardless of surrounding context Be paired with Content Security Policy: script src attr 'none' as a browser level backstop SVG uploads as stored XSS. SVG files can carry <script / onload . Most blob / object storage serves uploads with the declared content type. Reject image/svg+xml in upload allow lists unless you have a sanitizer (e.g. DOMPurify SVG profile) and serve with Content Disposition: attachment . Sanitize on write AND on render. For stored XSS / injection, sanitize at the trust boundary (write to DB) AND at the render boundary (defense in depth). On finding a stored XSS bug, plan a one time backfill migration to sanitize existing data — render only fixes leave poisoned rows that any new render path will re expose. Rails JSON LD breakout: inside <script type="application/ld+json" , do NOT use j / escape javascript for field values — j emits \' and \$ (valid JS, invalid JSON), so JSON.parse fails on any field containing an apostrophe or $ . Use this idiom instead: to json handles JSON escaping; json escape covers < &\u2028\u2029 against </script breakout. Verify with round trip: JSON.parse(json escape(article.to json)) equals the source hash. Template injection: user input in template literals Grep for: exec( , eval( , innerHTML , dangerouslySetInnerHTML , $where , raw SQL strings A04: Insecure Design Authentication flows with logic flaws Missing rate limiting on sensitive endpoints (login, password reset, API) Business logic constraints only enforced client side Background / fire and forget jobs inherit the caller's auth context but lose the request scoped guards. Re check authorization inside the job, not just at enqueue. Grep for: Promise.all(...).catch( , void someAsync( , .catch(noop) , queue enqueue( without re auth in the worker. Sister route audit. When you find a state machine or immutability guard on one handler (e.g., WHERE … AND signedAt IS NULL on PUT /api/foo/[id] ), grep for every other handler that writes the same table: Each call site needs the same guard, the same userId predicate, and the same conflict handling ( returning() + 0 rows check). Common offender: a POST /:id/send or POST /:id/convert route that ships after the PUT was hardened and was never re audited. External resource create TOCTOU with billing implications. Any handler that does "SELECT to check, then provider.create() , then INSERT to record the new resource ID" can create orphan resources on the provider side under concurrency. Stripe accounts, Auth0 / Clerk users, SendGrid templates, S3 buckets — all bill or count toward quota whether you stored the ID or not. Fix pattern: 1. Claim first with INSERT … ON CONFLICT DO NOTHING (DB UNIQUE constraint is the lock) 2. Call the provider 3. Persist with optimistic guard: UPDATE … SET externalId = ? WHERE externalId IS NULL and check 0 rows 4. On race loss, clean up the orphan via provider.delete(id) best effort; log on cleanup failure Worker queue state transitions need atomic claim. Any cron / worker polling pending rows must atomically claim each row before processing. SELECT + process() + UPDATE is a race — two workers (or two overlapping cron invocations) both see the same pending row and both call out, causing duplicate delivery. Fix: UPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING … — Postgres RETURNING lets you claim and read in one round trip. If the UPDATE returns 0 rows, someone else got it. Alternative: SELECT … FOR UPDATE SKIP LOCKED (Postgres / Cockroach) for higher throughput queues. Multi tenant webhook signature matching. When an unauthenticated webhook endpoint identifies its tenant by trying each tenant's secret in turn, every request — including garbage — does O(N) DB lookups + O(N) HMAC computations. Attackers flood with random signatures and amplify CPU/DB load without ever passing auth. Defences (compose them): 1. Signature shape prefilter before any DB work — reject signatures that aren't the exact length/charset the provider sen