From ee38509fe37bf3c99139936327f6c47a48e7778a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 26 Feb 2026 22:39:07 +0100 Subject: [PATCH] http2: validate non-link headers in writeEarlyHints Validate header names and values for non-link hints passed to writeEarlyHints() in the HTTP/2 compat layer using assertValidHeader() and checkIsHttpToken(), consistent with the HTTP/1.1 validation added in https://github.com/nodejs/node/pull/61897. Previously, hints were forwarded into the headers object without any validation, allowing invalid characters in header names/values to surface as opaque errors deeper in the HTTP/2 stack. Signed-off-by: Matteo Collina --- lib/internal/http2/compat.js | 6 +- ...compat-write-early-hints-invalid-header.js | 60 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-http2-compat-write-early-hints-invalid-header.js diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 86dc7dc047fd78..a85143e90cab0f 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -921,7 +921,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-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(); + })); + })); +}