Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a normal web request, PHP exposes the connecting peer’s address in $_SERVER['REMOTE_ADDR']:

<?php
$ip = $_SERVER['REMOTE_ADDR'] ?? null;

That value is the client address when the browser connects directly to your web server. If a reverse proxy or load balancer sits in front of PHP, it may instead be the proxy’s address. Forwarded headers can recover the original address only when they are supplied and sanitized by infrastructure you trust. Always validate an address before storing it, displaying it, or using it in a security decision.

Read the direct peer address

REMOTE_ADDR is the address from which the user is viewing the current page, in PHP’s terminology. It is supplied by the web server, not generated by PHP. A minimal page is:

<?php
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
echo htmlspecialchars($ip, ENT_QUOTES, 'UTF-8');

htmlspecialchars is appropriate when the value is inserted into HTML. Do not assume that an address is safe merely because it came from a server variable: request metadata is still input data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What the value means

  • With a direct browser-to-origin connection, it normally identifies the visitor’s network endpoint.
  • Behind a reverse proxy, CDN, ingress controller, or load balancer, it normally identifies the immediate proxy.
  • It may be IPv4 or IPv6. Do not split on a colon or otherwise assume IPv4 notation.
  • During CLI execution there is no ordinary HTTP client, so normal $_SERVER request variables may be absent or meaningless.

Validate the address before using it

Use PHP’s filter extension to check IPv4 and IPv6 syntax and provide an explicit failure path:

<?php
$raw = $_SERVER['REMOTE_ADDR'] ?? '';
$ip = filter_var($raw, FILTER_VALIDATE_IP) ?: null;

if ($ip === null) {
    http_response_code(400);
    exit('A valid client IP was not available.');
}

// Use $ip as validated input.

FILTER_VALIDATE_IP accepts syntactically valid IPv4 and IPv6 addresses. A valid result is the filtered string; false means validation failed. The shorthand above converts that failure to null, making it easy to distinguish “no usable address” from a real value.

Apply a narrower policy when needed

Validation is not the same as deciding whether an address is acceptable for a particular job. PHP also provides flags for narrower policies:

Policy Filter Use case
Any valid IP FILTER_VALIDATE_IP Logging, display, general request metadata
IPv4 only FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 Legacy systems that cannot store IPv6
IPv6 only FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 An IPv6-specific endpoint or test
Reject private ranges FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE Policies that require publicly routable addresses
Reject reserved ranges FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE Policies that exclude reserved, non-public ranges

Do not reject private addresses automatically for ordinary application logging. A visitor on an internal network, or a proxy inside your network, can legitimately produce one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why X-Forwarded-For is not automatically trustworthy

HTTP request headers appear in PHP as $_SERVER['HTTP_...']. For example, X-Forwarded-For appears as $_SERVER['HTTP_X_FORWARDED_FOR'], and Client-IP may appear as $_SERVER['HTTP_CLIENT_IP']. A client can send those headers itself. If your application accepts them unconditionally, a visitor can claim any address.

That makes this pattern unsafe:

$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;

Never use an unchecked forwarded header as the sole basis for authentication, authorization, rate limiting, fraud controls, or an allowlist.

Get the original address through a trusted proxy chain

A safe design has four parts:

  1. Identify the direct peer. Read and validate REMOTE_ADDR.
  2. Check the peer against configured trusted proxy ranges. Do not decide trust from a request header.
  3. Only for a trusted peer, parse the proxy’s documented forwarding header. The proxy must remove untrusted incoming values and add its own chain in a known format.
  4. Apply that proxy’s documented trust direction. Split comma-separated values, trim whitespace, validate every candidate, and walk from the appropriate side until you reach the first untrusted hop.

The exact order depends on your infrastructure. Some deployments treat the rightmost entries as the newest proxy hops; others document a different convention. Follow the documentation for the proxy or CDN that terminates the connection, and configure its trusted network ranges explicitly.

A deliberately limited parser

The following example demonstrates validation and a fallback. It does not know your proxy ranges or trust direction, so it must not be copied as a complete security policy:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
$peer = $_SERVER['REMOTE_ADDR'] ?? '';
$peer = filter_var($peer, FILTER_VALIDATE_IP) ?: null;

$ip = $peer;
$forwarded = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';

// Replace this with a real check against your configured proxy CIDRs.
$peerIsTrustedProxy = false;

