Skip to content

Commit e876c0c

Browse files
committedJul 15, 2020
http2: add support for sensitive headers
Add support for “sensitive”/“never-indexed” HTTP2 headers. Fixes: nodejs#34091 PR-URL: nodejs#34145 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Denys Otrishko <[email protected]>
1 parent 9ae8491 commit e876c0c

12 files changed

+167
-27
lines changed
 

‎doc/api/http2.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -2461,6 +2461,17 @@ added: v8.4.0
24612461
Returns a [HTTP/2 Settings Object][] containing the deserialized settings from
24622462
the given `Buffer` as generated by `http2.getPackedSettings()`.
24632463

2464+
### `http2.sensitiveHeaders`
2465+
<!-- YAML
2466+
added: REPLACEME
2467+
-->
2468+
2469+
* {symbol}
2470+
2471+
This symbol can be set as a property on the HTTP/2 headers object with an array
2472+
value in order to provide a list of headers considered sensitive.
2473+
See [Sensitive headers][] for more details.
2474+
24642475
### Headers object
24652476

24662477
Headers are represented as own-properties on JavaScript objects. The property
@@ -2509,6 +2520,33 @@ server.on('stream', (stream, headers) => {
25092520
});
25102521
```
25112522

2523+
<a id="http2-sensitive-headers"></a>
2524+
#### Sensitive headers
2525+
2526+
HTTP2 headers can be marked as sensitive, which means that the HTTP/2
2527+
header compression algorithm will never index them. This can make sense for
2528+
header values with low entropy and that may be considered valuable to an
2529+
attacker, for example `Cookie` or `Authorization`. To achieve this, add
2530+
the header name to the `[http2.sensitiveHeaders]` property as an array:
2531+
2532+
```js
2533+
const headers = {
2534+
':status': '200',
2535+
'content-type': 'text-plain',
2536+
'cookie': 'some-cookie',
2537+
'other-sensitive-header': 'very secret data',
2538+
[http2.sensitiveHeaders]: ['cookie', 'other-sensitive-header']
2539+
};
2540+
2541+
stream.respond(headers);
2542+
```
2543+
2544+
For some headers, such as `Authorization` and short `Cookie` headers,
2545+
this flag is set automatically.
2546+
2547+
This property is also set for received headers. It will contain the names of
2548+
all headers marked as sensitive, including ones marked that way automatically.
2549+
25122550
### Settings object
25132551
<!-- YAML
25142552
added: v8.4.0
@@ -3696,5 +3734,6 @@ following additional properties:
36963734
[`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket
36973735
[`tls.connect()`]: tls.html#tls_tls_connect_options_callback
36983736
[`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener
3699-
[error code]: #error_codes
37003737
[`writable.writableFinished`]: stream.html#stream_writable_writablefinished
3738+
[error code]: #error_codes
3739+
[Sensitive headers]: #http2-sensitive-headers

‎lib/http2.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
getDefaultSettings,
99
getPackedSettings,
1010
getUnpackedSettings,
11+
sensitiveHeaders,
1112
Http2ServerRequest,
1213
Http2ServerResponse
1314
} = require('internal/http2/core');
@@ -20,6 +21,7 @@ module.exports = {
2021
getDefaultSettings,
2122
getPackedSettings,
2223
getUnpackedSettings,
24+
sensitiveHeaders,
2325
Http2ServerRequest,
2426
Http2ServerResponse
2527
};

‎lib/internal/http2/core.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const {
123123
getSettings,
124124
getStreamState,
125125
isPayloadMeaningless,
126+
kSensitiveHeaders,
126127
kSocket,
127128
kRequest,
128129
kProxySocket,
@@ -303,7 +304,7 @@ function emit(self, ...args) {
303304
// create the associated Http2Stream instance and emit the 'stream'
304305
// event. If the stream is not new, emit the 'headers' event to pass
305306
// the block of headers on.
306-
function onSessionHeaders(handle, id, cat, flags, headers) {
307+
function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
307308
const session = this[kOwner];
308309
if (session.destroyed)
309310
return;
@@ -317,7 +318,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
317318
let stream = streams.get(id);
318319

319320
// Convert the array of header name value pairs into an object
320-
const obj = toHeaderObject(headers);
321+
const obj = toHeaderObject(headers, sensitiveHeaders);
321322

322323
if (stream === undefined) {
323324
if (session.closed) {
@@ -2232,6 +2233,7 @@ function processHeaders(oldHeaders) {
22322233
headers[key] = oldHeaders[key];
22332234
}
22342235
}
2236+
headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders];
22352237
}
22362238

22372239
const statusCode =
@@ -2251,6 +2253,10 @@ function processHeaders(oldHeaders) {
22512253
if (statusCode < 200 || statusCode > 599)
22522254
throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]);
22532255

2256+
const neverIndex = headers[kSensitiveHeaders];
2257+
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
2258+
throw new ERR_INVALID_OPT_VALUE('headers[http2.neverIndex]', neverIndex);
2259+
22542260
return headers;
22552261
}
22562262

@@ -3166,6 +3172,7 @@ module.exports = {
31663172
getDefaultSettings,
31673173
getPackedSettings,
31683174
getUnpackedSettings,
3175+
sensitiveHeaders: kSensitiveHeaders,
31693176
Http2Session,
31703177
Http2Stream,
31713178
Http2ServerRequest,

‎lib/internal/http2/util.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
ObjectCreate,
99
ObjectKeys,
1010
Set,
11+
StringPrototypeToLowerCase,
1112
Symbol,
1213
} = primordials;
1314

@@ -25,11 +26,14 @@ const {
2526
hideStackFrames
2627
} = require('internal/errors');
2728

29+
const kSensitiveHeaders = Symbol('nodejs.http2.sensitiveHeaders');
2830
const kSocket = Symbol('socket');
2931
const kProxySocket = Symbol('proxySocket');
3032
const kRequest = Symbol('request');
3133

3234
const {
35+
NGHTTP2_NV_FLAG_NONE,
36+
NGHTTP2_NV_FLAG_NO_INDEX,
3337
NGHTTP2_SESSION_CLIENT,
3438
NGHTTP2_SESSION_SERVER,
3539

@@ -443,6 +447,9 @@ const assertValidPseudoHeaderTrailer = hideStackFrames((key) => {
443447
throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
444448
});
445449

450+
const emptyArray = [];
451+
const kNeverIndexFlag = String.fromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
452+
const kNoHeaderFlags = String.fromCharCode(NGHTTP2_NV_FLAG_NONE);
446453
function mapToHeaders(map,
447454
assertValuePseudoHeader = assertValidPseudoHeader) {
448455
let ret = '';
@@ -455,6 +462,8 @@ function mapToHeaders(map,
455462
let value;
456463
let isSingleValueHeader;
457464
let err;
465+
const neverIndex =
466+
(map[kSensitiveHeaders] || emptyArray).map(StringPrototypeToLowerCase);
458467
for (i = 0; i < keys.length; ++i) {
459468
key = keys[i];
460469
value = map[key];
@@ -483,11 +492,12 @@ function mapToHeaders(map,
483492
throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
484493
singles.add(key);
485494
}
495+
const flags = neverIndex.includes(key) ? kNeverIndexFlag : kNoHeaderFlags;
486496
if (key[0] === ':') {
487497
err = assertValuePseudoHeader(key);
488498
if (err !== undefined)
489499
throw err;
490-
ret = `${key}\0${value}\0${ret}`;
500+
ret = `${key}\0${value}\0${flags}${ret}`;
491501
count++;
492502
continue;
493503
}
@@ -500,12 +510,12 @@ function mapToHeaders(map,
500510
if (isArray) {
501511
for (j = 0; j < value.length; ++j) {
502512
const val = String(value[j]);
503-
ret += `${key}\0${val}\0`;
513+
ret += `${key}\0${val}\0${flags}`;
504514
}
505515
count += value.length;
506516
continue;
507517
}
508-
ret += `${key}\0${value}\0`;
518+
ret += `${key}\0${value}\0${flags}`;
509519
count++;
510520
}
511521

@@ -544,7 +554,7 @@ const assertWithinRange = hideStackFrames(
544554
}
545555
);
546556

547-
function toHeaderObject(headers) {
557+
function toHeaderObject(headers, sensitiveHeaders) {
548558
const obj = ObjectCreate(null);
549559
for (var n = 0; n < headers.length; n = n + 2) {
550560
const name = headers[n];
@@ -585,6 +595,7 @@ function toHeaderObject(headers) {
585595
}
586596
}
587597
}
598+
obj[kSensitiveHeaders] = sensitiveHeaders;
588599
return obj;
589600
}
590601

@@ -614,6 +625,7 @@ module.exports = {
614625
getSettings,
615626
getStreamState,
616627
isPayloadMeaningless,
628+
kSensitiveHeaders,
617629
kSocket,
618630
kProxySocket,
619631
kRequest,

‎src/node_http2.cc

+14-7
Original file line numberDiff line numberDiff line change
@@ -1213,22 +1213,29 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
12131213
// this way for performance reasons (it's faster to generate and pass an
12141214
// array than it is to generate and pass the object).
12151215

1216-
std::vector<Local<Value>> headers_v(stream->headers_count() * 2);
1216+
MaybeStackBuffer<Local<Value>, 64> headers_v(stream->headers_count() * 2);
1217+
MaybeStackBuffer<Local<Value>, 32> sensitive_v(stream->headers_count());
1218+
size_t sensitive_count = 0;
1219+
12171220
stream->TransferHeaders([&](const Http2Header& header, size_t i) {
12181221
headers_v[i * 2] = header.GetName(this).ToLocalChecked();
12191222
headers_v[i * 2 + 1] = header.GetValue(this).ToLocalChecked();
1223+
if (header.flags() & NGHTTP2_NV_FLAG_NO_INDEX)
1224+
sensitive_v[sensitive_count++] = headers_v[i * 2];
12201225
});
12211226
CHECK_EQ(stream->headers_count(), 0);
12221227

12231228
DecrementCurrentSessionMemory(stream->current_headers_length_);
12241229
stream->current_headers_length_ = 0;
12251230

1226-
Local<Value> args[5] = {
1227-
stream->object(),
1228-
Integer::New(isolate, id),
1229-
Integer::New(isolate, stream->headers_category()),
1230-
Integer::New(isolate, frame->hd.flags),
1231-
Array::New(isolate, headers_v.data(), headers_v.size())};
1231+
Local<Value> args[] = {
1232+
stream->object(),
1233+
Integer::New(isolate, id),
1234+
Integer::New(isolate, stream->headers_category()),
1235+
Integer::New(isolate, frame->hd.flags),
1236+
Array::New(isolate, headers_v.out(), headers_v.length()),
1237+
Array::New(isolate, sensitive_v.out(), sensitive_count),
1238+
};
12321239
MakeCallback(env()->http2session_on_headers_function(),
12331240
arraysize(args), args);
12341241
}

‎src/node_http2.h

-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ using Nghttp2SessionCallbacksPointer =
116116

117117
struct Http2HeadersTraits {
118118
typedef nghttp2_nv nv_t;
119-
static const uint8_t kNoneFlag = NGHTTP2_NV_FLAG_NONE;
120119
};
121120

122121
struct Http2RcBufferPointerTraits {

‎src/node_http_common-inl.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ NgHeaders<T>::NgHeaders(Environment* env, v8::Local<v8::Array> headers) {
5555
return;
5656
}
5757

58-
nva[n].flags = T::kNoneFlag;
5958
nva[n].name = reinterpret_cast<uint8_t*>(p);
6059
nva[n].namelen = strlen(p);
6160
p += nva[n].namelen + 1;
6261
nva[n].value = reinterpret_cast<uint8_t*>(p);
6362
nva[n].valuelen = strlen(p);
6463
p += nva[n].valuelen + 1;
64+
nva[n].flags = *p;
65+
p++;
6566
}
6667
}
6768

@@ -189,6 +190,11 @@ size_t NgHeader<T>::length() const {
189190
return name_.len() + value_.len();
190191
}
191192

193+
template <typename T>
194+
uint8_t NgHeader<T>::flags() const {
195+
return flags_;
196+
}
197+
192198
} // namespace node
193199

194200
#endif // SRC_NODE_HTTP_COMMON_INL_H_

‎src/node_http_common.h

+2
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ struct NgHeaderBase : public MemoryRetainer {
460460
virtual std::string name() const = 0;
461461
virtual std::string value() const = 0;
462462
virtual size_t length() const = 0;
463+
virtual uint8_t flags() const = 0;
463464
virtual std::string ToString() const;
464465
};
465466

@@ -505,6 +506,7 @@ class NgHeader final : public NgHeaderBase<typename T::allocator_t> {
505506
inline std::string name() const override;
506507
inline std::string value() const override;
507508
inline size_t length() const override;
509+
inline uint8_t flags() const override;
508510

509511
void MemoryInfo(MemoryTracker* tracker) const override;
510512

‎src/quic/node_quic_http3_application.h

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ struct Http3RcBufferPointerTraits {
4242

4343
struct Http3HeadersTraits {
4444
typedef nghttp3_nv nv_t;
45-
static const uint8_t kNoneFlag = NGHTTP3_NV_FLAG_NONE;
4645
};
4746

4847
using Http3ConnectionPointer = DeleteFnPtr<nghttp3_conn, nghttp3_conn_del>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
const assert = require('assert');
6+
const http2 = require('http2');
7+
const makeDuplexPair = require('../common/duplexpair');
8+
9+
{
10+
const testData = '<h1>Hello World</h1>';
11+
const server = http2.createServer();
12+
server.on('stream', common.mustCall((stream, headers) => {
13+
stream.respond({
14+
'content-type': 'text/html',
15+
':status': 200,
16+
'cookie': 'donotindex',
17+
'not-sensitive': 'foo',
18+
'sensitive': 'bar',
19+
// sensitiveHeaders entries are case-insensitive
20+
[http2.sensitiveHeaders]: ['Sensitive']
21+
});
22+
stream.end(testData);
23+
}));
24+
25+
const { clientSide, serverSide } = makeDuplexPair();
26+
server.emit('connection', serverSide);
27+
28+
const client = http2.connect('http://localhost:80', {
29+
createConnection: common.mustCall(() => clientSide)
30+
});
31+
32+
const req = client.request({ ':path': '/' });
33+
34+
req.on('response', common.mustCall((headers) => {
35+
assert.strictEqual(headers[':status'], 200);
36+
assert.strictEqual(headers.cookie, 'donotindex');
37+
assert.deepStrictEqual(headers[http2.sensitiveHeaders],
38+
['cookie', 'sensitive']);
39+
}));
40+
41+
req.on('end', common.mustCall(() => {
42+
clientSide.destroy();
43+
clientSide.end();
44+
}));
45+
req.resume();
46+
req.end();
47+
}

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

+28-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ if (!common.hasCrypto)
99
common.skip('missing crypto');
1010
const assert = require('assert');
1111
const { mapToHeaders, toHeaderObject } = require('internal/http2/util');
12+
const { sensitiveHeaders } = require('http2');
1213
const { internalBinding } = require('internal/test/binding');
1314
const {
1415
HTTP2_HEADER_STATUS,
@@ -102,8 +103,9 @@ const {
102103

103104
assert.deepStrictEqual(
104105
mapToHeaders(headers),
105-
[ [ ':path', 'abc', ':status', '200', 'abc', '1', 'xyz', '1', 'xyz', '2',
106-
'xyz', '3', 'xyz', '4', 'bar', '1', '' ].join('\0'), 8 ]
106+
[ [ ':path', 'abc\0', ':status', '200\0', 'abc', '1\0', 'xyz', '1\0',
107+
'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', 'bar', '1\0', '' ].join('\0'),
108+
8 ]
107109
);
108110
}
109111

@@ -118,8 +120,8 @@ const {
118120

119121
assert.deepStrictEqual(
120122
mapToHeaders(headers),
121-
[ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
122-
'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
123+
[ [ ':status', '200\0', ':path', 'abc\0', 'abc', '1\0', 'xyz', '1\0',
124+
'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 7 ]
123125
);
124126
}
125127

@@ -135,8 +137,8 @@ const {
135137

136138
assert.deepStrictEqual(
137139
mapToHeaders(headers),
138-
[ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
139-
'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
140+
[ [ ':status', '200\0', ':path', 'abc\0', 'abc', '1\0', 'xyz', '1\0',
141+
'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 7 ]
140142
);
141143
}
142144

@@ -151,8 +153,8 @@ const {
151153

152154
assert.deepStrictEqual(
153155
mapToHeaders(headers),
154-
[ [ ':status', '200', ':path', 'abc', 'xyz', '1', 'xyz', '2', 'xyz', '3',
155-
'xyz', '4', '' ].join('\0'), 6 ]
156+
[ [ ':status', '200\0', ':path', 'abc\0', 'xyz', '1\0', 'xyz', '2\0',
157+
'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 6 ]
156158
);
157159
}
158160

@@ -164,7 +166,7 @@ const {
164166
};
165167
assert.deepStrictEqual(
166168
mapToHeaders(headers),
167-
[ [ 'set-cookie', 'foo=bar', '' ].join('\0'), 1 ]
169+
[ [ 'set-cookie', 'foo=bar\0', '' ].join('\0'), 1 ]
168170
);
169171
}
170172

@@ -182,6 +184,23 @@ const {
182184
});
183185
}
184186

187+
{
188+
const headers = {
189+
'abc': 1,
190+
':path': 'abc',
191+
':status': [200],
192+
':authority': [],
193+
'xyz': [1, 2, 3, 4],
194+
[sensitiveHeaders]: ['xyz']
195+
};
196+
197+
assert.deepStrictEqual(
198+
mapToHeaders(headers),
199+
[ ':status\x00200\x00\x00:path\x00abc\x00\x00abc\x001\x00\x00' +
200+
'xyz\x001\x00\x01xyz\x002\x00\x01xyz\x003\x00\x01xyz\x004\x00\x01', 7 ]
201+
);
202+
}
203+
185204
// The following are not allowed to have multiple values
186205
[
187206
HTTP2_HEADER_STATUS,

‎test/parallel/test-http2-zero-length-header.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ server.on('stream', (stream, headers) => {
1414
':method': 'GET',
1515
':path': '/',
1616
'bar': '',
17-
'__proto__': null
17+
'__proto__': null,
18+
[http2.sensitiveHeaders]: []
1819
});
1920
stream.session.destroy();
2021
server.close();

0 commit comments

Comments
 (0)
Please sign in to comment.