Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: server check Host header in request, to meet RFC 7230 5.4 requirement #45597

Closed
wants to merge 14 commits into from
Closed
4 changes: 4 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -3185,6 +3185,10 @@ changes:
* `uniqueHeaders` {Array} A list of response headers that should be sent only
once. If the header's value is an array, the items will be joined
using `; `.
* `requireHostHeader` {boolean} It forces the server to respond with
a 400 (Bad Request) status code to any HTTP/1.1 request message
that lacks a Host header (as mandated by the specification).
**Default:** `true`.

* `requestListener` {Function}

Expand Down
20 changes: 19 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,14 @@ function storeHTTPOptions(options) {
} else {
this.connectionsCheckingInterval = 30_000; // 30 seconds
}

const requireHostHeader = options.requireHostHeader;
if (requireHostHeader !== undefined) {
validateBoolean(requireHostHeader, 'options.requireHostHeader');
this.requireHostHeader = requireHostHeader;
} else {
this.requireHostHeader = true;
}
}

function setupConnectionsTracking(server) {
Expand Down Expand Up @@ -1022,7 +1030,18 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {

let handled = false;


if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
marco-ippolito marked this conversation as resolved.
Show resolved Hide resolved

// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any
// HTTP/1.1 request message that lacks a Host header field
if (server.requireHostHeader && req.headers.host === undefined) {
res.writeHead(400, ['Connection', 'close']);
res.end();
return 0;
}

const isRequestsLimitSet = (
typeof server.maxRequestsPerSocket === 'number' &&
server.maxRequestsPerSocket > 0
Expand All @@ -1045,7 +1064,6 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {

if (RegExpPrototypeExec(continueExpression, req.headers.expect) !== null) {
res._expect_continue = true;

if (server.listenerCount('checkContinue') > 0) {
server.emit('checkContinue', req, res);
} else {
Expand Down
1 change: 1 addition & 0 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ let maxHeaderSize;
* ServerResponse?: ServerResponse;
* insecureHTTPParser?: boolean;
* maxHeaderSize?: number;
* requireHostHeader?: boolean
* }} [opts]
* @param {Function} [requestListener]
* @returns {Server}
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-chunked-304.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function test(statusCode) {
const conn = net.createConnection(
server.address().port,
common.mustCall(() => {
conn.write('GET / HTTP/1.1\r\n\r\n');
conn.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');

let resp = '';
conn.setEncoding('utf8');
Expand Down
17 changes: 13 additions & 4 deletions test/parallel/test-http-client-headers-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ function execute(options) {
const expectHeaders = {
'x-foo': 'boom',
'cookie': 'a=1; b=2; c=3',
'connection': 'keep-alive'
'connection': 'keep-alive',
'host': 'example.com',
};

// no Host header when you set headers an array
Expand Down Expand Up @@ -43,13 +44,20 @@ function execute(options) {
// Should be the same except for implicit Host header on the first two
execute({ headers: { 'x-foo': 'boom', 'cookie': 'a=1; b=2; c=3' } });
execute({ headers: { 'x-foo': 'boom', 'cookie': [ 'a=1', 'b=2', 'c=3' ] } });
execute({ headers: [[ 'x-foo', 'boom' ], [ 'cookie', 'a=1; b=2; c=3' ]] });
execute({ headers: [
[ 'x-foo', 'boom' ], [ 'cookie', [ 'a=1', 'b=2', 'c=3' ]],
[ 'x-foo', 'boom' ],
[ 'cookie', 'a=1; b=2; c=3' ],
[ 'Host', 'example.com' ],
] });
execute({ headers: [
[ 'x-foo', 'boom' ],
[ 'cookie', [ 'a=1', 'b=2', 'c=3' ]],
[ 'Host', 'example.com' ],
] });
execute({ headers: [
[ 'x-foo', 'boom' ], [ 'cookie', 'a=1' ],
[ 'cookie', 'b=2' ], [ 'cookie', 'c=3'],
[ 'cookie', 'b=2' ], [ 'cookie', 'c=3' ],
[ 'Host', 'example.com'],
] });

// Authorization and Host header both missing from the second
Expand All @@ -58,4 +66,5 @@ execute({ auth: 'foo:bar', headers:
execute({ auth: 'foo:bar', headers: [
[ 'x-foo', 'boom' ], [ 'cookie', 'a=1' ],
[ 'cookie', 'b=2' ], [ 'cookie', 'c=3'],
[ 'Host', 'example.com'],
] });
6 changes: 3 additions & 3 deletions test/parallel/test-http-content-length.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ const server = http.createServer(function(req, res) {

switch (req.url.substr(1)) {
case 'multiple-writes':
delete req.headers.host;
assert.deepStrictEqual(req.headers, expectedHeadersMultipleWrites);
res.write('hello');
res.end('world');
break;
case 'end-with-data':
delete req.headers.host;
assert.deepStrictEqual(req.headers, expectedHeadersEndWithData);
res.end('hello world');
break;
case 'empty':
delete req.headers.host;
assert.deepStrictEqual(req.headers, expectedHeadersEndNoData);
res.end();
break;
Expand All @@ -56,7 +59,6 @@ server.listen(0, function() {
path: '/multiple-writes'
});
req.removeHeader('Date');
req.removeHeader('Host');
req.write('hello ');
req.end('world');
req.on('response', function(res) {
Expand All @@ -70,7 +72,6 @@ server.listen(0, function() {
path: '/end-with-data'
});
req.removeHeader('Date');
req.removeHeader('Host');
req.end('hello world');
req.on('response', function(res) {
assert.deepStrictEqual(res.headers, { ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' });
Expand All @@ -83,7 +84,6 @@ server.listen(0, function() {
path: '/empty'
});
req.removeHeader('Date');
req.removeHeader('Host');
req.end();
req.on('response', function(res) {
assert.deepStrictEqual(res.headers, { ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' });
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-header-badrequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ server.listen(0, mustCall(() => {
let received = '';

c.on('connect', mustCall(() => {
c.write('GET /blah HTTP/1.1\r\n\r\n');
c.write('GET /blah HTTP/1.1\r\nHost: example.com\r\n\r\n');
}));
c.on('data', mustCall((data) => {
received += data.toString();
Expand Down
4 changes: 4 additions & 0 deletions test/parallel/test-http-insecure-parser-per-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const MakeDuplexPair = require('../common/duplexpair');

serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
Expand All @@ -39,6 +40,7 @@ const MakeDuplexPair = require('../common/duplexpair');

serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
Expand All @@ -62,6 +64,7 @@ const MakeDuplexPair = require('../common/duplexpair');
server.emit('connection', serverSide);

clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
}
Expand All @@ -77,6 +80,7 @@ const MakeDuplexPair = require('../common/duplexpair');
server.emit('connection', serverSide);

clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
}
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-http-insecure-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ server.listen(0, common.mustCall(function() {
client.write(
'GET / HTTP/1.1\r\n' +
'Content-Type: text/te\x08t\r\n' +
'Host: example.com' +
'Connection: close\r\n\r\n');
}
);
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-http-keep-alive-drop-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const assert = require('assert');
function request(socket) {
socket.write('GET / HTTP/1.1\r\n');
socket.write('Connection: keep-alive\r\n');
socket.write('Host: localhost\r\n');
socket.write('\r\n\r\n');
}

Expand Down
2 changes: 2 additions & 0 deletions test/parallel/test-http-keep-alive-max-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ function writeRequest(socket, withBody) {
socket.write('POST / HTTP/1.1\r\n');
socket.write('Connection: keep-alive\r\n');
socket.write('Content-Type: text/plain\r\n');
socket.write('Host: localhost\r\n');
socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`);
socket.write(`${bodySent}\r\n`);
socket.write('\r\n\r\n');
} else {
socket.write('GET / HTTP/1.1\r\n');
socket.write('Connection: keep-alive\r\n');
socket.write('Host: localhost\r\n');
socket.write('\r\n\r\n');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function assertResponse(headers, body, expectClosed) {

function writeRequest(socket) {
socket.write('POST / HTTP/1.1\r\n');
socket.write('Host: localhost\r\n');
socket.write('Connection: keep-alive\r\n');
socket.write('Content-Type: text/plain\r\n');
socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`);
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-malformed-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ server.listen(0);
server.on('listening', function() {
const c = net.createConnection(this.address().port);
c.on('connect', function() {
c.write('GET /hello?foo=%99bar HTTP/1.1\r\n\r\n');
c.write('GET /hello?foo=%99bar HTTP/1.1\r\nHost: example.com\r\n\r\n');
c.end();
});
});
1 change: 1 addition & 0 deletions test/parallel/test-http-max-header-size-per-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const MakeDuplexPair = require('../common/duplexpair');
server.emit('connection', serverSide);

clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'\r\n\r\n');
}
Expand Down
8 changes: 5 additions & 3 deletions test/parallel/test-http-max-headers-count.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ const http = require('http');
let requests = 0;
let responses = 0;

const headers = {};
const headers = {
host: 'example.com'
};
const N = 100;
for (let i = 0; i < N; ++i) {
headers[`key${i}`] = i;
Expand Down Expand Up @@ -56,8 +58,8 @@ server.maxHeadersCount = max;
server.listen(0, function() {
const maxAndExpected = [ // for client
[20, 20],
[1200, 103],
[0, N + 3], // Connection, Date and Transfer-Encoding
[1200, 104],
[0, N + 4], // Host and Connection
];
doRequest();

Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-pipeline-assertionerror-finish.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const server = http
.listen(0, function() {
const s = net.connect(this.address().port);

const big = 'GET / HTTP/1.1\r\n\r\n'.repeat(COUNT);
const big = 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.repeat(COUNT);

s.write(big);
s.resume();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const server = http
});
})
.listen(0, function() {
const req = 'GET / HTTP/1.1\r\n\r\n'.repeat(COUNT);
const req = 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.repeat(COUNT);
client = net.connect(this.address().port, function() {
client.write(req);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const server = http
.listen(0, () => {
const s = net.connect(server.address().port);
more = () => {
s.write('GET / HTTP/1.1\r\n\r\n');
s.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
};
done = () => {
s.write(
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-http-req-close-robust-from-tampering.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ server.listen(0, common.mustCall(() => {

const req = [
'POST / HTTP/1.1',
'Host: example.com',
'Content-Length: 11',
'',
'hello world',
Expand Down
41 changes: 41 additions & 0 deletions test/parallel/test-http-request-host-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');

{
const server = http.createServer(common.mustNotCall((req, res) => {
res.writeHead(200);
res.end();
}));

// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any
// HTTP/1.1 request message that lacks a Host header field
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port, headers: [] }, (res) => {
assert.strictEqual(res.statusCode, 400);
assert.strictEqual(res.headers.connection, 'close');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.writeHead(200, ['test', '1']);
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port, headers: [] }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.test, '1');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
2 changes: 1 addition & 1 deletion test/parallel/test-http-response-splitting.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const server = http.createServer((req, res) => {
res.end('ok');
});
server.listen(0, () => {
const end = 'HTTP/1.1\r\n\r\n';
const end = 'HTTP/1.1\r\nHost: example.com\r\n\r\n';
const client = net.connect({ port: server.address().port }, () => {
client.write(`GET ${str} ${end}`);
client.write(`GET / ${end}`);
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-server-close-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ server.listen(0, function() {

client2.on('close', common.mustCall());

client2.write('GET / HTTP/1.1\r\n\r\n');
client2.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
}));

client1.on('close', common.mustCall());
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http-server-close-idle.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ server.listen(0, function() {
client2Closed = true;
}));

client2.write('GET / HTTP/1.1\r\n\r\n');
client2.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
}));

client1.on('close', common.mustCall(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { connect } = require('net');

function performRequestWithDelay(client, firstDelay, secondDelay, closeAfter) {
client.resume();
client.write('GET / HTTP/1.1\r\n');
client.write('GET / HTTP/1.1\r\nHost: example.com\r\n');

setTimeout(() => {
client.write('Connection: ');
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-http-server-headers-timeout-pipelining.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ server.listen(0, common.mustCall(() => {

// Send two requests using pipelining. Delay before finishing the second one
client.resume();
client.write('GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nConnection: ');
client.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n' +
'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: ');

// Complete the request
setTimeout(() => {
Expand Down
Loading