Advanced Web Security: A PHP Threat Catalog
Build a working map of the web application threat landscape beyond SQL injection and session attacks -- SSRF, command injection, RCE, path traversal, insecure deserialization, file upload attacks, authentication/authorization flaws, request smuggling, and the security-header/operational controls (CSP, HSTS, CORS, SameSite, rate limiting, secret management, dependency risk, safe logging) that defend against them.
Security in a real PHP application is rarely one big vulnerability -- it's a catalog of smaller ones, each with its own attack shape and its own fix. Two of the biggest, SQL injection and session hijacking/fixation, already have dedicated lessons elsewhere in this course, so this lesson deliberately keeps those brief and focuses its depth on five threats that tend to be under-covered: server-side request forgery (SSRF), command injection, remote code execution (RCE), path traversal, and insecure deserialization. It then works through the rest of the landscape -- file uploads, auth/authz flaws, request smuggling, security headers, and the operational controls (rate limiting, secret management, dependency risk, safe logging) every production app needs regardless of which specific vulnerability class is in play. Every attack pattern shown below is clearly labeled illustrative and immediately paired with its fix -- none of it is a working exploit.
Already Covered in Depth Elsewhere
SQL injection -- concatenating untrusted input directly into a SQL query, letting an attacker change the query's meaning -- is covered in full depth, including PDO prepared statements and the trickier case of dynamic identifiers, in the 'SQL Injection' module. The short version: never build SQL by concatenating untrusted strings; always use bound parameters.
Session fixation and hijacking -- an attacker fixing a known session ID on a victim, or stealing a session ID and reusing it -- are covered in depth in the Sessions module (session_regenerate_id(), cookie hardening, distributed storage) and this course's second Sessions lesson (locking, custom handlers, stateless alternatives). The short version: regenerate the session ID on login and on every privilege escalation, and set Secure/HttpOnly/SameSite on the session cookie.
• SQL Injection -- see the dedicated 'SQL Injection' module for prepared statements, dynamic identifiers, and least-privileged DB users. • Session fixation/hijacking -- see the Sessions module(s) for session_regenerate_id(), cookie hardening, and locking/custom handlers.
Server-Side Request Forgery (SSRF)
SSRF happens when an application fetches a URL that's fully or partially controlled by the user, and the attacker points it somewhere the server can reach but the attacker never could directly. A very ordinary-looking feature -- 'paste an image URL and we'll fetch it for you,' a webhook tester, a PDF-from-URL generator -- becomes a way to make the SERVER itself issue requests on the attacker's behalf. The classic payoff is reaching internal-only infrastructure: cloud providers commonly expose an instance metadata service at a fixed internal address (e.g. http://169.254.169.254/) that hands back temporary cloud credentials to anything running ON the instance -- if an app blindly fetches whatever URL a user supplies, an attacker can simply supply that internal address and have the server fetch its own cloud credentials back to them. The same technique reaches internal admin panels, databases, and other services that were never meant to be reachable from outside the private network at all.
The fix is defense in depth, not a single check: validate the URL's scheme (allow only http/https, never file://, gopher://, etc.), resolve the hostname and reject anything that resolves to a private, loopback, or link-local IP range (10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, and so on) -- and re-check the resolved IP at connection time, not just at validation time, since DNS can be made to answer differently a moment later (a 'time-of-check to time-of-use' gap). Disable automatic redirect-following, or re-validate the destination after every redirect, since an attacker can point an allowed URL at a redirect that lands on an internal address. Where possible, prefer an explicit allowlist of permitted hosts over a denylist of forbidden ones.
• SSRF: the app fetches an attacker-influenced URL, and the SERVER's network position (not the attacker's) determines what's reachable. • Classic target: cloud instance metadata endpoints, which hand back credentials to anything running on the instance. • Mitigations: restrict URL schemes, resolve and reject private/loopback/link-local IP ranges, re-check the IP at connection time (not just validation time), disable or re-validate redirects, prefer an allowlist of hosts.
Command Injection and Remote Code Execution
Command injection happens when untrusted input reaches a shell -- via exec(), shell_exec(), system(), passthru(), or backticks -- and the input contains shell metacharacters (;, |, &&, $(...)) that change what command actually runs, exactly the same structural flaw as SQL injection but against a shell interpreter instead of a database. Remote code execution (RCE) is the broader, more severe category this often leads directly into: any path by which an attacker gets arbitrary code to run on the server, which also includes dangerous PHP constructs like eval() on untrusted input, include()/require() on an attacker-influenced path (local/remote file inclusion), or unserialize() on untrusted data triggering a gadget chain (covered below).
The safest fix for command injection is architectural: avoid shelling out at all when a native PHP library can do the same job (e.g. the Imagick or GD extensions instead of shelling out to ImageMagick's convert). When a shell command is genuinely unavoidable, never build it by string concatenation -- pass arguments as an array to proc_open(), or, at minimum, wrap every piece of untrusted input individually with escapeshellarg() (not escapeshellcmd(), which is not a substitute for properly escaping each argument). For eval() and dynamic include paths: there is essentially never a legitimate reason to eval() user input, and any include()/require() whose path is influenced by user input must be checked against a strict allowlist of known-good filenames, never passed through directly.
• Command injection: untrusted input reaches a shell and its metacharacters change what command actually executes. • RCE is the broader category -- command injection, eval() on untrusted input, and unsafe include()/require() paths (LFI/RFI) all lead here. • Best fix: avoid the shell entirely when a native PHP library can do the same job. • If a shell is unavoidable: pass arguments as an array to proc_open(), or escapeshellarg() every piece of untrusted input individually.
Path Traversal and File Upload Attacks
Path traversal happens when a filename or path built from user input is used directly in a filesystem operation without confining it to an intended directory -- input like ../../etc/passwd (or its URL-encoded/obfuscated variants) walks the resulting path outside the directory the developer intended, reading (or, worse, writing) files far outside where the application meant to operate. The fix is to never trust a user-supplied path directly: strip it down to just the basename (basename($input)), resolve the final path with realpath(), and explicitly verify the resolved path still starts with the intended base directory before using it -- rejecting anything that doesn't.
File upload attacks compound this: an attacker uploads a file whose CONTENT is executable (a PHP script disguised with an image extension, or genuinely executable content) into a location the web server will later execute as code, or uses the upload mechanism itself to smuggle a path-traversal payload into the destination filename. Defenses layer together -- validate the file's actual content type with something like finfo (never trust the client-supplied MIME type or the extension alone, both are trivially attacker-controlled), generate a new random filename rather than trusting the uploaded one, store uploads OUTSIDE the web root (or in a location explicitly configured to never execute scripts), and re-encode images through a library like GD/Imagick, which incidentally strips most attempts to smuggle executable content inside an otherwise valid image file.
• Path traversal: an unconfined user-supplied path walks outside the intended directory (../../...). • Fix: basename() the input, realpath() to resolve it, then verify the result still starts with the intended base directory. • File upload risk: attacker-controlled content executed later, or a traversal payload smuggled into the filename. • Fix: verify content-type via finfo (not the client-supplied MIME type/extension), randomize the stored filename, store outside the web root, re-encode images through GD/Imagick.
Insecure Deserialization
PHP's unserialize() reconstructs full PHP objects from a serialized string -- including invoking __wakeup(), __destruct(), and other magic methods on the reconstructed object automatically. If unserialize() is ever called on data an attacker controls (a cookie, a query parameter, a cached value an attacker could influence), a sufficiently motivated attacker can craft a serialized string that, once reconstructed, invokes a chain of existing classes' magic methods in a sequence the developer never intended -- a 'gadget chain' -- potentially leading to file deletion, SQL execution, or full RCE, entirely depending on which classes happen to be autoloadable in the application.
The fix is almost always to not use PHP's native serialization format for anything crossing a trust boundary at all: prefer json_encode()/json_decode() for data exchanged with the client, since JSON has no equivalent of PHP's magic-method-invoking object reconstruction -- decoding JSON can produce arrays or plain stdClass objects, never an instance of an arbitrary application class with __wakeup()/__destruct() side effects. If unserialize() of untrusted-ish data is genuinely unavoidable, PHP's unserialize($data, ['allowed_classes' => false]) (or an explicit allowlist of safe classes) prevents it from instantiating anything beyond plain arrays/scalars, closing off the gadget-chain path entirely.
• unserialize() reconstructs full objects and invokes their magic methods (__wakeup, __destruct) automatically. • A 'gadget chain': attacker-crafted serialized data that, once reconstructed, chains existing classes' magic methods toward an unintended effect. • Fix: use json_encode()/json_decode() for anything crossing a trust boundary -- JSON cannot reconstruct arbitrary objects. • If unserialize() of untrusted data is unavoidable, pass ['allowed_classes' => false] (or an explicit allowlist) to block object instantiation entirely.
Authentication, Authorization, and Session Attacks
Authentication flaws (proving who you are) include weak or missing rate limiting on login attempts (enabling credential stuffing/brute force), storing passwords with a fast or reversible hash instead of password_hash()'s bcrypt/Argon2, and password-reset flows that leak whether an email exists or use a guessable/non-expiring reset token. Authorization flaws (proving what you're allowed to do, once already authenticated) are a distinct category and arguably more common in practice -- the textbook example is an Insecure Direct Object Reference (IDOR): an endpoint like /invoices/1042 that checks the user is LOGGED IN but never checks that invoice 1042 actually belongs to them, letting any authenticated user view or modify any other user's data just by changing the ID in the URL.
Session-specific attacks (fixation, hijacking, cookie theft) are covered in depth in the Sessions module(s) already referenced above -- the core discipline that carries over here is: authenticate on every request that needs it, and separately, explicitly authorize every object access against the currently authenticated user rather than assuming 'logged in' implies 'allowed to see this specific record.'
• Authentication flaws: weak rate limiting on login, weak password hashing, leaky/guessable password-reset flows. • Authorization flaws (IDOR): checking that a user is LOGGED IN is not the same as checking they're ALLOWED to access this specific record. • Fix for IDOR: every object-access check must include an explicit ownership/permission check against the authenticated user, not just an authentication check.
HTTP Request Smuggling (Awareness Level)
Request smuggling exploits a disagreement between a front-end proxy/load balancer and a back-end server about where one HTTP request ends and the next begins -- typically by sending both a Content-Length header and a Transfer-Encoding: chunked header in the same request, where the two servers in the chain interpret which one 'wins' differently. A carefully crafted request can, as a result, get its trailing bytes interpreted by the back-end server as the START of the NEXT unrelated request from a different user sharing that connection, letting an attacker inject request data into someone else's response or bypass a front-end security control that never saw the smuggled part.
This is primarily an infrastructure-configuration concern rather than something application code fixes directly -- keeping the front-end proxy and back-end server's HTTP parsing behavior consistent (ideally both terminating on HTTP/2 internally, where this ambiguity doesn't exist the same way), and keeping both pieces of infrastructure patched, are the standard mitigations. Application-level awareness matters mainly so you know to ask your infrastructure team whether the reverse proxy in front of your PHP app has been evaluated for it.
• Root cause: front-end and back-end disagree about where one request ends and the next begins (commonly a Content-Length vs. Transfer-Encoding conflict). • Impact: one user's request data can smuggle into another user's response/request stream. • Primarily an infrastructure concern -- consistent HTTP parsing across the proxy chain (or internal HTTP/2) is the standard mitigation.
Security Headers, CSP, HSTS, CORS, and SameSite
A handful of response headers act as browser-enforced guardrails: Content-Security-Policy (CSP) restricts which sources scripts, styles, and other resources may be loaded from, meaningfully limiting what an XSS payload can even DO if one slips through (e.g. blocking it from loading an external script). Strict-Transport-Security (HSTS) tells the browser to only ever connect to this site over HTTPS for a given duration, closing off downgrade attacks after the first visit. CORS (Access-Control-Allow-Origin and friends) controls which OTHER origins a browser will let JavaScript read a cross-origin response from -- it protects users of OTHER sites from a malicious page reading your API's responses on their behalf, not your site from attack directly. SameSite on cookies (covered from the session angle in the Sessions module) restricts when a cookie rides along on a cross-site request, which is a major structural defense against CSRF specifically.
The full header-by-header mechanics live in the 'HTTP Headers' module; the point here is architectural -- these headers are cheap, broad-spectrum mitigations that reduce the BLAST RADIUS of a vulnerability elsewhere (an XSS bug, a CSRF-able endpoint) even when they don't prevent the underlying bug from existing. They belong on every response, not just ones that seem 'security sensitive.'
• CSP: limits what an XSS payload can load/execute, even if one gets injected. • HSTS: forces HTTPS-only connections after the first visit, closing off downgrade attacks. • CORS: protects OTHER sites' users from a malicious page reading your API on their behalf -- not your site from direct attack. • SameSite: restricts when a cookie rides along cross-site, a major structural CSRF defense.
Rate Limiting, Secret Management, Dependency Risk, and Safe Logging
Rate limiting throttles how many requests a client can make in a time window -- essential on login, password-reset, and any expensive or abusable endpoint, both to blunt brute-force/credential-stuffing attempts and to protect the server from being overwhelmed. Secret management means database passwords, API keys, and signing secrets live in environment variables or a dedicated secrets manager -- never committed to version control, never hard-coded in a file that might end up in a public repository or an error message.
Dependency/supply-chain risk is the reminder that most of a modern PHP app's code is code YOU didn't write -- a vulnerable Composer package (or one that's been compromised upstream) is now part of your attack surface exactly as much as your own code is; composer audit and keeping dependencies patched are baseline hygiene, not optional extras. Finally, safe logging means logging enough to debug and detect incidents (who did what, when, from where) while never logging secrets themselves -- passwords, full credit card numbers, session IDs, API keys, or full request bodies that might contain them -- since a log file is itself a target, and a leaked log is functionally the same breach as a leaked database.
• Rate limiting: essential on login, password reset, and any expensive/abusable endpoint. • Secret management: secrets live in env vars/a secrets manager, never in version control or hard-coded. • Dependency risk: a vulnerable or compromised Composer package is part of your attack surface -- run composer audit, keep dependencies patched. • Safe logging: log enough to investigate an incident, but never log secrets/passwords/full card numbers/session IDs -- a leaked log is a breach too.
Code Examples
The unsafe version fetches any URL as-is -- an attacker who controls $url can point it at an internal-only address the server can reach but they can't. The safe version restricts the scheme, resolves the hostname, and explicitly rejects private/reserved IP ranges (FILTER_FLAG_NO_PRIV_RANGE/FILTER_FLAG_NO_RES_RANGE) before ever making the request, and disables automatic redirect-following so an allowed URL can't silently redirect to a disallowed one.
Common mistake: Validating only the URL's HOSTNAME string (e.g. checking it 'looks like' a public domain) instead of resolving it and checking the actual IP address -- a hostname can resolve to a private IP, and DNS answers can change between the check and the actual request.
The unsafe version lets shell metacharacters in the filename change what command actually runs. Option 1 removes the shell from the picture entirely by using a native PHP extension for the same job -- the strongest fix. Option 2 shows the fallback when a shell command is genuinely unavoidable: escapeshellarg() wraps the untrusted value so it can only ever be interpreted as a single literal argument, never as additional shell syntax.
Common mistake: Reaching for escapeshellcmd() and assuming it makes concatenated input safe -- escapeshellcmd() escapes shell metacharacters in the whole command STRING, it is not a substitute for wrapping each untrusted ARGUMENT individually with escapeshellarg().
basename() removes any directory-traversal components from the supplied name, and realpath() resolves the FINAL actual path (following symlinks, collapsing '..' segments) so it can be checked against the real base directory -- rejecting the read entirely if the resolved path ever lands outside where it's supposed to.
json_decode() has no mechanism to construct an arbitrary application object or invoke its magic methods, which is exactly why it's immune to this attack class -- it can only ever produce arrays or plain stdClass values. Where unserialize() truly can't be avoided, ['allowed_classes' => false] blocks it from instantiating any class at all, closing off the gadget-chain path while still allowing arrays/scalars through.
Common mistake: Calling unserialize() on any value that crosses a trust boundary (a cookie, a cache entry an attacker could poison, a query parameter) without the allowed_classes restriction -- the vulnerability doesn't require any bug in YOUR code, only that some autoloadable class in the application happens to have an exploitable magic method.
The unsafe version trusts the attacker-supplied filename outright, which can smuggle an executable script into the web root under a misleading name. The safe version determines the file's TYPE by inspecting its actual bytes (finfo), generates a fresh random filename instead of trusting the original, and stores it outside the web root entirely so even an unexpected file type can never be directly executed by a request.
<?php
// -- ILLUSTRATIVE, VULNERABLE PATTERN -- DO NOT USE --
// Fetches whatever URL the user supplies, with no validation at all.
// An attacker can supply an internal address (e.g. the cloud metadata
// endpoint, or an internal admin panel) and the SERVER will fetch it.
function fetchPreviewUnsafe(string $url): string
{
return file_get_contents($url); // DANGER: no scheme/host/IP validation
}
// -- SECURE FIX --
function fetchPreviewSafe(string $url): string|false
{
$parts = parse_url($url);
if (!in_array($parts['scheme'] ?? '', ['http', 'https'], true)) {
return false;
}
$ip = gethostbyname($parts['host'] ?? '');
if ($ip === ($parts['host'] ?? '') || !filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
)) {
return false; // resolution failed, or landed on a private/reserved IP
}
$ctx = stream_context_create([
'http' => ['timeout' => 3, 'follow_location' => 0], // no auto-redirects
]);
return file_get_contents($url, false, $ctx);
}<?php
// -- ILLUSTRATIVE, VULNERABLE PATTERN -- DO NOT USE --
// Builds a shell command by concatenating unescaped user input.
function convertImageUnsafe(string $userFilename): void
{
// DANGER: a filename like "a.jpg; rm -rf /" changes what actually runs.
shell_exec("convert {$userFilename} output.png");
}
// -- SECURE FIX (option 1: avoid the shell entirely) --
function convertImageSafe(string $userFilename): void
{
$image = new Imagick($userFilename); // native extension, no shell involved
$image->setImageFormat('png');
$image->writeImage('output.png');
}
// -- SECURE FIX (option 2: if a shell is truly unavoidable) --
function convertImageShellButSafe(string $userFilename): void
{
$cmd = sprintf('convert %s output.png', escapeshellarg($userFilename));
shell_exec($cmd); // every piece of untrusted input individually escaped
}<?php
$baseDir = '/var/app/user_files';
// -- ILLUSTRATIVE, VULNERABLE PATTERN -- DO NOT USE --
// Uses the user-supplied filename directly in a filesystem path.
function readUserFileUnsafe(string $baseDir, string $userFilename): string|false
{
// DANGER: "../../../../etc/passwd" walks straight out of $baseDir.
return file_get_contents($baseDir . '/' . $userFilename);
}
// -- SECURE FIX --
function readUserFileSafe(string $baseDir, string $userFilename): string|false
{
$safeName = basename($userFilename); // strip any directory components
$fullPath = realpath($baseDir . '/' . $safeName);
// Reject anything that resolves outside the intended base directory.
if ($fullPath === false || !str_starts_with($fullPath, realpath($baseDir) . DIRECTORY_SEPARATOR)) {
return false;
}
return file_get_contents($fullPath);
}<?php
// -- ILLUSTRATIVE, VULNERABLE PATTERN -- DO NOT USE --
// Restores application state directly from a user-supplied cookie value.
function loadPreferencesUnsafe(string $cookieValue): mixed
{
// DANGER: unserialize() on attacker-controlled data can instantiate
// ANY autoloadable class and invoke its __wakeup()/__destruct(), which
// is the basis of PHP object-injection "gadget chain" attacks.
return unserialize($cookieValue);
}
// -- SECURE FIX (preferred: don't use PHP's native format at all) --
function loadPreferencesSafe(string $cookieValue): mixed
{
return json_decode($cookieValue, true); // arrays only -- no object reconstruction
}
// -- SECURE FIX (if unserialize() is unavoidable for legacy reasons) --
function loadLegacyPreferencesSafer(string $cookieValue): mixed
{
return unserialize($cookieValue, ['allowed_classes' => false]); // arrays/scalars only
}<?php
// -- ILLUSTRATIVE, VULNERABLE PATTERN -- DO NOT USE --
// Trusts the client-supplied extension/MIME type and the original filename.
function saveUploadUnsafe(array $file): string
{
$dest = __DIR__ . '/uploads/' . $file['name']; // attacker controls this name
move_uploaded_file($file['tmp_name'], $dest);
return $dest; // e.g. could be "shell.php" saved inside the public web root
}
// -- SECURE FIX --
function saveUploadSafe(array $file): string|false
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']); // inspect actual content, not the extension
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (!isset($allowed[$mime])) {
return false;
}
$randomName = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$destDir = '/var/app/uploads_private'; // OUTSIDE the public web root
move_uploaded_file($file['tmp_name'], "{$destDir}/{$randomName}");
return $randomName;
}Important Rules
Never fetch a user-influenced URL without validating scheme, resolved IP (not just hostname), and redirect targets -- SSRF uses the SERVER's network position, not the attacker's.
Avoid shelling out at all when a native PHP library can do the same job; when a shell command is unavoidable, escapeshellarg() every untrusted argument individually.
Never trust a user-supplied path directly -- basename() it, realpath() it, and verify the resolved path still starts with the intended base directory.
Never unserialize() untrusted data without ['allowed_classes' => false] (or an explicit allowlist) -- prefer JSON for anything crossing a trust boundary.
Validate uploaded file content by actual bytes (finfo), not the client-supplied extension/MIME type, and store uploads outside the web root under a randomized filename.
Authentication (are you logged in?) and authorization (are you allowed to access THIS record?) are separate checks -- every object-access endpoint needs both.
Security headers (CSP, HSTS, CORS, SameSite) reduce the blast radius of a bug elsewhere; they belong on every response, not just 'sensitive' ones.
Where this shows up
SSRF against cloud instance metadata endpoints has been behind some of the highest-profile cloud breaches on record -- the pattern is always the same shape as the illustrative example above: a feature that fetches a user-supplied URL, with no validation that the destination isn't internal infrastructure. Insecure PHP deserialization gadget chains (often summarized under the term 'PHP Object Injection' or 'POP chains') have repeatedly been found in widely-used CMS plugins precisely because unserialize() was called on cookie or session-adjacent data without restricting which classes it could instantiate.
Fetching a user-supplied URL directly (file_get_contents($url), a raw curl call) with no validation of scheme, resolved IP, or redirect target.
Restrict the scheme to http/https, resolve the hostname and reject private/loopback/link-local IP ranges, re-check the IP at connection time, and disable or re-validate automatic redirects -- ideally against an explicit allowlist of permitted hosts.
An SSRF-vulnerable URL fetcher lets an attacker use the SERVER's network position to reach infrastructure (cloud metadata endpoints, internal admin panels) that was never meant to be reachable from outside, and a hostname-only check is not enough because DNS resolution can be manipulated or simply differ between validation time and request time.
Calling unserialize() on any data that originated from the client (a cookie, a hidden form field, a query parameter) without restricting which classes it can instantiate.
Use json_encode()/json_decode() for anything crossing a trust boundary, or, if unserialize() is unavoidable, pass ['allowed_classes' => false] to block object instantiation entirely.
unserialize() on untrusted data can be turned into a PHP object-injection gadget chain purely by the existence of exploitable magic methods somewhere in the application's autoloadable classes -- the vulnerability doesn't require a bug in the code that calls unserialize() itself.
[multiple-choice] What makes SSRF dangerous even when the attacker could never directly reach the target address themselves? A. SSRF always requires the attacker to already be on the internal network B. The vulnerable server fetches the URL using its OWN network position, which can reach internal-only infrastructure (like a cloud metadata endpoint) the attacker cannot reach directly C. SSRF only affects the attacker's own machine D. It doesn't -- SSRF is only a risk if the target is a public website
B. The vulnerable server fetches the URL using its OWN network position, which can reach internal-only infrastructure (like a cloud metadata endpoint) the attacker cannot reach directly — SSRF exploits the fact that the vulnerable server, not the attacker, makes the outbound request -- so it can reach internal-only addresses (cloud metadata services, internal admin panels) the attacker has no direct network path to.
[multiple-choice] Why is escapeshellarg() the correct tool for making a shell command safe, rather than escapeshellcmd()? A. They are interchangeable and either one is sufficient B. escapeshellarg() wraps an individual untrusted VALUE so it's treated as one literal argument; escapeshellcmd() escapes an entire command STRING and is not a substitute for escaping each argument C. escapeshellcmd() is strictly more secure and should always be preferred D. Neither function has any effect on command injection risk
B. escapeshellarg() wraps an individual untrusted VALUE so it's treated as one literal argument; escapeshellcmd() escapes an entire command STRING and is not a substitute for escaping each argument — escapeshellarg() is designed to neutralize a single untrusted value so it can only be interpreted as one literal argument; escapeshellcmd() operates on the whole command string and doesn't provide the same per-argument guarantee, so relying on it alone is a common and dangerous mistake.
[multiple-choice] What is the key confinement check that prevents a path-traversal payload from reaching a file outside the intended directory? A. Checking that the filename doesn't contain the literal string ".." B. Resolving the final path with realpath() and verifying it still starts with the realpath() of the intended base directory C. Checking only the file extension D. There is no reliable check -- path traversal cannot be prevented
B. Resolving the final path with realpath() and verifying it still starts with the realpath() of the intended base directory — String-matching for '..' is fragile against encoding tricks and symlinks; resolving the actual final path with realpath() and checking it's still inside the intended base directory catches traversal regardless of how the input was obfuscated.
[multiple-choice] Why is json_decode() immune to the PHP object-injection risk that unserialize() carries on untrusted data? A. json_decode() is simply a faster implementation of the same object-reconstruction logic B. json_decode() can only ever produce arrays or plain stdClass values -- it has no mechanism to instantiate an arbitrary application class or invoke its magic methods C. JSON data cannot be sent by an attacker D. json_decode() automatically sanitizes any dangerous content in the string
B. json_decode() can only ever produce arrays or plain stdClass values -- it has no mechanism to instantiate an arbitrary application class or invoke its magic methods — Unlike unserialize(), which can reconstruct instances of ANY autoloadable class and invoke __wakeup()/__destruct() automatically, json_decode() has no equivalent concept -- it only produces arrays or stdClass objects with no magic-method side effects, closing off the gadget-chain attack entirely.
[multiple-choice] An endpoint checks that a user is authenticated, then returns /invoices/{id} for whatever id is in the URL, without checking who owns that invoice. What category of flaw is this? A. SQL injection B. An authorization flaw (specifically an Insecure Direct Object Reference / IDOR) -- authentication was checked, but not whether this user may access this specific record C. Command injection D. HTTP request smuggling
B. An authorization flaw (specifically an Insecure Direct Object Reference / IDOR) -- authentication was checked, but not whether this user may access this specific record — This is the textbook IDOR pattern: the endpoint confirms the user is logged in (authentication) but never confirms they're allowed to access this SPECIFIC record (authorization) -- the two checks are distinct and both are required.
[multiple-choice] What is the primary defense against HTTP request smuggling, and at what layer does it typically apply? A. Application-level input validation in PHP B. Keeping the front-end proxy and back-end server's HTTP parsing behavior consistent -- an infrastructure/configuration concern more than an application-code fix C. Escaping all output with htmlspecialchars() D. Using prepared statements for every database query
B. Keeping the front-end proxy and back-end server's HTTP parsing behavior consistent -- an infrastructure/configuration concern more than an application-code fix — Request smuggling exploits a parsing DISAGREEMENT between chained HTTP infrastructure (a front-end proxy and a back-end server), so the standard mitigation is keeping that infrastructure's HTTP parsing consistent and patched, rather than something fixed purely in PHP application code.
A PHP endpoint lets a logged-in user download any of their own invoices via GET /invoice.php?id=1042, which runs file_get_contents("/var/app/invoices/" . $_GET['id'] . ".pdf") after only checking that the user is authenticated. Identify BOTH vulnerabilities present (not just one), and describe the fix for each.
(1) Path traversal: $_GET['id'] is concatenated directly into a filesystem path with no validation, so a value like '../../etc/passwd%00' (or similar) could walk outside the invoices directory -- fix with basename()/realpath() confinement as shown in this lesson's path-traversal example, and additionally validate that 'id' is strictly numeric before using it at all. (2) IDOR / missing authorization: the check only confirms the user is LOGGED IN, not that invoice 1042 actually BELONGS to them -- fix by looking up the invoice's owner and confirming it matches the authenticated user's ID before returning the file, exactly as described in this lesson's authorization section.
One vulnerability is about the shape of the FILE PATH itself; the other is about what's being checked (or not checked) before the file is returned.