A response header leaks an internal address
What this check looks for
One of your headers contains an address from inside your own network — a private IP or an internal-only hostname. Every visitor receives it, including anyone mapping your infrastructure.
Why it matters
It hands an attacker a free map: which internal ranges you use, how many backends sit behind the load balancer, what your naming convention is. That is the reconnaissance step for server-side request forgery and for lateral movement after any foothold.
When the check passes, your report says: “Response headers keep internal addresses to themselves”.
What it costs your score
When this check fails it removes 8 points from your HTTP score, before the status, confidence and repeat multipliers are applied. HTTP carries a weight of 7 in the overall score.
It shares the http.hygiene family ceiling of 10 points: however many findings that family produces, together they cannot remove more than that from HTTP. One underlying problem showing up in several places is still one problem.
- Severity
- medium
- Default confidence
- high
- Status when triggered
- warn
- Deduction
- 8 points
- Family cap
- http.hygiene · 10
- Category
- HTTP
- Module
- Http headers
- Fix owned by
- user
- In the ruleset since
- 2026.09
How to fix it
Stop sending headers that contain internal addresses or hostnames to the public.
They map your internal network for anyone who reads a response, and an internal Location is a redirect no visitor can follow.
Remove the diagnostic headers at the outermost proxy — the last hop before the visitor — so nothing behind it can reintroduce them.
Remove them at the origin too. A proxy that is bypassed, by IP or through an unproxied DNS record, exposes them again.
For an internal
Location, fix the cause rather than the symptom: tell the application its public origin (X-Forwarded-Host/X-Forwarded-Protohonoured correctly, or an explicit base-URL setting) so it builds absolute URLs on the public hostname — or emit a relativeLocation, which RFC 9110 permits.nginx:
proxy_hide_headerremoves a header arriving from an upstream; removing an arbitrary header needs the headers-more module. Apache:Header always unset.If you need the header for debugging, make it conditional on a trusted source address rather than sending it to every visitor.
How to confirm it worked
curl -sSI https://‹host›/ | grep -inE '(10|127|169\.254|192\.168)\.[0-9]+\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|\.(local|internal|lan|corp)\b' — expect nothing
curl -sSI https://‹host›/ | grep -i '^location:' — any redirect must point at a public hostname
A named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.
Remediation by platform
# Requires the headers-more module (ngx_headers_more):
more_clear_headers 'X-Backend-Server' 'X-Served-By' 'X-Real-IP' 'Via';
# For a header added by a proxied upstream, stock nginx can do this instead:
# proxy_hide_header X-Backend-Server;<IfModule mod_headers.c>
Header always unset X-Backend-Server
Header always unset X-Served-By
Header always unset X-Real-IP
Header always unset Via
</IfModule>header {
-X-Backend-Server
-X-Served-By
-X-Real-IP
-Via
}Rules → Overview → Create rule → Response Header Transform Rule → Remove (path as at 2026-09).Stock nginx cannot remove an arbitrary response header:
add_headeronly adds, andproxy_hide_headerworks only for a header arriving from a proxied upstream. If neither applies, stop the application emitting it instead — which is the better fix in any case.Shown because the response identified Apache. Put it in the virtual host or server configuration rather than .htaccess: .htaccess is re-read on every request, is often disabled by AllowOverride None, and runs too late to influence a response the virtual host generates itself.
alwaysmatters here: without it the rule skips error responses, which are frequently the ones that leak the most.This applies only to responses that reach visitors through Cloudflare. An origin that is reachable directly — by IP, or through a DNS record that is not proxied — still serves the response measured here unchanged. Setting it at the origin as well covers both paths.
Technical detail
Headers carrying an internal address on the response from ‹origin›: ‹headers›. ‹detail›
What counts as internal here: RFC 1918 private space (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), link-local (169.254/16, fe80::/10), IPv6 unique-local (fc00::/7), CGNAT space (100.64/10, RFC 6598), and hostnames under .local, .internal, .lan and .corp.
The headers this usually appears in are Via, X-Served-By, X-Backend-Server, X-Cache-Server, X-Real-IP, Server and — the one that actually breaks things — Location or Content-Location. A Location pointing at an internal address is not merely a disclosure: the redirect is unusable from outside, so the response is broken for every visitor as well as informative to an attacker. It is the signature of a reverse proxy whose upstream does not know its public hostname.
Most of these are diagnostic headers switched on during an incident and never switched off. Where one is genuinely wanted for debugging, gate it behind a request from a trusted source rather than sending it to everyone. Detected server: ‹detected server›.
Standards and references
Test this on your domain
Run the check that produces this finding, on its own, against any domain.
Other http headers checks
- The caching instructions contradict each other
- A text response does not declare its character encoding
- The declared Content-Encoding may not match the body
- A compressible response was sent uncompressed
- The response has a body but no Content-Type
- The response sends obsolete headers, one of them harmful
- No Alt-Svc header advertises HTTP/3
- The response does not say how long it may be cached
- The response cannot be revalidated without downloading it again
- A compressed response does not vary on Accept-Encoding
- Vary: * makes the response uncacheable by any shared cache