Skip to content

Commit 3cbedff

Browse files
committed
new config option: trustProxy
1 parent 5624e22 commit 3cbedff

7 files changed

Lines changed: 97 additions & 51 deletions

File tree

README.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ An updater function can also be passed to the `env` option to update the environ
190190
}
191191
```
192192

193+
### trustProxy
194+
195+
Set to `true` to trust proxy-related HTTP headers (`X-Forwarded-For`, `X-Forwarded-Proto`, and `Host`). This affects CGI environment variables such as:
196+
197+
- `REMOTE_ADDR` — will use the leftmost IP in `X-Forwarded-For`
198+
- `HTTPS` — will be `"on"` if `X-Forwarded-Proto` is `"https"`
199+
- `SERVER_NAME` and `SERVER_PORT` — will be parsed from the `Host` header
200+
201+
Default: `false`
202+
203+
> ⚠️ **Important:** Only enable this if you are **running behind a trusted reverse proxy** (like Nginx or a load balancer). Enabling `trustProxy` when exposed to the public internet can allow **header spoofing** by clients.
204+
193205
# Start a CGI Server from the Command Line
194206

195207
The command `cgi-server` can be used to run an HTTP server to serve CGI scripts.
@@ -303,20 +315,6 @@ http {
303315
}
304316
```
305317

306-
### ⚠️ Note on Proxy Headers and CGI Environment Variables
307-
308-
If you run `cgi-core` behind a reverse proxy (such as **Nginx**), certain CGI environment variables may be influenced by proxy headers, including:
309-
310-
- `REMOTE_ADDR` — may reflect the proxy's IP unless `X-Forwarded-For` is set
311-
- `HTTPS` — determined by `X-Forwarded-Proto` if TLS is terminated at the proxy
312-
- `SERVER_NAME` — usually derived from the `Host` header sent by the proxy
313-
314-
By default, `cgi-core` **does not validate proxy headers**. If untrusted clients can set headers like `X-Forwarded-For`, this could result in spoofed values being passed to your CGI scripts.
315-
316-
🔐 **Important**: Make sure your reverse proxy is properly configured and only accepts requests from trusted sources. If needed, filter or overwrite proxy headers before they reach your Node.js server.
317-
318-
_You may choose to implement your own logic for handling trusted proxies or wait for future support of a `trustProxy` option._
319-
320318
# License
321319

