Skip to content

Commit 9c20674

Browse files
authored
Do not allow CRLF in headers (#932)
While the underlying HTTP server does in fact throw an error when the response will have invalid HTTP headers, this happens at runtime at the time of the first request rather than immediately after the process is started. This should be considered a fatal configuration error, causing the process to exit immediately.
1 parent a8cd44c commit 9c20674

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

lib/core/opts.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,26 @@ module.exports = (opts) => {
2929
return typeof opts[k] !== 'undefined' && opts[k] !== null;
3030
}
3131

32+
function validateNoCRLF(str) {
33+
if (typeof str === 'string' && (str.includes('\r') || str.includes('\n'))) {
34+
throw new Error('Header is not a string or contains CRLF');
35+
}
36+
}
37+
38+
function addHeader(key, value) {
39+
validateNoCRLF(key);
40+
validateNoCRLF(value);
41+
headers[key] = value;
42+
}
43+
3244
function setHeader(str) {
45+
validateNoCRLF(str);
46+
3347
const m = /^(.+?)\s*:\s*(.*)$/.exec(str);
3448
if (!m) {
35-
headers[str] = true;
49+
addHeader(str, true); // Use addHeader instead of direct assignment
3650
} else {
37-
headers[m[1]] = m[2];
51+
addHeader(m[1], m[2]); // Use addHeader instead of direct assignment
3852
}
3953
}
4054

@@ -135,7 +149,7 @@ module.exports = (opts) => {
135149
opts[k].forEach(setHeader);
136150
} else if (opts[k] && typeof opts[k] === 'object') {
137151
Object.keys(opts[k]).forEach((key) => {
138-
headers[key] = opts[k][key];
152+
addHeader(key, opts[k][key]); // Uses same validation path
139153
});
140154
} else {
141155
setHeader(opts[k]);

lib/http-server.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ function HttpServer(options) {
4242
}
4343
}
4444

45+
// CRLF injection prevention
46+
for ( const [key, value] of Object.entries(options.headers || {}) ) {
47+
if (typeof key !== 'string' || typeof value !== 'string') {
48+
throw new Error('Header is not a string or contains CRLF');
49+
}
50+
if (key.includes('\r') || key.includes('\n') || value.includes('\r') || value.includes('\n')) {
51+
throw new Error('Header is not a string or contains CRLF');
52+
}
53+
}
54+
4555
this.headers = options.headers || {};
4656
this.headers['Accept-Ranges'] = 'bytes';
4757

test/headers.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,23 @@ test('H array', (t) => {
8484
t.equal(headers.beep, 'boop');
8585
});
8686
});
87+
88+
// CRLF injection prevention
89+
test('CRLF injection prevention', (t) => {
90+
t.plan(1);
91+
92+
t.throws(() => {
93+
const server = http.createServer(
94+
ecstatic({
95+
root,
96+
H: [
97+
'X-CRLF-Injection: X\r\nContent-Type: text/html',
98+
],
99+
autoIndex: true,
100+
defaultExt: 'html',
101+
})
102+
);
103+
104+
server.close();
105+
}, /Header is not a string or contains CRLF/);
106+
});

0 commit comments

Comments
 (0)