PHP Request Handling: Superglobals and Raw Input
Use every PHP request superglobal ($_GET, $_POST, $_SERVER, $_COOKIE, $_FILES, $_SESSION, $_ENV, $_REQUEST) and php://input correctly, and internalize the rule that all of it is untrusted until validated.
Every piece of data that arrives from outside your PHP script — a query string, a submitted form, a cookie, an uploaded file, a header — lands in one of a small set of built-in superglobal arrays. Superglobals are available in every scope (inside functions and classes, without needing global) and PHP populates them automatically at the start of each request. Knowing exactly which superglobal holds what, and how each one is shaped, is the foundation of writing PHP that talks to the web. But there's a second, more important half of this lesson: everything in these arrays was typed, clicked, uploaded, or spoofed by someone outside your program, so none of it can be trusted until you've validated and sanitized it. This lesson walks through each superglobal and the raw-input stream, and drills the 'never trust external input' principle with real validation code.
$_GET, $_POST, and $_REQUEST
$_GET holds the key/value pairs parsed from the URL's query string — everything after the ? in page.php?id=42&sort=asc. GET data is visible in the URL, gets bookmarked, gets cached by browsers and proxies, and shows up in server access logs, so it should never carry sensitive data (passwords, tokens) and is best reserved for things like pagination, filters, and search terms — idempotent, safe-to-repeat operations.
$_POST holds form data submitted with method="post" and, by default, Content-Type: application/x-www-form-urlencoded or multipart/form-data. POST bodies are not part of the URL, are not cached or bookmarked, and are the right choice for anything that changes state on the server — creating a record, logging in, deleting something — or that includes larger payloads or file uploads.
$_REQUEST is a merged array containing GET, POST, and (depending on the request_order php.ini directive, default "GP") COOKIE data, with later sources overwriting earlier ones on key collisions. Because it blurs the line between where data actually came from, most style guides — and this course — recommend avoiding $_REQUEST in real code and reading from $_GET or $_POST explicitly instead, so it's always clear which transport a value arrived over.
• $_GET — query-string parameters; visible in the URL, logs, browser history; use for idempotent reads. • $_POST — request-body form data; use for state-changing actions and file uploads. • $_REQUEST — GET + POST (+ COOKIE) merged; ambiguous origin, generally avoided in favor of explicit $_GET/$_POST. • Both are associative arrays of strings (or nested arrays for bracketed field names like tags[]) — never assume a particular type without checking.
$_SERVER, $_COOKIE, $_ENV
$_SERVER is populated by the web server and contains request metadata: $_SERVER['REQUEST_METHOD'] (GET/POST/etc.), $_SERVER['REQUEST_URI'], $_SERVER['HTTP_HOST'], $_SERVER['HTTP_USER_AGENT'], $_SERVER['REMOTE_ADDR'] (the client's IP, though this can be spoofed or be a proxy's IP), $_SERVER['QUERY_STRING'], and dozens of other keys whose exact set depends on the server (Apache, Nginx+FPM, CLI built-in server) and configuration. Header-derived keys are prefixed HTTP_ with dashes turned to underscores and uppercased, e.g. the X-Forwarded-For header becomes $_SERVER['HTTP_X_FORWARDED_FOR'] — and like any header, it is fully attacker-controlled unless your infrastructure strips/overwrites it at a trusted proxy boundary.
$_COOKIE holds the cookies the browser sent back with this request, as simple key/value strings. It only reflects what a Set-Cookie response header previously asked the browser to store (or what a client crafted by hand) — a client can send arbitrary cookie values, so cookie contents need the same distrust as any other input, especially if they influence authentication or authorization decisions.
$_ENV holds environment variables that were available to the PHP process, populated according to the variables_order ini setting (it must include E to be populated at all; many production setups disable it for performance and use getenv() instead). It's the conventional place to read configuration and secrets that were injected by the hosting environment — database URLs, API keys — rather than hardcoding them in source files, following twelve-factor-app style configuration.
• $_SERVER — request/server metadata (method, URI, headers, remote IP); most keys are attacker-influenceable via headers. • $_COOKIE — cookies sent by the browser for this request; fully client-controlled data. • $_ENV — process environment variables; typically used for server-side configuration/secrets, not user input. • getenv('NAME') is a common alternative to $_ENV that works regardless of the variables_order setting.
$_FILES and File Uploads
$_FILES is populated when a form is submitted with enctype="multipart/form-data" and contains file input fields. For a field named avatar, PHP gives you $_FILES['avatar']['name'] (the client-supplied original filename — untrusted), ['type'] (the client-supplied MIME type — also untrusted, easily spoofed), ['tmp_name'] (the real path to a temp file PHP has already saved on disk), ['error'] (an UPLOAD_ERR_* constant you must check before touching the file), and ['size'] (in bytes).
The tmp_name file is deleted automatically at the end of the request unless you move it first with move_uploaded_file(), which is the only safe way to relocate an uploaded file — it verifies the file really was created by PHP's upload mechanism (guarding against a path-traversal trick where someone tries to make you 'move' an arbitrary server file). Never trust name or type for deciding what the file actually is; instead inspect real file content (e.g. finfo_file() for the true MIME type, or re-encoding an uploaded image) and always re-generate the filename you store it under.
• $_FILES['field']['name'|'type'|'tmp_name'|'error'|'size'] — one entry per uploaded field. • Check ['error'] === UPLOAD_ERR_OK before doing anything else with a file. • Only move an upload with move_uploaded_file($tmp_name, $dest) — never copy()/rename() the tmp path directly. • Never trust the client-supplied name or type for validation or storage decisions.
$_SESSION and php://input
$_SESSION is different from the others: it isn't built from the current request's data, but from server-side storage tied to the visitor via a session ID (by default sent as a cookie, PHPSESSID). Calling session_start() at the top of a script loads that visitor's stored data into $_SESSION as a normal associative array you can read and write; PHP persists it back to storage automatically at the end of the request. Because the data lives on the server, it's more trustworthy than GET/POST/COOKIE — but the session ID that selects which bucket of data to load is still just a cookie value, so session-fixation and session-hijacking defenses (regenerating the ID on login, using Secure/HttpOnly cookies) still matter.
php://input is a read-only stream giving you the raw HTTP request body, exactly as it was sent — bytes PHP has not tried to parse into an array. This matters because $_POST is only auto-populated for application/x-www-form-urlencoded and multipart/form-data bodies; a JSON API request (Content-Type: application/json) leaves $_POST empty, and you must read and decode the body yourself: $data = json_decode(file_get_contents('php://input'), true);. php://input cannot be read for multipart/form-data requests (use $_POST/$_FILES for those) and, unlike $_POST, can only be read once per request in most SAPIs.
• $_SESSION — server-side per-visitor storage, unlocked by session_start(); more trustworthy but still keyed by a client-held cookie. • php://input — raw request body bytes; essential for reading JSON/XML API payloads that $_POST won't parse. • file_get_contents('php://input') + json_decode() is the standard pattern for a JSON PHP API endpoint. • php://input is unavailable for multipart/form-data bodies.
Never Trust External Input
Every superglobal covered above — GET, POST, SERVER (header-derived keys), COOKIE, FILES, REQUEST, and the php://input stream — originates outside your program's control, even the ones that feel 'internal' like $_SERVER. A malicious or simply buggy client can send a negative number where you expected a positive quantity, an array where you expected a string, a 50 MB payload where you expected a username, HTML/JS where you expected plain text, or SQL-looking text where you expected a search term. If your code acts on that data without checking it first, you get bugs at best and SQL injection, XSS, path traversal, or broken-access-control vulnerabilities at worst.
The discipline is: validate type and shape (is it a string? an int? within range? one of an allowed set?) before using a value for logic, and escape/encode a value for the context it is output into (HTML-escape for HTML, parameterize for SQL, never build a filesystem path directly from user text). PHP gives you filter_var() with FILTER_VALIDATE_* filters as a built-in, battle-tested way to validate common shapes (integers, email addresses, URLs, IP addresses) instead of hand-rolling regular expressions, plus filter_input() to read and validate directly from a superglobal in one step.
• Treat $_GET, $_POST, $_SERVER, $_COOKIE, $_FILES, $_ENV, $_REQUEST, and php://input as hostile by default. • Validate shape/type/range before using a value in logic; never assume a key exists — use isset()/array_key_exists() first. • Encode/escape for the output context: htmlspecialchars() for HTML, prepared statements for SQL, never raw-concatenate into a shell command or file path. • filter_var($value, FILTER_VALIDATE_INT|FILTER_VALIDATE_EMAIL|...) is the standard built-in validation tool.
Reading Superglobals Safely
isset() (or ??) guards against undefined-array-key warnings for optional fields. filter_input() returns false on failed validation and null when the key is missing — check both. json_decode with JSON_THROW_ON_ERROR throws instead of silently returning null on malformed JSON.
// Never index a superglobal directly without checking it exists
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
// filter_input() reads + validates in one step, returns false/null on failure
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
http_response_code(400);
exit('Invalid id');
}
// Reading a JSON API body via php://input
$raw = file_get_contents('php://input');
$payload = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);Code Examples
This shows the recommended shape: read GET and POST explicitly (never $_REQUEST), validate every value with filter_input() before using it, and HTML-escape anything echoed back into a page. Expected result: Subscribing: [email protected] Searching for 'laptop', page 2
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$_SERVER and $_COOKIE both carry client-influenced data (a header or a cookie value), so each is whitelisted/validated before use; $_ENV/getenv() is treated as trusted server configuration but read defensively with a fallback anyway. Whitelisting against a small set of allowed values (rather than blacklisting bad ones) is one of the most reliable validation patterns for low-cardinality inputs like themes, sort directions, or status filters.
if (!in_array($theme, $allowedThemes, true)) { $theme = 'light'; }
The upload's declared error code, size, and real (content-sniffed) MIME type are all checked before the file is moved with move_uploaded_file(), and the stored filename is server-generated rather than trusting the client's original filename.
$realType = $finfo->file($file['tmp_name']);
Common mistake: Trusting $file['type'] (attacker-controlled) instead of sniffing real content, or storing the file under $file['name'] unmodified, which enables path traversal and extension-spoofing attacks.
$_SESSION is used for a server-trusted visit counter, while the raw JSON body from php://input is decoded defensively (catching malformed JSON) and every field pulled out of it is type- and length-checked before use. Expected result: {"ok":true,"name":"Ada","visits":1}
$body = json_decode(file_get_contents('php://input'), true, flags: JSON_THROW_ON_ERROR);
<?php
declare(strict_types=1);
// URL: /search.php?q=laptop&page=2
$query = isset($_GET['q']) ? trim((string) $_GET['q']) : '';
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 1, 'min_range' => 1],
]);
// Form submitted with method="post"
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
http_response_code(422);
exit('A valid email address is required.');
}
echo "Subscribing: " . htmlspecialchars($email, ENT_QUOTES) . "\n";
}
echo "Searching for '" . htmlspecialchars($query, ENT_QUOTES) . "', page {$page}\n";
// $_REQUEST is avoided here on purpose: we always know exactly which
// transport (GET vs POST) each value came from.<?php
declare(strict_types=1);
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
// Cookie set by a previous response (see the Cookies lesson) — still
// client-supplied, so treat it as an untrusted hint, not a fact.
$theme = $_COOKIE['theme'] ?? 'light';
$allowedThemes = ['light', 'dark'];
if (!in_array($theme, $allowedThemes, true)) {
$theme = 'light'; // fall back rather than trusting an unknown value
}
// Server-side config, not user input — but still validated defensively.
$dbHost = getenv('DB_HOST') ?: ($_ENV['DB_HOST'] ?? '127.0.0.1');
echo "{$method} {$path} from {$ua}, theme={$theme}, db={$dbHost}\n";<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
if ($file['error'] !== UPLOAD_ERR_OK) {
exit('Upload failed with error code ' . $file['error']);
}
$maxBytes = 2 * 1024 * 1024; // 2 MB
if ($file['size'] > $maxBytes) {
exit('File too large.');
}
// Never trust ['type'] or ['name'] — inspect the actual bytes.
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realType = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (!isset($allowed[$realType])) {
exit('Only JPEG or PNG images are allowed.');
}
// Generate our own filename; never reuse the client-supplied name.
$destination = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.' . $allowed[$realType];
if (!move_uploaded_file($file['tmp_name'], $destination)) {
exit('Could not save the uploaded file.');
}
echo "Saved avatar to " . basename($destination) . "\n";
}<?php
declare(strict_types=1);
session_start();
// Server-side counter, keyed by this visitor's session — trustworthy
// because it lives on the server, not in a value the client sent.
$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
if ($_SERVER['REQUEST_METHOD'] === 'POST'
&& str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'application/json')) {
try {
$body = json_decode(
file_get_contents('php://input'),
true,
flags: JSON_THROW_ON_ERROR
);
} catch (JsonException) {
http_response_code(400);
exit(json_encode(['error' => 'Malformed JSON body']));
}
$name = is_string($body['name'] ?? null) ? trim($body['name']) : '';
if ($name === '' || mb_strlen($name) > 100) {
http_response_code(422);
exit(json_encode(['error' => 'name must be 1-100 characters']));
}
echo json_encode(['ok' => true, 'name' => $name, 'visits' => $_SESSION['visits']]);
}Where Each Superglobal Comes From
Only $_SESSION's payload and $_ENV originate on the server; the rest is client-controlled input.
Browser/Client PHP Superglobals
---------------- ----------------------
URL query string ────────────────▶ $_GET
Form body (POST) ────────────────▶ $_POST, $_FILES (multipart)
Raw request body ────────────────▶ php://input (JSON/XML/etc.)
Cookie header ────────────────▶ $_COOKIE
Request line/headers ────────────────▶ $_SERVER
Session cookie ID ─▶ [server storage] ─▶ $_SESSION (data lives server-side)
Host process env ────────────────▶ $_ENV / getenv()
$_GET + $_POST (+cookie) ─▶ $_REQUEST (ambiguous, avoid)
Everything above the dashed line is client-influenced → validate before use.Important Rules
Never index a superglobal without checking the key exists (isset(), ??, or filter_input()).
Prefer explicit $_GET/$_POST over $_REQUEST so the data's transport is always unambiguous.
Validate type/shape/range before using a value in logic; whitelist against known-good values when possible.
Escape output for its context: htmlspecialchars() for HTML, prepared statements for SQL, never string-build shell commands or file paths from input.
For file uploads, check ['error'], enforce a size limit, sniff real content type, and move with move_uploaded_file() only.
Use php://input (not $_POST) to read JSON/XML API request bodies; $_POST is only auto-populated for form-encoded and multipart bodies.
APIs, Forms, and the Same Discipline
A typical modern PHP application reads $_GET for filters and pagination on list pages, $_POST plus $_FILES for classic HTML form submissions (profile edits, uploads), php://input for a JSON REST API consumed by a JS frontend or mobile app, $_SESSION to remember who's logged in between requests, and $_SERVER to log request metadata or make routing decisions. Frameworks like Laravel and Symfony wrap all of this in a Request object with validation rules attached, but under the hood it is still these same superglobals and the same 'never trust it' discipline — the framework just makes the validation step harder to accidentally skip.
$name = $_POST['name']; mysqli_query($conn, "SELECT * FROM users WHERE name = '$name'");
$name = trim((string) ($_POST['name'] ?? '')); $stmt = $conn->prepare('SELECT * FROM users WHERE name = ?'); $stmt->bind_param('s', $name); $stmt->execute();
Concatenating raw $_POST data straight into a SQL string is a classic SQL-injection hole; a prepared statement with a bound parameter treats the value strictly as data, never as executable SQL syntax, no matter what the attacker submits.
$isAdmin = $_COOKIE['is_admin'] === 'true';
// Authorization must come from server-side session/DB state, never a client-editable cookie. session_start(); $isAdmin = ($_SESSION['user']['role'] ?? null) === 'admin';
Any cookie value can be edited by the client in their browser's dev tools; deciding authorization from $_COOKIE lets anyone grant themselves admin access by editing a cookie. Authorization decisions must be based on server-side state ($_SESSION backed by a database check), never client-supplied values.
[multiple-choice] Which superglobal should you use to read a JSON body sent with Content-Type: application/json? A. $_POST, because PHP always parses request bodies into it B. $_REQUEST, since it merges every possible input source C. php://input, read with file_get_contents() and then json_decode()d D. $_SERVER['BODY']
C. php://input, read with file_get_contents() and then json_decode()d — $_POST is only auto-populated for application/x-www-form-urlencoded and multipart/form-data bodies. A JSON body must be read manually from the php://input stream and decoded yourself.
[multiple-choice] Why is deciding a user's admin status from $_COOKIE['is_admin'] dangerous? A. Cookies expire too quickly to be reliable B. Cookie values are fully editable by the client, so anyone could set is_admin=true themselves C. $_COOKIE cannot store boolean-like strings D. It would only work over HTTPS
B. Cookie values are fully editable by the client, so anyone could set is_admin=true themselves — Cookies are client-held and client-editable data. Any authorization decision must be derived from server-side state (a session tied to a verified login, checked against the database), never trusted directly from a cookie's contents.
[true-false] True or false: $_FILES['field']['type'] can be trusted to identify the real file type of an upload. A. True B. False
B. False — ['type'] is the MIME type the client's browser claimed, and it is trivial to spoof. The real type must be checked server-side, e.g. with finfo_file() on the temp file's actual content.
Write a small PHP script for a POST-only 'rename item' JSON endpoint: read the JSON body from php://input, require a string field 'name' between 1 and 60 characters (respond with HTTP 422 and a JSON error otherwise), require an integer field 'id' via filter (respond 422 if missing/invalid), and on success echo back a JSON object {"ok": true, "id": ..., "name": ...}. Reject non-POST requests with HTTP 405.
<?php declare(strict_types=1); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit(json_encode(['error' => 'Method not allowed'])); } $body = json_decode(file_get_contents('php://input'), true) ?? []; $id = filter_var($body['id'] ?? null, FILTER_VALIDATE_INT); if ($id === false) { http_response_code(422); exit(json_encode(['error' => 'id must be an integer'])); } $name = is_string($body['name'] ?? null) ? trim($body['name']) : ''; if ($name === '' || mb_strlen($name) > 60) { http_response_code(422); exit(json_encode(['error' => 'name must be 1-60 characters'])); } echo json_encode(['ok' => true, 'id' => $id, 'name' => $name]);
Use $_SERVER['REQUEST_METHOD'], file_get_contents('php://input') + json_decode(), and check gettype()/is_string()/is_int() (or filter_var with FILTER_VALIDATE_INT) on the decoded values before trusting them.