Skip to content

Commit db6b9a4

Browse files
committed
http2: reject non-latin1 client request values
1 parent 2c024cd commit db6b9a4

5 files changed

Lines changed: 76 additions & 18 deletions

File tree

doc/api/http2.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,10 @@ For HTTP/2 Client `Http2Session` instances only, the `http2session.request()`
959959
creates and returns an `Http2Stream` instance that can be used to send an
960960
HTTP/2 request to the connected server.
961961

962+
When sending a request, header values must not contain characters outside the
963+
`latin1` encoding. The `:path` pseudo-header must not contain unescaped
964+
characters.
965+
962966
When a `ClientHttp2Session` is first created, the socket may not yet be
963967
connected. if `clienthttp2session.request()` is called during this time, the
964968
actual request will be deferred until the socket is ready to go.

lib/internal/http2/util.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@ const {
1818
Symbol,
1919
} = primordials;
2020

21+
const {
22+
_checkInvalidHeaderChar: checkInvalidHeaderChar,
23+
_checkIsHttpToken: checkIsHttpToken,
24+
} = require('_http_common');
25+
2126
const binding = internalBinding('http2');
2227
const {
2328
codes: {
2429
ERR_HTTP2_HEADER_SINGLE_VALUE,
2530
ERR_HTTP2_INVALID_CONNECTION_HEADERS,
31+
ERR_HTTP2_INVALID_HEADER_VALUE,
2632
ERR_HTTP2_INVALID_PSEUDOHEADER: { HideStackFramesError: ERR_HTTP2_INVALID_PSEUDOHEADER },
2733
ERR_HTTP2_INVALID_SETTING_VALUE,
2834
ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS,
2935
ERR_INVALID_ARG_TYPE,
3036
ERR_INVALID_HTTP_TOKEN,
37+
ERR_UNESCAPED_CHARACTERS,
3138
},
3239
getMessage,
3340
hideStackFrames,
@@ -113,6 +120,18 @@ const kValidPseudoHeaders = new SafeSet([
113120
HTTP2_HEADER_PROTOCOL,
114121
]);
115122

123+
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
124+
125+
function assertValidHeaderValue(name, value) {
126+
if (name === HTTP2_HEADER_PATH && INVALID_PATH_REGEX.test(value)) {
127+
throw new ERR_UNESCAPED_CHARACTERS('Request path');
128+
}
129+
130+
if (checkInvalidHeaderChar(value)) {
131+
throw new ERR_HTTP2_INVALID_HEADER_VALUE(value, name);
132+
}
133+
}
134+
116135
// This set contains headers that are permitted to have only a single
117136
// value. Multiple instances must not be specified.
118137
const kSingleValueHeaders = new SafeSet([
@@ -595,6 +614,8 @@ function mapToHeaders(map,
595614
let pseudoHeaders = '';
596615
let count = 0;
597616
const keys = ObjectKeys(map);
617+
const shouldValidateHeaderValue =
618+
assertValuePseudoHeader === assertValidPseudoHeader;
598619
const singles = new SafeSet();
599620
let i, j;
600621
let isArray;
@@ -640,6 +661,9 @@ function mapToHeaders(map,
640661
err = assertValuePseudoHeader(key);
641662
if (err !== undefined)
642663
throw err;
664+
if (shouldValidateHeaderValue) {
665+
assertValidHeaderValue(key, value);
666+
}
643667
pseudoHeaders += `${key}\0${value}\0${flags}`;
644668
count++;
645669
continue;
@@ -653,11 +677,17 @@ function mapToHeaders(map,
653677
if (isArray) {
654678
for (j = 0; j < value.length; ++j) {
655679
const val = String(value[j]);
680+
if (shouldValidateHeaderValue) {
681+
assertValidHeaderValue(key, val);
682+
}
656683
headers += `${key}\0${val}\0${flags}`;
657684
}
658685
count += value.length;
659686
continue;
660687
}
688+
if (shouldValidateHeaderValue) {
689+
assertValidHeaderValue(key, value);
690+
}
661691
headers += `${key}\0${value}\0${flags}`;
662692
count++;
663693
}

test/parallel/test-http2-client-unescaped-path.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
const common = require('../common');
44
if (!common.hasCrypto)
55
common.skip('missing crypto');
6+
const assert = require('assert');
67
const http2 = require('http2');
7-
const Countdown = require('../common/countdown');
88

99
const server = http2.createServer();
1010

@@ -14,24 +14,22 @@ const count = 32;
1414

1515
server.listen(0, common.mustCall(() => {
1616
const client = http2.connect(`http://localhost:${server.address().port}`);
17-
client.setMaxListeners(33);
1817

19-
const countdown = new Countdown(count + 1, () => {
20-
server.close();
21-
client.close();
22-
});
23-
24-
// nghttp2 will catch the bad header value for us.
25-
function doTest(i) {
26-
const req = client.request({ ':path': `bad${String.fromCharCode(i)}path` });
27-
req.on('error', common.expectsError({
28-
code: 'ERR_HTTP2_STREAM_ERROR',
29-
name: 'Error',
30-
message: 'Stream closed with error code NGHTTP2_PROTOCOL_ERROR'
31-
}));
32-
req.on('close', common.mustCall(() => countdown.dec()));
18+
for (let i = 0; i <= count; i += 1) {
19+
const path = `bad${String.fromCharCode(i)}path`;
20+
assert.throws(() => client.request({ ':path': path }), {
21+
code: 'ERR_UNESCAPED_CHARACTERS',
22+
name: 'TypeError',
23+
message: 'Request path contains unescaped characters'
24+
});
3325
}
3426

35-
for (let i = 0; i <= count; i += 1)
36-
doTest(i);
27+
assert.throws(() => client.request({ ':path': 'bad\u0100path' }), {
28+
code: 'ERR_UNESCAPED_CHARACTERS',
29+
name: 'TypeError',
30+
message: 'Request path contains unescaped characters'
31+
});
32+
33+
client.close();
34+
server.close();
3735
}));

test/parallel/test-http2-invalidheaderfields-client.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ server1.listen(0, common.mustCall(() => {
1414
}, {
1515
code: 'ERR_INVALID_HTTP_TOKEN'
1616
});
17+
assert.throws(() => {
18+
session.request({ 'x-bad-char': 'oʊmɪɡə' });
19+
}, {
20+
code: 'ERR_HTTP2_INVALID_HEADER_VALUE'
21+
});
1722
session.on('error', common.mustCall((e) => {
1823
assert.strictEqual(e.code, 'ERR_INVALID_HTTP_TOKEN');
24+
session.close();
1925
server1.close();
2026
}));
2127
}));

test/parallel/test-http2-util-headers-list.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,26 @@ mapToHeaders({ te: ['trailers'] });
336336
// Refs: https://github.com/nodejs/node/issues/29858
337337
mapToHeaders({ [HTTP2_HEADER_HOST]: 'abc' });
338338

339+
assert.throws(() => buildNgHeaderString(
340+
{ ':path': 'bad\u0100path' },
341+
assertValidPseudoHeader,
342+
true
343+
), {
344+
code: 'ERR_UNESCAPED_CHARACTERS',
345+
name: 'TypeError',
346+
message: 'Request path contains unescaped characters'
347+
});
348+
349+
assert.throws(() => buildNgHeaderString(
350+
{ 'x-bad-char': 'oʊmɪɡə' },
351+
assertValidPseudoHeader,
352+
true
353+
), {
354+
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
355+
name: 'TypeError',
356+
message: 'Invalid value "oʊmɪɡə" for header "x-bad-char"'
357+
});
358+
339359
// If both are present, the latter has priority
340360
assert.strictEqual(getAuthority({
341361
[HTTP2_HEADER_AUTHORITY]: 'abc',

0 commit comments

Comments
 (0)