Lesson 10 of 20

Superglobals

The Arrays PHP Fills In For You

Superglobals are arrays that PHP creates before your script starts and makes available everywhere — including inside functions, where ordinary variables from outside are invisible. That is the "super" part of the name: they ignore scope entirely.

What they contain is the request itself, taken apart and handed to you. The browser sent a URL, some headers, possibly a form body and some cookies; PHP parses all of that and puts each piece into the array where you would expect to find it. Working with them is most of what web programming in PHP is.

It is worth being precise about where each one comes from, because that determines how much you can trust it. $_GET, $_POST, $_COOKIE and $_FILES all come from the visitor and can contain absolutely anything. $_SESSION comes from your own server's storage and is therefore trustworthy, as long as you put trustworthy things into it. $_SERVER is a mixture — some entries come from the web server and are reliable, and some are raw HTTP headers sent by the client, which are not.

  • $_GET — values from the URL query string, after the ?
  • $_POST — values from a form submitted with method="post"
  • $_FILES — details of uploaded files; covered in the file-handling lesson
  • $_COOKIE — cookies the browser sent back with this request
  • $_SESSION — your server-side session store, available after session_start()
  • $_SERVER — request and server information, including HTTP headers
  • $_ENV — environment variables, when the server is configured to expose them
  • $_REQUEST — a merge of GET, POST and COOKIE; avoid it, for reasons below
  • $GLOBALS — every global variable in the script; a legacy feature you should not need
Notes
  • If a superglobal is unexpectedly empty inside a function, you have almost certainly typed a lowercase name or a normal variable by mistake. Superglobals are always uppercase with a leading underscore, and they never need the global keyword.

GET Versus POST, and When Each Is Correct

The mechanical difference is where the data travels. GET values ride in the URL, so they are visible in the address bar, saved in browser history, kept in bookmarks, and written into server access logs. POST values travel in the request body, which does not appear in any of those places.

The design difference matters more. A GET request is supposed to ask for something without changing it. A POST request is for actions that change state. This is not just convention — search engines, browser prefetching and link previews will happily follow GET links on their own. A delete link written as <a href="delete.php?id=5"> can therefore be triggered by something that was merely looking at your page, and this has genuinely wiped real databases. Deletions and updates belong in POST forms.

There are practical limits too. URLs have length limits imposed by browsers and servers — a couple of thousand characters is the safe assumption — so a long blog post cannot go through GET. File uploads require POST with enctype="multipart/form-data"; there is no GET equivalent.

So: GET for search queries, filters, pagination and anything you want a shareable link to. POST for logins, registrations, comments, payments, and anything that writes to your database. And note that POST is not more secure in itself — the data is equally visible to anyone inspecting the request. Only HTTPS makes a request private.

Finally, avoid $_REQUEST. It merges GET, POST and cookies, so you can no longer tell where a value came from, and its exact contents depend on a server setting you may not control. An attacker who cannot influence your POST body may still be able to add a query parameter or set a cookie, and $_REQUEST hands them a way in. Always read from the specific array you meant.

Example
<?php
// GET: filters and pagination, shareable as a link
// search.php?q=php&page=2&sort=marks
$query = trim($_GET['q'] ?? '');
$page  = max(1, (int) ($_GET['page'] ?? 1));
$sort  = in_array($_GET['sort'] ?? '', ['name', 'marks'], true)
       ? $_GET['sort'] : 'name';

// POST: actions that change something
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $comment = trim($_POST['comment'] ?? '');
    // ... save it
}

// A delete link is a bug waiting to happen
// <a href="delete.php?id=5">Delete</a>

// A delete form is correct
?>
<form method="post" action="delete.php">
  <input type="hidden" name="id" value="5">
  <button type="submit">Delete</button>
</form>
<?php

// Note how $sort is handled above: the value is checked against an
// allow-list of column names rather than trusted. Never paste a
// user-supplied string into an ORDER BY clause.
Notes
  • $_POST is filled in only when the request body is form-encoded — either application/x-www-form-urlencoded or multipart/form-data. If a JavaScript fetch() sends JSON, $_POST will be completely empty and you must read the raw body yourself with file_get_contents('php://input') and then json_decode() it. This catches almost everyone building their first API endpoint.

Everything in $_GET and $_POST Is Attacker-Controlled

This is the most important idea in the whole course, so it is worth stating without hedging: anything that arrives in a superglobal from the browser can be anything at all. Not "usually valid", not "whatever the form allowed" — anything.

