The OWASP Top 10 is the industry's most-cited shortlist of web-application security risks. The 2021 edition is the current one. This page walks each category with two specific twists: what the failure mode looks like in PHP an AI assistant wrote, and which free tool catches it.

If you're using this as a checklist, work top to bottom — OWASP orders the categories by prevalence, so A01 catches more bugs than A10 in real-world audits.

  1. A01 — Broken Access Control
  2. A02 — Cryptographic Failures
  3. A03 — Injection
  4. A04 — Insecure Design
  5. A05 — Security Misconfiguration
  6. A06 — Vulnerable and Outdated Components
  7. A07 — Identification and Authentication Failures
  8. A08 — Software and Data Integrity Failures
  9. A09 — Security Logging and Monitoring Failures
  10. A10 — Server-Side Request Forgery (SSRF)

A01 — Broken Access Control

The category at the top of the list, and the one AI assistants get wrong most often: letting a user access resources or perform actions they shouldn't be authorized for.

What it looks like in AI-generated PHP: A page that checks login but doesn't check ownership. /edit_post.php?id=42 loads any post, not just the caller's. Or the page-level check is right but the AJAX parser that does the actual write has no check at all (AJAX parsers are a separate request and have to re-do every check).

// Logged in? Yes. Allowed to edit THIS post? Not asked.
$post = $db->query("SELECT * FROM posts WHERE id = ?", [$_GET['id']])->first();
if ($post) {
    $db->update('posts', $post->id, ['title' => $_POST['title']]);
}

Fix: every load and every write checks both auth (logged in) and authz (this user can do this thing to this resource). Compare the resource's owner to the session user before loading, before showing, before writing.

Tools that catch it: mostly manual review. Static analyzers struggle with "no permission check is present" because absence-of-code is hard to pattern-match. The /userspice-audit skill flags AJAX parsers missing auth re-checks — that's the largest catchable subset of this category.

A02 — Cryptographic Failures

Previously called "Sensitive Data Exposure." Covers weak crypto, plaintext storage of sensitive data, weak random sources, and missing transport encryption.

What it looks like in AI-generated PHP: md5(uniqid()) for reset tokens. mt_rand() for session IDs. Passwords stored encrypted (instead of hashed) so they can be "decrypted" for password reminders. API keys hardcoded in the source. HTTP-only deployment because the assistant didn't know to ask about TLS.

// Three different cryptographic failures in three lines:
$token = md5(uniqid(mt_rand(), true));
$encrypted_password = openssl_encrypt($pw, 'aes-256-cbc', $KEY, 0, $IV);
header('Location: http://internal.example.com/admin');

Fix: random_bytes(32) for any unguessable value. password_hash() / password_verify() for passwords — never encryption, which implies decryption, which implies a key, which implies a leak. HTTPS everywhere; HSTS to prevent downgrade.

Tools: Semgrep has rules for weak random functions. The 10 vibe-coded security holes page covers the specific bad patterns to grep for.

A03 — Injection