322320
`cgi-core` is released under the [MIT License](https://github.com/lfortin/node-cgi-core/blob/master/LICENSE).

bin/cgi-server.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ const options = {
6868
type: "boolean",
6969
short: "l",
7070
},
71+
trustProxy: {
72+
type: "boolean",
73+
},
7174
port: {
7275
type: "string",
7376
short: "p",
@@ -91,16 +94,20 @@ if (values.help) {
9194
9295
-h, --help Display help
9396
-v, --version Display cgi-core version string
97+
9498
--urlPath <urlPath> Set base url path for routing
9599
--filePath <filePath> Set file path where the CGI scripts are located
100+
-p, --port <port> Set the port to listen on
101+
96102
--indexExtension <extension> Set file extension to lookup for index files
97103
--maxBuffer <bytes> Set the allowed HTTP request and response payloads size in bytes
98104
--requestChunkSize <bytes> Set the HTTP request payload data chunks size in bytes
99105
--responseChunkSize <bytes> Set the HTTP response payload data chunks size in bytes
100106
--requestTimeout <ms> Set the HTTP request timeout delay in milliseconds
107+
--trustProxy Trust proxy headers (X-Forwarded-For, etc.)
108+
101109
-d, --debugOutput Output errors for HTTP status 500
102110
-l, --logRequests Log HTTP requests to STDOUT
103-
-p, --port <port> Set the port to listen on
104111
`);
105112
process.exit();
106113
}

cgi-core.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface Config {
4141
requestTimeout: number;
4242
forceKillDelay: number;
4343
requireExecBit: boolean;
44+
trustProxy: boolean;
4445
statusPages: StatusPages;
4546
env: EnvVars | EnvUpdaterFunction;
4647
}

cgi-core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ function createHandler(configOptions = {}) {
109109
filePath,
110110
fullFilePath,
111111
env: config.env,
112+
trustProxy: config.trustProxy,
112113
});
113114
} catch (err) {
114115
if (err.code === "ENOENT") {

lib/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const DEFAULT_CONFIG = {
7070
requestTimeout: 30000,
7171
forceKillDelay: 1000,
7272
requireExecBit: false,
73+
trustProxy: false,
7374
statusPages: {},
7475
env: {},
7576
};

lib/util.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ function getExecPath(filePath, extensions) {
8686
}
8787

8888
function createEnvObject(req, extraInfo) {
89+
const { trustProxy } = extraInfo;
90+
8991
const env = {};
9092
for (const header of Object.keys(req.headers)) {
9193
const snakeCase = header.replace(/\-/g, "_").toUpperCase();
@@ -118,35 +120,37 @@ function createEnvObject(req, extraInfo) {
118120
env.PATH_INFO = `${separator}${pathInfo}`.replace(/\/+/g, "/");
119121
}
120122

121-
// REMOTE_ADDR may be affected by proxy headers (e.g., X-Forwarded-For).
122-
// We default to using the first IP from X-Forwarded-For if present.
123-
// IMPORTANT: These headers are untrusted by default — ensure your reverse proxy is secured.
124-
const forwardedFor = req.headers["x-forwarded-for"];
125-
const clientIp = forwardedFor
126-
? forwardedFor.split(",")[0].trim()
127-
: req.socket?.remoteAddress || req.connection?.remoteAddress;
123+
// REMOTE_ADDR may be affected by X-Forwarded-For
124+
const clientIp =
125+
trustProxy && req.headers["x-forwarded-for"]
126+
? req.headers["x-forwarded-for"].split(",")[0].trim()
127+
: req.socket?.remoteAddress || req.connection?.remoteAddress;
128128
env.REMOTE_ADDR = clientIp;
129129

130-
env.SERVER_PORT = req.socket?.localPort || req.connection?.localPort;
130+
// HTTPS is set to "on" if the connection is encrypted or proxied via X-Forwarded-Proto
131+
if (
132+
(trustProxy && req.headers["x-forwarded-proto"] === "https") ||
133+
req.socket?.encrypted
134+
) {
135+
env.HTTPS = "on";
136+
}
131137

132-
// SERVER_NAME may reflect the Host header, which can be manipulated by clients.
133-
// This is common practice behind proxies, but security-sensitive scripts should treat with caution.
134-
if (req.headers["host"]) {
135-
env.SERVER_NAME = req.headers["host"].split(":")[0];
138+
// SERVER_NAME may reflect the Host header
139+
if (trustProxy && req.headers["host"]) {
140+
const [name, port] = req.headers["host"].split(":");
141+
env.SERVER_NAME = name;
142+
env.SERVER_PORT = port || (env.HTTPS === "on" ? "443" : "80");
136143
} else if (req.socket?.localAddress) {
137-
env.SERVER_NAME = req.socket.localAddress;
144+
env.SERVER_NAME = req.socket?.localAddress;
145+
env.SERVER_PORT = String(
146+
req.socket?.localPort || req.connection?.localPort
147+
);
138148
}
139149

140150
if (req.headers["authorization"]) {
141151
env.AUTH_TYPE = req.headers["authorization"].split(" ")[0];
142152
}
143153

144-
// HTTPS is set to "on" if the connection is encrypted or proxied via X-Forwarded-Proto.
145-
// Only use this logic if your reverse proxy is configured correctly and trusted.
146-
if (req.headers["x-forwarded-proto"] === "https" || req.socket?.encrypted) {
147-
env.HTTPS = "on";
148-
}
149-
150154
if (extraInfo.env) {
151155
if (typeof extraInfo.env === "function") {
152156
Object.assign(env, extraInfo.env(env, req));

test/spec-runner.js

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,18 @@ script.cgi`);
103103
const req = {
104104
url: "/cgi-bin/script.cgi/extra/path?param1=test&param2=test",
105105
socket: {
106-
remoteAddress: "127.0.0.1",
106+
remoteAddress: "100.100.100.100",
107107
localAddress: "127.0.0.1",
108-
localPort: 3001,
108+
localPort: "3001",
109109
},
110110
headers: {
111111
"content-type": "application/json",
112112
"content-length": 1024,
113113
cookie: "yummy_cookie=choco; tasty_cookie=strawberry",
114114
authorization: "Bearer [token]",
115+
"x-forwarded-for": "200.200.200.200",
116+
"x-forwarded-proto": "https",
117+
host: "www.example.org:3002",
115118
},
116119
method: "GET",
117120
httpVersion: "1.1",
@@ -121,6 +124,7 @@ script.cgi`);
121124
filePath: "files/script.cgi",
122125
fullFilePath: "/home/username/cgi-bin/files/script.cgi",
123126
env: { ANOTHER_VAR: "another value" },
127+
trustProxy: false,
124128
};
125129
let env = createEnvObject(req, extraInfo);
126130

@@ -149,29 +153,56 @@ script.cgi`);
149153
env.SCRIPT_FILENAME,
150154
"/home/username/cgi-bin/files/script.cgi"
151155
);
152-
assert.strictEqual(env.REMOTE_ADDR, "127.0.0.1");
153-
assert.strictEqual(env.SERVER_PORT, 3001);
156+
assert.strictEqual(env.HTTP_HOST, "www.example.org:3002");
157+
assert.strictEqual(env.REMOTE_ADDR, "100.100.100.100");
158+
assert.strictEqual(env.SERVER_PORT, "3001");
154159
assert.strictEqual(env.SERVER_NAME, "127.0.0.1");
160+
assert.notStrictEqual(env.HTTPS, "on");
155161
assert.strictEqual(env.AUTH_TYPE, "Bearer");
156162
assert.strictEqual(env.SCRIPT_NAME, "/files/script.cgi");
157163
assert.strictEqual(env.ANOTHER_VAR, "another value");
158164

159-
extraInfo.env = function (env, req) {
160-
return {
161-
UNIQUE_ID: req.uniqueId,
162-
};
163-
};
164-
req.headers["x-forwarded-for"] = "127.0.0.1";
165-
req.headers["x-forwarded-proto"] = "https";
166-
req.headers["host"] = "www.example.org:3001";
167-
env = createEnvObject(req, extraInfo);
168-
assert.strictEqual(env.HTTP_HOST, "www.example.org:3001");
169-
assert.strictEqual(env.REMOTE_ADDR, "127.0.0.1");
170-
assert.strictEqual(env.SERVER_PORT, 3001);
165+
env = createEnvObject(
166+
req,
167+
Object.assign({}, extraInfo, {
168+
trustProxy: true,
169+
env: function (env, req) {
170+
return {
171+
UNIQUE_ID: req.uniqueId,
172+
};
173+
},
174+
})
175+
);
176+
177+
assert.strictEqual(env.REMOTE_ADDR, "200.200.200.200");
178+
assert.strictEqual(env.SERVER_PORT, "3002");
171179
assert.strictEqual(env.SERVER_NAME, "www.example.org");
172180
assert.strictEqual(env.HTTPS, "on");
173181
assert.strictEqual(env.UNIQUE_ID, req.uniqueId);
174182
});
183+
it("should fall back gracefully without host or proxy headers", async () => {
184+
const req = {
185+
url: "/cgi-bin/test.cgi",
186+
method: "GET",
187+
httpVersion: "1.1",
188+
headers: {},
189+
socket: {
190+
localAddress: "127.0.0.1",
191+
localPort: "8080",
192+
remoteAddress: "192.168.1.10",
193+
},
194+
};
195+
const extraInfo = {
196+
filePath: "test.cgi",
197+
fullFilePath: "/var/www/cgi-bin/test.cgi",
198+
trustProxy: true,
199+
};
200+
201+
const env = createEnvObject(req, extraInfo);
202+
assert.strictEqual(env.SERVER_NAME, "127.0.0.1");
203+
assert.strictEqual(env.SERVER_PORT, "8080");
204+
assert.strictEqual(env.REMOTE_ADDR, "192.168.1.10");
205+
});
175206
});
176207
describe("parseResponse", () => {
177208
it("should return a parsed response", async () => {
@@ -342,7 +373,10 @@ script.cgi`);
342373
});
343374
describe("isAbsoluteWindowsPath", () => {
344375
it("should return true", async () => {
345-
assert.strictEqual(isAbsoluteWindowsPath("C:\\Program Files\\perl"), true);
376+
assert.strictEqual(
377+
isAbsoluteWindowsPath("C:\\Program Files\\perl"),
378+
true
379+
);
346380
assert.strictEqual(isAbsoluteWindowsPath("C:/Program Files/perl"), true);
347381
assert.strictEqual(isAbsoluteWindowsPath("\\\\volume\\perl"), true);
348382
});

0 commit comments

Comments
 (0)