if ($peerIsTrustedProxy) {
    $candidates = array_map('trim', explode(',', $forwarded));
    foreach ($candidates as $candidate) {
        if (filter_var($candidate, FILTER_VALIDATE_IP) !== false) {
            $ip = $candidate;
            break; // Change direction and selection to your proxy's specification.
        }
    }
}

$ip = $ip ?? 'unknown';
echo htmlspecialchars($ip, ENT_QUOTES, 'UTF-8');

For production applications, use the trusted-proxy facilities in your framework or web stack where available. Symfony’s request object, for example, reads forwarded addresses only after trusted proxies have been configured; without that configuration it returns the direct peer. The important property is the trust boundary, not the header name.

Store and display IP addresses correctly

Database and logs

  • Store the validated textual address or use a database type designed for IPv4/IPv6. Preserve IPv6 rather than truncating it to an IPv4-sized column.
  • Keep the direct peer and any derived client address separately if incident investigation requires both.
  • Record which proxy configuration produced a derived value, and rotate or protect logs because IP addresses can be personal data in many jurisdictions.
  • Do not use an IP address as a permanent identity. Addresses can be shared, reassigned, translated through NAT, or changed by mobile networks.

HTML and APIs

Escape an address for its output context. The HTML example uses htmlspecialchars; a JSON endpoint should use json_encode rather than string concatenation:

<?php
$ip = filter_var($_SERVER['REMOTE_ADDR'] ?? '', FILTER_VALIDATE_IP) ?: null;
header('Content-Type: application/json');
echo json_encode(['ip' => $ip], JSON_THROW_ON_ERROR);

Common failures and fixes

Symptom Likely cause Fix
You always see a load balancer or CDN address The proxy terminates the client connection. Configure trusted proxy ranges and consume its documented forwarding header only after the trust check.
The value is empty in a script The code runs from CLI, a worker, or a non-HTTP context. Handle the missing value explicitly; pass request metadata to a worker as job data if required.
Rate limits can be bypassed with a custom header The application trusts client-supplied X-Forwarded-For. Strip or overwrite that header at the edge and accept it only from known proxy peers.
IPv6 addresses fail a database insert The column or validation policy is IPv4-only. Use an IPv6-capable column and FILTER_VALIDATE_IP, or reject IPv6 deliberately with FILTER_FLAG_IPV4.
An address appears in an HTML page as markup Unescaped request data was output. Escape with htmlspecialchars($ip, ENT_QUOTES, 'UTF-8').
Private addresses are rejected unexpectedly A no-private-range flag was applied without a business reason. Remove that flag for normal logging, or document the public-address requirement.

Testing checklist

  1. Test a direct HTTP request and confirm the peer address.
  2. Test through every proxy layer used in production and record which address each layer adds.
  3. Send a forged X-Forwarded-For header directly to the origin (from an allowed test network) and verify it is ignored.
  4. Test IPv4, IPv6, missing variables, malformed values, and comma-separated chains with invalid entries.
  5. Verify rate limiting and access control use the trusted, derived value rather than raw headers.
  6. Run the same code from CLI and confirm it follows the documented no-request path.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your real goal is capturing a page rather than identifying the visitor making a PHP request, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL in one request and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the complete parameter list and response behavior in the ScreenshotNeo documentation. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000, and every feature is included on every plan. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Can PHP discover a visitor’s private, local IP address?

No. PHP receives the address presented by the network path to your server. A browser does not reveal every address on the visitor’s local network, and NAT commonly hides private addresses.

Should I anonymize IP addresses?

That depends on your legal, operational, and security requirements. Define a retention period and masking policy with your privacy and security owners; validation alone does not make indefinite retention appropriate.

Is an IP address proof that two requests came from the same person?

No. Multiple people can share one address, one person can use several addresses, and proxies, VPNs, mobile networks, and address reassignment can all change what your server sees.

Frequently Asked Questions

Can PHP discover a visitor’s private, local IP address?

No. PHP receives the address presented by the network path to your server; NAT commonly hides private addresses.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I anonymize IP addresses?

Set retention and masking rules according to your legal, operational, and security requirements; validation does not justify indefinite retention.

Is an IP address proof that two requests came from the same person?

No. Shared networks, VPNs, mobile connections, proxies, and reassignment mean an address is not a durable identity.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.