This is the PHP-specific layer on top of auditing-code-for-vulnerabilities. Take the methodology from that skill — context → attack surface → bug-class hunt → variant analysis → the four-question verification gate — and apply the PHP sink catalog below to it. This skill exists because PHP has footguns no language-agnostic methodology carries: features that turn a file read into remote code execution, and comparison rules that turn == into an auth bypass.
The collection deliberately has no per-language audit skills except this one. PHP earns the exception because it dominates the legacy-web and WordPress/Magento space where critical bugs actually live, and because its dynamic features fail in ways a reviewer must be primed for specifically.
When to Use
- Reviewing a PHP codebase or plugin for exploitable vulnerabilities
- Auditing a WordPress, Magento, Joomla, or Laravel application's source
- Tracing a suspected RCE, LFI, SQLi, or object-injection path in PHP
- Deciding whether a
unserialize,include, or==on user input is exploitable
When NOT to Use
- General audit methodology, or a non-PHP language — use
auditing-code-for-vulnerabilities; this skill assumes it
- Finding a planted webshell or backdoor rather than a vulnerability — use
- Building the deserialization exploit chain once you have the sink — use
- Black-box testing a running app — use
testing-web-applications - Reviewing only the diff of a change — use
reviewing-code-changes
Sinks by Bug Class
Trace user input (superglobals, headers, uploaded filenames, DB values that were once user input) to each of these. A sink is only a bug when a source reaches it.
Remote code execution
- Direct eval sinks:
eval,assert(string arg, pre-8.0),
preg_replace with /e (pre-7.0), create_function (pre-8.0), mb_ereg_replace/mb_eregi_replace with the e option.
- Command execution:
system,exec,passthru,shell_exec, backticks,
proc_open, popen, pcntl_exec. Watch escapeshellarg vs escapeshellcmd misuse — escapeshellcmd does not prevent argument injection.
- Dynamic dispatch:
$func()where$funcderives from input,
call_user_func/call_user_func_array, array_map/usort/ preg_replace_callback with a user-influenced callback.
Object injection (the PHP-defining bug)
unserialize()on any user-controlled data is object injection. Exploitation
is a POP chain: existing classes with __wakeup, __destruct, __toString, or __call magic methods that do something dangerous when the object is materialized. The vulnerable file often contains no dangerous code at all — the gadget lives in a framework or dependency.
phar://is unserialize in disguise — but version-gated. On **PHP
before 8.0, any file operation (file_exists, fopen, include, getimagesize, …) on a phar:// path automatically deserializes the Phar's metadata, so a path-traversal or LFI that points a file function at an uploaded .phar (or any file carrying Phar metadata) is object injection with no unserialize call in sight. PHP 8.0 removed that automatic unserialize** — only explicit Phar::getMetadata() deserializes now. So the bug is critical on the huge installed base of PHP 7.x and a non-issue on 8.x unless the code calls getMetadata() itself. Check the runtime version before ruling it in or out.
- Prefer
json_decodein remediation; flag everyunserializeon non-trusted
data regardless of an obvious gadget — the gadget may arrive with the next dependency update.
LFI / RFI and stream wrappers
include,require,include_once,require_oncewith user input.- Wrappers that change everything:
php://filter/...(read source, and a
known RCE chain via convert.iconv/compression filters), php://input (RFI-style body inclusion), data:// (inline payload), phar:// (object injection), expect://, zip://. allow_url_include being on turns LFI into remote RFI.
- File-read sinks (
file_get_contents,readfile,fopen,highlight_file)
with traversal give source and secret disclosure.
SQL injection
- Legacy
mysql_query/mysqli_querywith string-concatenated input. $wpdb->query("... $var ...")without$wpdb->prepare()— a->prepare
that is present but interpolates outside its placeholders is still injectable.
- PDO/mysqli used with string building instead of bound parameters. Placeholder
usage on the table/column name (which cannot be bound) is a common real hole.
Type juggling and magic-hash auth bypass
==and!=are loose.strcmp/hash_equals/===are the safe forms.
The classic bypass: if (md5($input) == $stored) where two inputs both hash to a 0e[0-9]+ "magic hash" compare equal, because two numeric strings compare as numbers (0e123 == 0e456 → 0.0 == 0.0).
- Version matters and it is a trap. In PHP 8.0 the number-vs-string rules
changed: 0 == "foo" was true in PHP 7 and is false in PHP 8, so a in_array(0, $userStrings) or 0 == $token bypass that worked on PHP 7 fails on PHP 8. But the 0e magic-hash bypass survives PHP 8, because both sides are numeric strings and still compare as numbers. Check the target PHP version before ruling a juggling bug in or out.
strcmp($_GET['x'], $secret)returnedNULL(loosely== 0) when passed an
array in PHP 7 — an auth bypass; in PHP 8 it throws a TypeError. Same code, different verdict by version.
Superglobal trust and variable variables
extract($_GET/$_POST/$_REQUEST)lets a request set arbitrary local
variables — overwrites auth flags, config, anything not yet initialized.
$$var/${$key}variable variables,parse_strwithout a result array
(writes into scope), import_request_variables (removed 5.4) — all register_globals-flavoured variable injection.
SSRF and file upload
file_get_contents/curl/fopenon a user-supplied URL is SSRF — route the
exploitation reasoning through exploiting-ssrf.
- Uploads: check the destination is outside the web root, the extension is not
attacker-chosen (double extensions, .phtml, .php5, null bytes on old PHP), and the server will not execute the type. A validated MIME with an attacker-controlled extension in a web-served directory is RCE.
Ecosystem Context
- WordPress: nonces are CSRF protection, not authorization — a valid
nonce does not mean the user is allowed to act; look for the missing current_user_can() check. $wpdb->prepare is mandatory for interpolation. unfiltered_html, edit_* capabilities, and unauthenticated AJAX/REST (wp_ajax_nopriv_*, permission_callback => '__return_true') are the usual holes.
- Magento: heavy
unserializehistory, layout-XML injection, and the admin
path/patch level matter for known-CVE reachability.
- Laravel: mass assignment (
$fillable/$guarded), Blade{!! !!}
(unescaped) vs {{ }} (escaped), and a leaked APP_KEY enabling cookie/decrypt object injection.
Verify Before You Report
Apply auditing-code-for-vulnerabilities' gate: trace the source to the sink, confirm the path is reachable (auth, routing, and — critically — the PHP version for juggling and removed-function bugs), and demonstrate impact rather than asserting it. A unserialize with no reachable gadget, or a /e regex on PHP 8, is a hardening note, not a critical finding — say which.
Rationalizations to Reject
- "There's no
unserialize, so no object injection."phar://reaches
unserialize through ordinary file functions. Check every file operation whose path a user can influence.
- "It uses
==, that's fine for a hash check."==on hashes is the
magic-hash bypass. Only hash_equals/=== are safe, and the bypass survives PHP 8.
- "The comparison bug works, I confirmed it." On which PHP version?
0 == "x"
and strcmp(array) flipped verdict at PHP 8.0. State the version.
- "
prepare()is called, so the query is safe." Only for values inside
placeholders. Interpolated table/column names and text spliced around the placeholders are still injectable.
- "The nonce check passes, so it's authorized." WordPress nonces are CSRF
tokens. Authorization is a separate current_user_can() you must find.
- "The upload validates the MIME type." MIME is spoofable and irrelevant if
the attacker controls the extension and the directory executes PHP.
- "It's a framework class, the gadget isn't here." POP-chain gadgets live in
dependencies by design. The absence of dangerous code in the vulnerable file is normal, not exculpatory.
References
auditing-code-for-vulnerabilities— the general methodology and gate this
skill sits on top of
hunting-web-backdoors— planted malice rather than vulnerabilitiesexploiting-deserialization— building the POP chain once you find the
unserialize/phar:// sink
exploiting-ssrf— the exploitation side of a user-controlled URL fetchtesting-web-applications— black-box testing of the running application