SQL injection, command injection, LDAP injection, XSS (yes, XSS is injection in OWASP's 2021 taxonomy). User-controlled data interpreted as code in a different context.

What it looks like in AI-generated PHP: string concatenation in SQL queries (covered in depth on the SQL injection fix page). echo $row['name'] in templates. exec($cmd) where $cmd contains user input.

// SQLi
$db->query("SELECT * FROM users WHERE email = '" . $_POST['email'] . "'");

// XSS (stored)
echo "<h1>Welcome, " . $user['name'] . "</h1>";

// Command injection
$file = $_GET['file'];
shell_exec("convert /uploads/$file.jpg /thumbs/$file.jpg");

Fix: prepared statements for SQL. htmlspecialchars() / safeReturn() for HTML output. Argv arrays via proc_open() instead of shell strings for command execution. Never blend user input into code; always pass it as data.

Tools: Semgrep and Psalm both excel at this category. ZAP catches the runtime-visible XSS variants. Free, fast, ship-blocker.

A04 — Insecure Design

The category OWASP added in 2021 to capture "the architecture is the bug." Not a missing validator, not a missing flag — an entire design that doesn't account for adversarial inputs. Hard to test for; harder to fix.

What it looks like in AI-generated PHP: a password reset flow that emails the user their current password (because the database has the password in plaintext). A "secret" admin URL that just isn't linked anywhere (security by obscurity). A "verify ownership" check that succeeds if the user knows the resource's ID. A multi-step process where step N relies on step N-1 having validated something, but step N can be invoked directly.

Fix: threat-model before you build. Who can call this endpoint? What happens if they call it out of order? What if they call it 10,000 times? Design the validators so each one stands on its own; don't trust the caller.

Tools: no automated tool catches this category well, because the bug isn't a pattern, it's the absence of a model. This is what human audits and threat modelling exist for.

A05 — Security Misconfiguration

The runtime stuff: default credentials still installed, verbose errors enabled in production, missing security headers, debug routes exposed, framework version leaked in X-Powered-By.

What it looks like in AI-generated PHP: display_errors = On in production. expose_php = On in php.ini. /debug route still live. The admin user is still admin / password. Phpinfo() page accessible. Web server returning a verbose stack trace on 500 errors.

Fix: a checklist before launch — the hardening checklist covers this category specifically. Production php.ini with display_errors = Off, log_errors = On, expose_php = Off. Web server with server_tokens off. Real passwords for real accounts.

Tools: ZAP catches missing security headers automatically. The Security Scanner's headers check is the same idea — visit the URL, list everything missing.

A06 — Vulnerable and Outdated Components

Composer dependencies with known CVEs. npm packages too, if your front-end is part of the project. The "we never updated our dependencies" category.

What it looks like in AI-generated PHP: a composer.lock that's two years old, pinning a Symfony component with a published RCE. Or a fresh project that the assistant picked dependency versions for using its training-data priors, so the versions are all from 2022.

Fix: composer audit on every CI run. composer update regularly (within reason — don't bump majors blindly). Pin to current versions; subscribe to security advisories for your big deps.

Tools: composer audit (built into Composer 2.4+) is the zero-effort version. Trivy or Snyk free for broader coverage including npm and Docker base images. See the scanner comparison for tradeoffs.

A07 — Identification and Authentication Failures

Weak passwords accepted. No rate limit on login. Session fixation. Insecure "remember me" tokens. Predictable session IDs. Username enumeration via "user not found" errors.

What it looks like in AI-generated PHP: a login endpoint that accepts any password length, has no failed-attempt tracking, returns different error messages for "user not found" vs "wrong password", and generates session IDs with PHP's default (which is fine) but never rotates them on login (which isn't).

// Several A07 failures stacked:
if (!$_POST['email'] || !$_POST['password']) die('Missing fields');
$user = $db->query("SELECT * FROM users WHERE email = ?", [$_POST['email']])->first();
if (!$user) die('User not found');                                  // enumeration
if (!password_verify($_POST['password'], $user->password_hash)) {
    die('Wrong password');                                          // enumeration
}
$_SESSION['user_id'] = $user->id;                                   // no regenerate_id
// no rate limiting anywhere

Fix: single generic error message ("invalid credentials"). Rate limit by both IP and identifier. session_regenerate_id(true) on login. Minimum password length (12+). Reject the top 10,000 most-common passwords on registration.

Tools: the /userspice-audit skill flags missing rate limits on auth-adjacent endpoints. ZAP catches some auth misconfigurations at runtime.

A08 — Software and Data Integrity Failures

Unsigned updates, unverified deserialization, dependencies fetched from untrusted sources. The category that covers "you trusted something you shouldn't have."

What it looks like in AI-generated PHP: unserialize($_COOKIE['prefs']) trusting cookie data as a PHP object. eval() on user input. A self-update mechanism that downloads code from an HTTP URL without checksum verification. Composer dependencies installed from a fork without pinning the commit hash.

// "Convenient" — and remote code execution.
$prefs = unserialize($_COOKIE['user_prefs']);

// Self-update with no integrity check.
$plugin = file_get_contents("http://updates.example.com/plugin.phar");
file_put_contents('plugin.phar', $plugin);

Fix: use JSON, not serialize(), for any data the user can influence. Sign downloads with a real signature (GPG, Sigstore). Pin dependencies to a specific commit hash for anything you can't trust to a published release.

Tools: Semgrep and Psalm both flag unserialize and eval aggressively. Trivy catches some integrity issues at the dependency layer.

A09 — Security Logging and Monitoring Failures

Critical events not logged. Logs not monitored. No alerting on auth failures, permission escalations, or suspicious patterns. The category that turns a 15-minute incident into a 6-month-undetected breach.

What it looks like in AI-generated PHP: AI assistants almost never add audit logging unless asked. Logins succeed and fail silently. Admin actions complete with no trace. Password resets happen with no record. When something goes wrong six months from now, there's nothing in the logs to triage from.

Fix: log every security-relevant event with user ID, IP, timestamp, and action. At minimum: login attempts (success and failure), password changes, admin actions, permission grants, deletions. Send to somewhere that's monitored — a centralized log system, an email alert, a Slack channel.

Tools: no scanner finds "you didn't add logging." This is on you to design in. Frameworks with audit-logging built in (UserSpice has one, Laravel has a few) remove most of the friction.

A10 — Server-Side Request Forgery (SSRF)

Your server fetches a URL the user controls. The user picks an internal URL. Your server hits it, with the privileges of being on the internal network.

What it looks like in AI-generated PHP: an avatar uploader that lets the user paste a URL ("we'll fetch your avatar from this URL"). A webhook tester. A URL preview generator. Any feature where the server makes an HTTP request based on user-supplied input.

// Looks innocent. Lets the attacker GET any URL your server can reach.
$avatar_url = $_POST['avatar_url'];
$image = file_get_contents($avatar_url);
file_put_contents("avatars/{$user_id}.jpg", $image);

// Payloads:
//   http://localhost/admin           — bypass IP allow-listing
//   http://169.254.169.254/...       — AWS metadata service
//   http://internal-db:3306          — pivot to internal services
//   file:///etc/passwd               — local file read (depending on wrapper)

Fix: whitelist the schemes (http and https only — block file://, gopher://, etc.). Resolve the hostname yourself and refuse any IP in private ranges (RFC1918, loopback, link-local). Use a HTTP client that lets you set those policies, not raw file_get_contents().

Tools: Psalm's taint analysis flags user input flowing into URL fetchers. Semgrep has rules for the common patterns. ZAP can detect some SSRF at runtime if you give it the right entrypoints.


How this maps to the toolkit

Of the ten categories above:

  • Catchable by static analysis (Semgrep + Psalm): A02, A03, A08, A10 — about 4 of the 10.
  • Catchable by dependency scanners (Trivy, composer audit): A06 alone, but it's a category that ships every week.
  • Catchable by secrets scanners (Gitleaks): part of A02.
  • Catchable by runtime scanners (ZAP): A05, parts of A03 (runtime XSS), parts of A07.
  • Catchable only by humans or AI agents reasoning about your code: A01, A04, parts of A07, A09.

The 50/50 split is the headline. Half of OWASP is mechanical pattern detection; the other half requires understanding what your application is supposed to do. That's why we recommend layering: scan for the mechanical half, audit for the conceptual half. The Security Scanner handles the mechanical half on UserSpice; the Claude Code skills handle as much of the conceptual half as can be reduced to a checklist.

Want an OWASP-aligned audit?

If you'd rather have an outside set of eyes walk your codebase against the Top 10 and send back a findings report mapped to each category, paste the repo URL below.

Need a hands-on OWASP-aligned audit of your codebase? Send the repo and we will work through each category and report what we find.

We reply within 1–2 business days.