Skip to content

Commit

Permalink
http: server check Host header, to meet RFC 7230 5.4 requirement
Browse files Browse the repository at this point in the history
Author:
PR-URL: nodejs#45597
Fixes: nodejs#39033
Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
Co-authored-by: mscdex <mscdex@users.noreply.github.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
3 people authored and ShogunPanda committed Nov 28, 2022
1 parent 71ff89f commit 4587866
Show file tree
Hide file tree
Showing 46 changed files with 156 additions and 42 deletions.
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) {

// 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

0 comments on commit 4587866

Please sign in to comment.