http: fix validation of "Link" header

Updated regex for "Link" header validation to better match the
specification in RFC 8288 section 3. Does not check for valid URI
format but handles the rest of the header more permissively than
before. Alternative to another outstanding PR that disables validation
entirely.

Fixes: https://github.com/nodejs/node/issues/46453
Refs: https://www.rfc-editor.org/rfc/rfc8288.html#section-3
Refs: https://github.com/nodejs/node/pull/46464
PR-URL: https://github.com/nodejs/node/pull/46466
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
This commit is contained in:
Steve Herzog 2023-02-23 03:54:33 -06:00 committed by GitHub
parent d12d8cd578
commit d0531eb752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 49 additions and 23 deletions

View File

@ -459,7 +459,15 @@ function validateUnion(value, name, union) {
}
}
const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title|crossorigin|disabled|fetchpriority|rel|referrerpolicy)=(")?[^;"]*\2)?$/;
/*
The rules for the Link header field are described here:
https://www.rfc-editor.org/rfc/rfc8288.html#section-3
This regex validates any string surrounded by angle brackets
(not necessarily a valid URI reference) followed by zero or more
link-params separated by semicolons.
*/
const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/;
/**
* @param {any} value

View File

@ -6,28 +6,44 @@ const debug = require('node:util').debuglog('test');
const testResBody = 'response content\n';
const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints('bad argument type');
{
const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
assert.throws(() => {
res.writeEarlyHints('bad argument type');
}, (err) => err.code === 'ERR_INVALID_ARG_TYPE');
debug('Server sending full response...');
res.end(testResBody);
}));
assert.throws(() => {
res.writeEarlyHints({
link: '</>; '
});
}, (err) => err.code === 'ERR_INVALID_ARG_VALUE');
server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});
assert.throws(() => {
res.writeEarlyHints({
link: 'rel=preload; </scripts.js>'
});
}, (err) => err.code === 'ERR_INVALID_ARG_VALUE');
req.end();
debug('Client sending request...');
assert.throws(() => {
res.writeEarlyHints({
link: 'invalid string'
});
}, (err) => err.code === 'ERR_INVALID_ARG_VALUE');
req.on('information', common.mustNotCall());
debug('Server sending full response...');
res.end(testResBody);
server.close();
}));
process.on('uncaughtException', (err) => {
debug(`Caught an exception: ${JSON.stringify(err)}`);
if (err.name === 'AssertionError') throw err;
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
process.exit(0);
});
}));
server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});
req.end();
debug('Client sending request...');
req.on('information', common.mustNotCall());
}));
}

View File

@ -58,7 +58,8 @@ const testResBody = 'response content\n';
res.writeEarlyHints({
link: [
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
'</scripts.js>; crossorigin; rel=preload; as=script',
'</scripts.js>; rel=preload; as=script; crossorigin',
]
});
@ -75,7 +76,8 @@ const testResBody = 'response content\n';
req.on('information', common.mustCall((res) => {
assert.strictEqual(
res.headers.link,
'</styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script'
'</styles.css>; rel=preload; as=style, </scripts.js>; crossorigin; ' +
'rel=preload; as=script, </scripts.js>; rel=preload; as=script; crossorigin'
);
}));