Beginners often assume the HTML form constrains what arrives. It does not. A <select> with three options, a maxlength="20", a type="email", a required attribute, a hidden field with a price in it, a JavaScript validation function — every one of these lives in the browser, and the browser belongs to the visitor. Anyone can open developer tools and change them, or skip your page entirely and send a request straight to your script with a tool like curl. HTML validation is a convenience for honest users; it is not a security control.

Three separate defences follow from this, and each guards a different place. Validate on the way in: check that the value is one of the things you expect, and reject it if not. Use prepared statements for anything that touches the database, so user text can never be read as SQL. Escape on the way out with htmlspecialchars(), so user text can never be read as HTML.

Note that these are three different jobs and none of them substitutes for the others. Validating an email address does not make it safe to paste into a query. Escaping for HTML does not make a value safe in SQL. Each defence protects the specific context it was designed for, and the two database lessons and the form lesson cover them in detail.

One more thing to distrust: hidden fields. A price, a user id or a role stored in <input type="hidden"> reaches you exactly as edited by whoever submitted the form. Prices come from your database at the moment of checkout; the logged-in user's identity comes from the session, never from the form.

Example
<?php
// The form says these are safe. They are not.
// <select name="role"><option>student</option><option>teacher</option></select>
// <input type="hidden" name="price" value="499">

// A visitor can send anything:
//   curl -X POST https://example.com/order.php -d "role=admin&price=1"

// 1. Validate against an allow-list, never a block-list
$allowedRoles = ['student', 'teacher'];
$role = $_POST['role'] ?? '';
if (!in_array($role, $allowedRoles, true)) {
    $role = 'student';   // or reject the request outright
}

// 2. Never trust a price from the form - look it up
$productId = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
if ($productId === false || $productId === null) {
    exit('Invalid product.');
}
$stmt = $pdo->prepare('SELECT price_paise FROM products WHERE id = ?');
$stmt->execute([$productId]);
$price = $stmt->fetchColumn();

// 3. The logged-in user comes from the session, not the form
$userId = $_SESSION['user_id'] ?? null;
if ($userId === null) {
    header('Location: /login.php');
    exit;
}
Notes
  • Prefer an allow-list to a block-list. "Accept only these three values" is a rule you can verify by reading it. "Reject anything containing these dangerous words" is a rule you can never finish writing, because there is always another encoding, another spelling, another character you did not think of.

Reading Input Without Warnings

A parameter that is not present is simply absent from the array, and reading a missing key raises an "Undefined array key" warning in PHP 8. Since the visitor decides what to send, you must assume anything can be missing — even a field your own form always includes.

The null coalescing operator is the standard answer: $_GET['page'] ?? 1 reads the value if it exists and supplies a default otherwise, with no warning. Combine it with trim() for text fields, since users paste values with stray spaces constantly.

For values that must be a particular type, filter_input() and filter_var() do validation and conversion in one step. Their return values need care: filter_input() returns null when the key is absent and false when it is present but invalid, and those are two different situations. Test with === so you can tell them apart.

Two filters worth knowing are FILTER_VALIDATE_INT and FILTER_VALIDATE_EMAIL. Both check the whole value rather than salvaging part of it, which is exactly what you want for input — unlike a bare (int) cast, which turns "abc" into a silent zero.

One deprecation to know about: FILTER_SANITIZE_STRING is deprecated as of PHP 8.1 and should not be used in new code. It was always a confused idea — it stripped tags on the way in, which is the wrong place to solve an output problem. Store what the user typed, and escape it with htmlspecialchars() when you print it.

Example
<?php
// Defaults, no warnings
$page   = (int) ($_GET['page'] ?? 1);
$search = trim($_GET['q'] ?? '');
$name   = trim($_POST['name'] ?? '');

// Keep pagination sane whatever arrives
$page    = max(1, $page);
$perPage = min(100, max(5, (int) ($_GET['per'] ?? 20)));

// filter_input: absent and invalid are different answers
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === null) {
    echo 'No id was supplied.';
} elseif ($id === false) {
    echo 'That id is not a whole number.';
} else {
    echo "Loading record $id";
}

// Email validation
$email = filter_var(trim($_POST['email'] ?? ''), FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Please enter a valid email address.';
}

// Checkboxes are absent when unticked - not false
$subscribe = isset($_POST['subscribe']);

// Multiple values need [] in the field name
// <input type="checkbox" name="subjects[]" value="Physics">
$subjects = $_POST['subjects'] ?? [];
if (!is_array($subjects)) {
    $subjects = [];      // someone sent ?subjects=x instead of an array
}

// A JSON body from fetch() never appears in $_POST
$raw  = file_get_contents('php://input');
$data = json_decode($raw, true) ?? [];
Notes
  • That is_array() check is not paranoia. If your code does foreach ($_POST['subjects'] as ...) and someone sends a plain string instead, PHP 8 warns and your page half-renders. Any input you expect to be an array must be checked, because the visitor chooses its shape as well as its contents.

