All 16 checks with why-it-matters prose, severity, and cross-references to related audits.
Declaring a Zod or Joi schema and never calling `.parse()` on it is the canonical AI security theater move — the schema exists only as a type alias, and your API accepts arbitrary JSON at runtime. OWASP A03 (Injection) and A04 (Insecure Design) are the direct mappings: without a parse call, mass-assignment attacks, prototype pollution via crafted payloads, and type coercion exploits all reach your database unchecked. If a breach occurs and an auditor finds schemas that were never enforced, the liability argument becomes 'you knew the controls were needed but shipped them as decoration.'
Why this severity: Critical because a dangling schema provides zero runtime protection — every route that trusts the schema is unvalidated in production, exposing the full attack surface of OWASP A03 Injection.
ai-slop-security-theater.unenforced-validation.validation-schemas-have-runtime-useSee full patternA `POST` handler that pipes `req.body` directly into `prisma.user.create({ data: body })` accepts whatever JSON the client sends — extra fields, injected relations, overridden IDs. CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) and OWASP A03 describe this exactly: an attacker can escalate privileges by setting `role: 'admin'` or inject unexpected columns. TypeScript types don't run at runtime; only an explicit `.parse()` call rejects malformed or malicious input before the write commits.
Why this severity: High because unvalidated POST/PUT/PATCH/DELETE bodies enable mass-assignment attacks and arbitrary data injection into the database on every mutating endpoint.
ai-slop-security-theater.unenforced-validation.mutating-routes-validate-inputSee full patternCalling `OrderSchema.parse(body)` after `prisma.order.create({ data: body })` already ran is not validation — it's an audit log that arrives after the crime. The bad data is committed to the database whether the parse throws or not. CWE-696 (Incorrect Behavior Order) captures this: the side effect precedes the safety check. An attacker who sends a malformed payload wins regardless of the parse result, because the DB write already succeeded.
Why this severity: High because validating after the write means every mutation executes with unvalidated input — the check fires after the attack surface is already exploited.
ai-slop-security-theater.unenforced-validation.validation-runs-before-db-writeSee full patternWhen `Body.parse(body)` throws a `ZodError` inside an uncaught handler, most frameworks return a generic 500 Internal Server Error. The client has no idea what field was invalid, and your server logs expose the full Zod error tree — which reveals your schema structure to anyone reading logs or observing error responses. CWE-755 (Improper Handling of Exceptional Conditions) applies: a validation library that crashes the handler instead of returning a 400 teaches clients nothing useful and may leak internal schema details.
Why this severity: Low because the primary failure is poor error communication and log leakage rather than direct exploitation, though repeated 500s from malformed inputs can mask real errors.
ai-slop-security-theater.unenforced-validation.error-responses-handle-validation-failuresSee full patternImporting `helmet` and never calling `app.use(helmet())` means none of its 15 default security headers — `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, `X-XSS-Protection`, `Referrer-Policy`, and others — are actually set on any response. OWASP A05 (Security Misconfiguration) is the exact mapping. Without these headers, browsers don't enforce clickjacking protection, MIME-type sniffing defenses, or HSTS. An attacker who discovers the headers are absent can exploit XSS more easily or frame your app in an iframe for credential harvesting.
Why this severity: High because the absence of security headers removes browser-enforced defenses against XSS, clickjacking, and MIME sniffing on every HTTP response the application returns.
ai-slop-security-theater.unapplied-middleware.helmet-imported-and-appliedSee full patternInstalling `csurf` or `csrf-csrf` and never calling `app.use(csrfProtection)` or the library's equivalent wrapper leaves every state-mutating route open to cross-site request forgery. OWASP A01 (Broken Access Control) and CWE-352 describe the attack: a malicious third-party page can POST to your `/api/transfer` endpoint using a victim's session cookie, and your server will accept it as a legitimate request. The library is evidence that the developer knew CSRF was a risk — the installation without application is the exact theater pattern.
Why this severity: High because every state-mutating endpoint is CSRF-vulnerable, enabling unauthorized actions on behalf of authenticated users from attacker-controlled pages.
ai-slop-security-theater.unapplied-middleware.csrf-imported-and-appliedSee full patternConstructing an `@upstash/ratelimit` or `express-rate-limit` instance and never calling `.limit()` or `app.use(rateLimit(...))` leaves your endpoints open to credential stuffing, enumeration, and API cost amplification. CWE-400 (Uncontrolled Resource Consumption) is the direct mapping. The library's presence in `package.json` signals the developer anticipated abuse — but without invocation, every endpoint accepts unlimited requests. An attacker who discovers the rate limiter is never called can run brute-force or scraping at full speed.
Why this severity: High because an unapplied rate limiter provides zero protection against brute-force, credential stuffing, and API resource exhaustion on every route.
ai-slop-security-theater.unapplied-middleware.rate-limiter-imported-and-appliedSee full patternCalling `app.use(cors())` with no `origin:` option silently defaults to `Access-Control-Allow-Origin: *`, which allows any third-party domain to make cross-origin requests to your API and read the responses. OWASP A05 (Security Misconfiguration) and CWE-942 (Permissive Cross-domain Policy) both call this out. When combined with `credentials: true`, wildcard CORS causes browsers to reject the response anyway — but misconfigured CORS without credentials still exposes unauthenticated API responses to attacker-controlled sites.
Why this severity: Medium because wildcard CORS exposes unauthenticated API responses to any origin, enabling data exfiltration from endpoints that rely on same-origin as a defense.
ai-slop-security-theater.unapplied-middleware.cors-explicitly-configuredSee full patternA project with 12 API routes and no centralized security middleware layer means security decisions are made (or forgotten) per-route. OWASP A05 (Security Misconfiguration) describes the systemic risk: when there is no single chokepoint, individual routes can omit headers, skip auth checks, or miss rate limits without any centralized catch. This is an architectural signal — it doesn't mean every route is insecure, but it means there is no structural guarantee.
Why this severity: Info because the absence of a security middleware layer increases the probability of per-route omissions but does not directly confirm exploitability.
ai-slop-security-theater.unapplied-middleware.security-middleware-export-detectedSee full patternA file inside `app/(authenticated)/` or `app/admin/` that never calls `getServerSession()`, `auth()`, or an equivalent session getter is unprotected — the directory name is a routing convention, not a security boundary. CWE-862 (Missing Authorization) and OWASP A01 (Broken Access Control) describe this exactly. An unauthenticated user who discovers the route URL gets full access. This pattern is extremely common in AI-generated code because the model groups files into protected directories without always wiring the session check inside each file.
Why this severity: Critical because a missing session check in a protected route grants unauthenticated access to admin data, user records, or privileged operations — a direct OWASP A01 broken access control.
ai-slop-security-theater.unbound-auth.protected-routes-call-session-getterSee full patternCalling `const session = await auth()` and then immediately proceeding to render data or execute logic without checking `if (!session)` means the code path runs identically whether the user is authenticated or not. CWE-284 (Improper Access Control) and OWASP A01 apply: a null session is not an error, it's the expected return value when no user is logged in. The absence of a guard after the session call is functionally equivalent to having no auth check at all — the protected logic executes unconditionally.
Why this severity: High because an unguarded session call allows the protected logic to execute when the session is null, bypassing authentication on any route where this pattern appears.
ai-slop-security-theater.unbound-auth.session-getter-failures-return-earlySee full patternA hand-rolled OAuth callback that processes the `code` parameter without comparing the `state` value to a stored cookie value is vulnerable to CSRF on the OAuth flow itself. CWE-346 (Origin Validation Error) and CWE-352 describe the attack: an attacker can initiate their own OAuth flow and trick a victim into completing it, linking the victim's account to the attacker's third-party identity. This enables account takeover without knowing the victim's credentials. Auth libraries (NextAuth, Clerk, Lucia) handle state internally — only hand-rolled callbacks are vulnerable.
Why this severity: Low because the attack requires an active victim session and social engineering to trigger the callback, but successful exploitation leads to account takeover via session fixation on the OAuth binding.
ai-slop-security-theater.unbound-auth.oauth-state-validated-when-handrolledSee full patternAn import of `bcrypt` or `sanitize-html` with zero call sites in the file indicates someone planned to add a security control and stopped. CWE-1164 (Irrelevant Code) covers the surface-level issue, but the real risk is the false assurance it creates during review: a developer or auditor who sees `import sanitize-html` assumes sanitization is happening somewhere. If the call site never got added, user input goes unsanitized, and the import just misleads everyone looking at the code.
Why this severity: Info because unused imports don't directly expose a vulnerability but indicate incomplete security controls that reviewers may incorrectly assume are active.
ai-slop-security-theater.unbound-auth.imported-securty-libs-actually-usedSee full pattern`jwt.decode()` in `jsonwebtoken` does not verify the signature — it base64-decodes the payload without checking the HMAC or RSA signature at all. An attacker can construct a JWT with any payload they want, including `{ role: 'admin', userId: '1' }`, and your application will accept it as a valid authenticated identity. CWE-347 (Improper Verification of Cryptographic Signature) and OWASP A07 (Identification & Authentication Failures) both cite this exact pattern. Authentication built on `jwt.decode()` provides zero security — every token is trusted unconditionally.
Why this severity: Critical because using `.decode()` instead of `.verify()` makes JWT-based authentication trivially bypassable — any attacker can forge a token with arbitrary claims and gain full authenticated access.
ai-slop-security-theater.half-wired-crypto.jwt-verify-not-decodeSee full patternA hardcoded AES key like `'this-is-a-32-character-secret!!'` passed to `crypto.createCipheriv()` is committed to source control and therefore permanently exposed to every developer who has ever had repo access, every CI system, every build log, and every git history tool. CWE-321 (Use of Hard-coded Cryptographic Key) and OWASP A02 (Cryptographic Failures) describe the consequence: if the key is compromised — and it is, the moment it was committed — every record encrypted with that key is decryptable by anyone with the history.
Why this severity: Medium because hardcoded crypto keys are permanently exposed in source history, but exploitation requires access to the codebase or ciphertext — not just network access.
ai-slop-security-theater.half-wired-crypto.crypto-keys-from-envSee full patternStoring `crypto.createHash('sha256').update(password).digest('hex')` as a password hash is not password hashing — it's a fingerprint. SHA-256 runs at ~3 billion hashes per second on a gaming GPU, making a leaked database crackable in hours for common passwords. CWE-916 (Use of Password Hash With Insufficient Computational Effort) and OWASP A02 describe the requirement: password KDFs (bcrypt, argon2, scrypt) are deliberately slow — 250ms per attempt — which makes brute-force impractical. An AI-generated auth module that uses `crypto.createHash` instead of `bcrypt.hash` is the most common credential-storage mistake in vibe-coded projects.
Why this severity: Medium because exploitation requires access to the hashed password database, but once obtained, SHA-256 or MD5 hashes can be cracked at billions of attempts per second on commodity hardware.
ai-slop-security-theater.half-wired-crypto.password-hashes-via-bcrypt-or-argon2See full patternRun this audit in your AI coding tool (Claude Code, Cursor, Bolt, etc.) and submit results here for scoring and benchmarks.
Open Security Implementation Validation Audit