$_SERVER
What Is $_SERVER?
In simple terms: $_SERVER is an array with information about the current request and the environment the script runs in: which URL was requested, which HTTP method was used, the visitor's IP address, request headers, the script's file path, and more. Technically: $_SERVER is a superglobal array populated by PHP from the web server (through the server API, such as PHP-FPM or Apache's module) and from the process environment. Its exact contents depend on the web server and its configuration; PHP does not guarantee that every key exists.
Why It Is Needed
Many decisions depend on details of the request itself rather than on submitted data:
- Was the form submitted (
POST) or just displayed (GET)? - Which path was requested (for routing)?
- Is the connection secure (HTTPS)?
- What language does the browser prefer?
- From which IP address did the request come (for rate limiting or logging)?
Common Keys
Request Information
| Key | Example Value | Meaning |
|---|---|---|
REQUEST_METHOD | GET, POST, PUT, DELETE | HTTP method |
REQUEST_URI | /products.php?category=books&page=2 | Path and query string as requested |
QUERY_STRING | category=books&page=2 | Only the query string |
REQUEST_TIME | 1791541200 | Unix timestamp of the request start |
REQUEST_TIME_FLOAT | 1791541200.2531 | Same, with microseconds |
SERVER_PROTOCOL | HTTP/1.1 | Protocol version |
HTTPS | on | Non-empty when the request used HTTPS (may be missing or off otherwise) |
CONTENT_TYPE | application/json | Type of the request body |
CONTENT_LENGTH | 57 | Size of the request body in bytes |
Script and Server Information
| Key | Example Value | Meaning |
|---|---|---|
SCRIPT_NAME | /products.php | URL path of the running script |
SCRIPT_FILENAME | /var/www/shop/public/products.php | Absolute file path of the script |
DOCUMENT_ROOT | /var/www/shop/public | Web server document root |
PHP_SELF | /products.php | Script path plus any extra path info (see Security) |
SERVER_NAME | gocourse.in | Server name from the web server configuration |
SERVER_PORT | 443 | Port the server received the request on |
SERVER_SOFTWARE | nginx/1.26.0 | Web server name and version |
Client and Header Information
| Key | Example Value | Meaning |
|---|---|---|
REMOTE_ADDR | 203.0.113.7 | IP address of the client (or of the last proxy) |
HTTP_HOST | gocourse.in | Host header sent by the client |
HTTP_USER_AGENT | Mozilla/5.0 (...) | Browser identification |
HTTP_ACCEPT_LANGUAGE | ta-IN,ta;q=0.9,en;q=0.8 | Preferred languages |
HTTP_REFERER | https://google.com/ | Page that linked here (often absent) |
How Request Headers Become Keys
Every request header is available with the prefix HTTP_, in uppercase, with hyphens replaced by underscores:
| Header | $_SERVER Key |
|---|---|
Accept-Language | HTTP_ACCEPT_LANGUAGE |
X-Requested-With | HTTP_X_REQUESTED_WITH |
Authorization | HTTP_AUTHORIZATION (some servers need extra configuration to pass it) |
Content-Type and Content-Length are exceptions: they appear as CONTENT_TYPE and CONTENT_LENGTH, without the prefix.
Reading $_SERVER Safely
Keys may be missing depending on the server, the request, and whether the script runs in the command line. Always provide defaults:
<?php
// Example request: GET https://gocourse.in/products.php?category=books&page=2
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($uri, PHP_URL_PATH);
$query = $_SERVER['QUERY_STRING'] ?? '';
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$referrer = $_SERVER['HTTP_REFERER'] ?? 'none';
echo "Method: $method\n";
echo "Path: $path\n";
echo "Query: $query\n";
echo "HTTPS: " . ($isHttps ? 'yes' : 'no') . "\n";
echo "Referrer: $referrer\n";
Output (for the example request, visited directly)
Method: GET
Path: /products.php
Query: category=books&page=2
HTTPS: yes
Referrer: none
parse_url(..., PHP_URL_PATH)extracts the path fromREQUEST_URI, dropping the query string.- The HTTPS check accepts any non-empty value except
off, which some servers (such as IIS) use for plain HTTP. HTTP_REFERERis missing when the visitor typed the address, used a bookmark, or the browser's privacy settings removed it.
Detecting the Request Method
The most common use of $_SERVER is distinguishing "show the form" from "process the form":
<?php
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
// validate and process $_POST, then redirect
} else {
// display the empty form
}
Checking the method is more reliable than checking whether $_POST is empty: a POST request can have an empty body, and a request whose body exceeds post_max_size arrives with an empty $_POST even though it was submitted. See $_POST.
Practical Example: A Small Router
A router maps the method and path of each request to the code that handles it. Keeping the logic in a function makes it testable without a web server:
<?php
function route(string $method, string $uri): array
{
$path = rtrim(parse_url($uri, PHP_URL_PATH) ?? '/', '/') ?: '/';
$routes = [
'GET /' => 'Home page',
'GET /products' => 'Product list',
'POST /products' => 'Product created',
];
if (isset($routes["$method $path"])) {
return [200, $routes["$method $path"]];
}
$pathExists = array_filter(
array_keys($routes),
fn(string $route): bool => explode(' ', $route, 2)[1] === $path
);
return $pathExists ? [405, 'Method not allowed'] : [404, 'Page not found'];
}
$requests = [
['GET', '/'],
['GET', '/products?page=2'],
['POST', '/products/'],
['DELETE', '/products'],
['GET', '/about'],
];
foreach ($requests as [$method, $uri]) {
[$status, $body] = route($method, $uri);
printf("%-6s %-18s → %d %s\n", $method, $uri, $status, $body);
}
// In a real front controller:
// [$status, $body] = route($_SERVER['REQUEST_METHOD'] ?? 'GET', $_SERVER['REQUEST_URI'] ?? '/');
// http_response_code($status);
// echo $body;
Output
GET / → 200 Home page
GET /products?page=2 → 200 Product list
POST /products/ → 200 Product created
DELETE /products → 405 Method not allowed
GET /about → 404 Page not found
Step-by-Step Explanation
parse_url()removes the query string, andrtrim(..., '/')removes a trailing slash, so/products/and/productsmatch the same route. The root path/would become an empty string, so?: '/'restores it.- Routes are stored as
"METHOD /path"keys for fast lookup. - If the path exists but not for this method, the correct HTTP status is 405 Method Not Allowed; if the path does not exist at all, it is 404 Not Found.
- The commented lines show how a front controller (
public/index.php) feeds$_SERVERvalues into the function and sends the response. Frameworks use the same principle with far more features: route parameters (/products/{id}), middleware, and controllers.
Security Considerations
Many $_SERVER values come directly from the client and can be manipulated.
PHP_SELF and Cross-Site Scripting
<!-- Vulnerable -->
<form action="<?= $_SERVER['PHP_SELF'] ?>" method="post">
PHP_SELF includes any extra path information after the script name. A request to /contact.php/"><script>alert(1)</script> can make PHP_SELF contain HTML, which is then printed into the page. Safer options:
<form action="" method="post"> <!-- submit to the current URL -->
<form action="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>" method="post">
HTTP_HOST Can Be Spoofed
The Host header is chosen by the client. Using it to build absolute URLs — for example, in password reset emails — allows host header injection: an attacker requests a reset for a victim with a forged Host, and the email contains a link to the attacker's domain. Fix: Build absolute URLs from a configured base URL (for example, APP_URL=https://gocourse.in in your configuration), not from HTTP_HOST. Also configure the web server to accept only your real domain names.
REMOTE_ADDR and Proxies
REMOTE_ADDR is the IP address of whoever connected to the server. Behind a load balancer, CDN, or reverse proxy, that is the proxy's address, and the real client address is passed in a header such as X-Forwarded-For. That header can be forged by anyone, so it may only be trusted when the request actually came from your own proxy:
<?php
function clientIp(array $server, array $trustedProxies): string
{
$remote = $server['REMOTE_ADDR'] ?? '0.0.0.0';
if (!in_array($remote, $trustedProxies, true) || empty($server['HTTP_X_FORWARDED_FOR'])) {
return $remote;
}
$chain = array_map('trim', explode(',', $server['HTTP_X_FORWARDED_FOR']));
foreach (array_reverse($chain) as $ip) {
if (!in_array($ip, $trustedProxies, true) && filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
return $remote;
}
$trusted = ['10.0.0.2'];
echo clientIp(['REMOTE_ADDR' => '203.0.113.7'], $trusted) . "\n";
echo clientIp(['REMOTE_ADDR' => '10.0.0.2', 'HTTP_X_FORWARDED_FOR' => '198.51.100.23'], $trusted) . "\n";
echo clientIp(['REMOTE_ADDR' => '203.0.113.7', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4'], $trusted) . "\n";
echo clientIp(['REMOTE_ADDR' => '10.0.0.2', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 198.51.100.23'], $trusted) . "\n";
Output
203.0.113.7
198.51.100.23
203.0.113.7
198.51.100.23
How It Works
- A direct connection (not from a trusted proxy) returns
REMOTE_ADDR; anyX-Forwarded-Forheader sent by the client is ignored (third case). - When the request comes from the trusted proxy, the function reads the forwarded chain from right to left. Each proxy appends the address it received the request from, so the rightmost address not belonging to your proxies is the real client.
- In the fourth case, the client tried to fake its address by sending
1.2.3.4itself; the proxy appended the real address198.51.100.23, which is the one returned. The function receives the server array as a parameter (clientIp($_SERVER, $trusted)in real code), which makes it easy to test with different inputs.
$_SERVER in the Command Line
When running a script with php script.php, there are no HTTP keys (REQUEST_METHOD, HTTP_HOST, and so on are missing). Instead, $_SERVER['argv'] and $_SERVER['argc'] contain the command-line arguments, and the environment variables of the shell are included. Code that may run in both contexts should use ?? defaults for every HTTP-related key (see PHP Command Line (CLI)).
Common Mistakes
Assuming a Key Always Exists
Effect: Warnings, especially for HTTP_REFERER, HTTPS, and in CLI scripts. Fix: Use ?? with sensible defaults.
Printing PHP_SELF Unescaped
Effect: Cross-site scripting. Fix: Use action="", or escape SCRIPT_NAME with htmlspecialchars().
Building Links From HTTP_HOST
Effect: Host header injection in emails and redirects. Fix: Use a configured application URL.
Trusting X-Forwarded-For Unconditionally
Effect: Attackers bypass IP-based rate limits or appear to come from any address. Fix: Trust forwarded headers only when REMOTE_ADDR is your own proxy.
Using HTTP_REFERER for Security
Effect: The header is optional and easily forged. Fix: Use CSRF tokens to verify form submissions (see CSRF Protection).
Best Practices
- Read
$_SERVERvalues with defaults and validate them. - Use
REQUEST_METHODto decide between displaying and processing forms. - Parse
REQUEST_URIwithparse_url()for routing. - Treat all client-controlled values (
HTTP_*,PHP_SELF,REQUEST_URI) as untrusted input. - Wrap request-dependent logic in functions that accept values as parameters, so it can be tested without a web server. Official reference: https://www.php.net/manual/en/reserved.variables.server.php