$_SERVER: Useful Keys and Dangerous Ones

$_SERVER holds information about the request and the environment it arrived in. Some of it is set by the web server and can be relied on; some of it is simply a copy of an HTTP header the client sent, and can be forged. Every key starting with HTTP_ falls into the second category.

$_SERVER['REQUEST_METHOD'] is the one you will use most, to check whether a form was submitted rather than merely displayed. REQUEST_URI gives the path and query string that were requested, which is what a small router uses to decide which page to show.

Now the dangerous ones. HTTP_HOST is the Host header, chosen by the client — building a password-reset link from it lets an attacker point that link at their own domain. HTTP_REFERER is optional, frequently absent and trivially faked, so it is never a security check. HTTP_USER_AGENT is a free-text string that says whatever the client wants.

REMOTE_ADDR is the real network address PHP is talking to, which makes it reliable — but if your site sits behind a proxy or a CDN, that address will be the proxy's, and the visitor's real address appears in a forwarded header that anyone can also send by hand. Only trust such a header when you know a proxy you control is overwriting it.

Finally, a classic: $_SERVER['PHP_SELF'] in a form's action attribute. It includes whatever path was requested, so a crafted URL can inject markup straight into your form tag. The simplest fix is not to use it — leaving action empty submits the form back to the same URL — and if you must print it, escape it like any other untrusted value.

Example
<?php
// Reliable, set by the server
$method = $_SERVER['REQUEST_METHOD'];        // 'GET', 'POST', ...
$uri    = $_SERVER['REQUEST_URI'];          // '/search.php?q=php'
$script = $_SERVER['SCRIPT_NAME'];          // '/search.php'
$ip     = $_SERVER['REMOTE_ADDR'];          // who PHP is actually talking to
$root   = $_SERVER['DOCUMENT_ROOT'];

// The standard "was this form submitted?" check
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // handle the submission
}

// Detecting HTTPS
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';

// Client-controlled - treat as untrusted text
$host    = $_SERVER['HTTP_HOST']       ?? '';   // forgeable
$agent   = $_SERVER['HTTP_USER_AGENT'] ?? '';   // forgeable
$referer = $_SERVER['HTTP_REFERER']    ?? '';   // often missing, forgeable

// Build absolute links from your own configuration, not from the Host header
const SITE_URL = 'https://example.com';
$resetLink = SITE_URL . '/reset.php?token=' . urlencode($token);

// The PHP_SELF trap
// <form action="<?= $_SERVER['PHP_SELF'] ?>">          <- injectable
// <form action="">                                     <- same page, safe
// <form action="<?= htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') ?>">
Notes
  • Logging a user agent or referer into your database is fine, and printing it back onto an admin page without escaping is not. Anything in $_SERVER that began life as an HTTP header needs htmlspecialchars() on output exactly like $_POST does.

The Rest, and Where They Are Covered

The remaining superglobals each get proper treatment in their own lessons, but it is useful to see the whole set together and know what each is for.

$_SESSION is the one that changes how you think about building an application, because it is the only superglobal whose contents your own server controls. It is empty until you call session_start(), and it is where a logged-in user's identity belongs.

$_ENV and the related getenv() function read environment variables, which is the modern way to keep secrets out of your code. Database passwords and API keys should come from the environment or from a configuration file stored outside the web root — never hard-coded into a file that might end up in a public git repository. Many projects use a .env file with a library such as phpdotenv for this, and add .env to .gitignore.

One piece of history worth knowing, because you will meet it in old tutorials. PHP once had a feature called register_globals that turned every query parameter into a plain variable, so ?admin=1 would create $admin. It caused a long list of security holes and was removed from PHP many versions ago. If a tutorial uses bare $name where it should use $_POST['name'], it predates PHP 5.4 and everything else in it should be treated with suspicion.

  • $_SESSION — server-side per-visitor storage; see the Sessions & Cookies lesson
  • $_COOKIE — small values stored in the browser and sent back on every request; visitor-editable
  • $_FILES — uploaded file details, including a temporary path and an error code; see File Handling
  • $_ENV / getenv() — environment variables, the right home for credentials
  • $_REQUEST — a merge of GET, POST and COOKIE whose contents depend on server configuration; do not use it
  • $GLOBALS — access to global variables from inside a function; a legacy escape hatch, not a design tool
Notes
  • A useful mental model when you sit down to write any page: which parts of what I am about to use came from the visitor, and which came from my own server? Answer that first, and the questions of what to validate, what to bind as a parameter and what to escape mostly answer themselves.
Ask AI