OWASP Top 10 for AI-generated PHP
A walkthrough of the 2021 OWASP Top 10 with the AI-codegen failure mode for each category — what it looks like in code your assistant writes, and which tool catches it.
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.
- A01 — Broken Access Control
- A02 — Cryptographic Failures
- A03 — Injection
- A04 — Insecure Design
- A05 — Security Misconfiguration
- A06 — Vulnerable and Outdated Components
- A07 — Identification and Authentication Failures
- A08 — Software and Data Integrity Failures
- A09 — Security Logging and Monitoring Failures
- 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.