diff --git a/lib/_http_server.js b/lib/_http_server.js index 1d1bc37c47e109..0c0f61b2aa1316 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -55,6 +55,8 @@ const { kUniqueHeaders, parseUniqueHeadersOption, OutgoingMessage, + validateHeaderName, + validateHeaderValue, } = require('_http_outgoing'); const { kOutHeaders, @@ -339,6 +341,8 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key !== 'link') { + validateHeaderName(key); + validateHeaderValue(key, hints[key]); head += key + ': ' + hints[key] + '\r\n'; } } diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index d1728278713fad..64eefcfb91eba8 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -913,7 +913,11 @@ class Http2ServerResponse extends Stream { for (const key of ObjectKeys(hints)) { if (key !== 'link') { - headers[key] = hints[key]; + const name = key.trim().toLowerCase(); + assertValidHeader(name, hints[key]); + if (!checkIsHttpToken(name)) + throw new ERR_INVALID_HTTP_TOKEN('Header name', name); + headers[name] = hints[key]; } } diff --git a/test/parallel/test-http-early-hints-invalid-argument.js b/test/parallel/test-http-early-hints-invalid-argument.js index f776bcafa40ed3..f494dfae8323f3 100644 --- a/test/parallel/test-http-early-hints-invalid-argument.js +++ b/test/parallel/test-http-early-hints-invalid-argument.js @@ -31,6 +31,20 @@ const testResBody = 'response content\n'; }); }, (err) => err.code === 'ERR_INVALID_ARG_VALUE'); + assert.throws(() => { + res.writeEarlyHints({ + 'link': '; rel=preload; as=style', + 'x-bad\r\n': 'value', + }); + }, (err) => err.code === 'ERR_INVALID_HTTP_TOKEN'); + + assert.throws(() => { + res.writeEarlyHints({ + 'link': '; rel=preload; as=style', + 'x-custom': 'bad\r\nvalue', + }); + }, (err) => err.code === 'ERR_INVALID_CHAR'); + debug('Server sending full response...'); res.end(testResBody); server.close(); diff --git a/test/parallel/test-http2-compat-write-early-hints-invalid-header.js b/test/parallel/test-http2-compat-write-early-hints-invalid-header.js new file mode 100644 index 00000000000000..222c8e997cfb72 --- /dev/null +++ b/test/parallel/test-http2-compat-write-early-hints-invalid-header.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('node:assert'); +const http2 = require('node:http2'); +const debug = require('node:util').debuglog('test'); + +const testResBody = 'response content'; + +{ + const server = http2.createServer(); + + server.on('request', common.mustCall((req, res) => { + debug('Server sending early hints...'); + + assert.throws(() => { + res.writeEarlyHints({ + 'link': '; rel=preload; as=style', + 'x\rbad': 'value', + }); + }, (err) => err.code === 'ERR_INVALID_HTTP_TOKEN'); + + assert.throws(() => { + res.writeEarlyHints({ + 'link': '; rel=preload; as=style', + 'x-custom': undefined, + }); + }, (err) => err.code === 'ERR_HTTP2_INVALID_HEADER_VALUE'); + + debug('Server sending full response...'); + res.end(testResBody); + })); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + debug('Client sending request...'); + + req.on('headers', common.mustNotCall()); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + })); + + let data = ''; + req.on('data', common.mustCallAtLeast((d) => data += d)); + + req.on('end', common.mustCall(() => { + debug('Got full response.'); + assert.strictEqual(data, testResBody); + client.close(); + server.close(); + })); + })); +}