Skip to content

Commit 121e3b8

Browse files
addaleaxBethGriggs
authored andcommitted
http2: add support for sensitive headers
Add support for “sensitive”/“never-indexed” HTTP2 headers. Fixes: #34091 PR-URL: #34145 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Denys Otrishko <[email protected]>
1 parent 3212da1 commit 121e3b8

11 files changed

+166
-25
lines changed

doc/api/http2.md

+38
Original file line numberDiff line numberDiff line change
@@ -2595,6 +2595,17 @@ added: v8.4.0
25952595
Returns a [HTTP/2 Settings Object][] containing the deserialized settings from
25962596
the given `Buffer` as generated by `http2.getPackedSettings()`.
25972597

2598+
### `http2.sensitiveHeaders`
2599+
<!-- YAML
2600+
added: REPLACEME
2601+
-->
2602+
2603+
* {symbol}
2604+
2605+
This symbol can be set as a property on the HTTP/2 headers object with an array
2606+
value in order to provide a list of headers considered sensitive.
2607+
See [Sensitive headers][] for more details.
2608+
25982609
### Headers object
25992610

26002611
Headers are represented as own-properties on JavaScript objects. The property
@@ -2643,6 +2654,32 @@ server.on('stream', (stream, headers) => {
26432654
});
26442655
```
26452656

2657+
#### Sensitive headers
2658+
2659+
HTTP2 headers can be marked as sensitive, which means that the HTTP/2
2660+
header compression algorithm will never index them. This can make sense for
2661+
header values with low entropy and that may be considered valuable to an
2662+
attacker, for example `Cookie` or `Authorization`. To achieve this, add
2663+
the header name to the `[http2.sensitiveHeaders]` property as an array:
2664+
2665+
```js
2666+
const headers = {
2667+
':status': '200',
2668+
'content-type': 'text-plain',
2669+
'cookie': 'some-cookie',
2670+
'other-sensitive-header': 'very secret data',
2671+
[http2.sensitiveHeaders]: ['cookie', 'other-sensitive-header']
2672+
};
2673+
2674+
stream.respond(headers);
2675+
```
2676+
2677+
For some headers, such as `Authorization` and short `Cookie` headers,
2678+
this flag is set automatically.
2679+
2680+
This property is also set for received headers. It will contain the names of
2681+
all headers marked as sensitive, including ones marked that way automatically.
2682+
26462683
### Settings object
26472684
<!-- YAML
26482685
added: v8.4.0
@@ -3814,3 +3851,4 @@ following additional properties:
38143851
[`tls.createServer()`]: tls.md#tls_tls_createserver_options_secureconnectionlistener
38153852
[`writable.writableFinished`]: stream.md#stream_writable_writablefinished
38163853
[error code]: #http2_error_codes_for_rst_stream_and_goaway
3854+
[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
@@ -129,6 +129,7 @@ const {
129129
getSettings,
130130
getStreamState,
131131
isPayloadMeaningless,
132+
kSensitiveHeaders,
132133
kSocket,
133134
kRequest,
134135
kProxySocket,
@@ -312,7 +313,7 @@ function emit(self, ...args) {
312313
// create the associated Http2Stream instance and emit the 'stream'
313314
// event. If the stream is not new, emit the 'headers' event to pass
314315
// the block of headers on.
315-
function onSessionHeaders(handle, id, cat, flags, headers) {
316+
function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
316317
const session = this[kOwner];
317318
if (session.destroyed)
318319
return;
@@ -326,7 +327,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
326327
let stream = streams.get(id);
327328

328329
// Convert the array of header name value pairs into an object
329-
const obj = toHeaderObject(headers);
330+
const obj = toHeaderObject(headers, sensitiveHeaders);
330331

331332
if (stream === undefined) {
332333
if (session.closed) {
@@ -2351,6 +2352,7 @@ function processHeaders(oldHeaders, options) {
23512352
headers[key] = oldHeaders[key];
23522353
}
23532354
}
2355+
headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders];
23542356
}
23552357

23562358
const statusCode =
@@ -2373,6 +2375,10 @@ function processHeaders(oldHeaders, options) {
23732375
if (statusCode < 200 || statusCode > 599)
23742376
throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]);
23752377

2378+
const neverIndex = headers[kSensitiveHeaders];
2379+
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
2380+
throw new ERR_INVALID_OPT_VALUE('headers[http2.neverIndex]', neverIndex);
2381+
23762382
return headers;
23772383
}
23782384

@@ -3333,6 +3339,7 @@ module.exports = {
33333339
getDefaultSettings,
33343340
getPackedSettings,
33353341
getUnpackedSettings,
3342+
sensitiveHeaders: kSensitiveHeaders,
33363343
Http2Session,
33373344
Http2Stream,
33383345
Http2ServerRequest,

lib/internal/http2/util.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const {
99
ObjectKeys,
1010
Set,
1111
String,
12+
StringFromCharCode,
13+
StringPrototypeToLowerCase,
1214
Symbol,
1315
} = primordials;
1416

@@ -25,11 +27,14 @@ const {
2527
hideStackFrames
2628
} = require('internal/errors');
2729

30+
const kSensitiveHeaders = Symbol('nodejs.http2.sensitiveHeaders');
2831
const kSocket = Symbol('socket');
2932
const kProxySocket = Symbol('proxySocket');
3033
const kRequest = Symbol('request');
3134

3235
const {
36+
NGHTTP2_NV_FLAG_NONE,
37+
NGHTTP2_NV_FLAG_NO_INDEX,
3338
NGHTTP2_SESSION_CLIENT,
3439
NGHTTP2_SESSION_SERVER,
3540

@@ -454,6 +459,9 @@ const assertValidPseudoHeaderTrailer = hideStackFrames((key) => {
454459
throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
455460
});
456461

462+
const emptyArray = [];
463+
const kNeverIndexFlag = StringFromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
464+
const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
457465
function mapToHeaders(map,
458466
assertValuePseudoHeader = assertValidPseudoHeader) {
459467
let ret = '';
@@ -466,6 +474,8 @@ function mapToHeaders(map,
466474
let value;
467475
let isSingleValueHeader;
468476
let err;
477+
const neverIndex =
478+
(map[kSensitiveHeaders] || emptyArray).map(StringPrototypeToLowerCase);
469479
for (i = 0; i < keys.length; ++i) {
470480
key = keys[i];
471481
value = map[key];
@@ -494,11 +504,12 @@ function mapToHeaders(map,
494504
throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
495505
singles.add(key);
496506
}
507+
const flags = neverIndex.includes(key) ? kNeverIndexFlag : kNoHeaderFlags;
497508
if (key[0] === ':') {
498509
err = assertValuePseudoHeader(key);
499510
if (err !== undefined)
500511
throw err;
501-
ret = `${key}\0${value}\0${ret}`;
512+
ret = `${key}\0${value}\0${flags}${ret}`;
502513
count++;
503514
continue;
504515
}
@@ -508,12 +519,12 @@ function mapToHeaders(map,
508519
if (isArray) {
509520
for (j = 0; j < value.length; ++j) {
510521
const val = String(value[j]);
511-
ret += `${key}\0${val}\0`;
522+
ret += `${key}\0${val}\0${flags}`;
512523
}
513524
count += value.length;
514525
continue;
515526
}
516-
ret += `${key}\0${value}\0`;
527+
ret += `${key}\0${value}\0${flags}`;
517528
count++;
518529
}
519530

@@ -552,7 +563,7 @@ const assertWithinRange = hideStackFrames(
552563
}
553564
);
554565

555-
function toHeaderObject(headers) {
566+
function toHeaderObject(headers, sensitiveHeaders) {
556567
const obj = ObjectCreate(null);
557568
for (var n = 0; n < headers.length; n += 2) {
558569
const name = headers[n];
@@ -593,6 +604,7 @@ function toHeaderObject(headers) {
593604
}
594605
}
595606
}
607+
obj[kSensitiveHeaders] = sensitiveHeaders;
596608
return obj;
597609
}
598610

@@ -621,6 +633,7 @@ module.exports = {
621633
getSettings,
622634
getStreamState,
623635
isPayloadMeaningless,
636+
kSensitiveHeaders,
624637
kSocket,
625638
kProxySocket,
626639
kRequest,

src/node_http2.cc

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

1218-
std::vector<Local<Value>> headers_v(stream->headers_count() * 2);
1218+
MaybeStackBuffer<Local<Value>, 64> headers_v(stream->headers_count() * 2);
1219+
MaybeStackBuffer<Local<Value>, 32> sensitive_v(stream->headers_count());
1220+
size_t sensitive_count = 0;
1221+
12191222
stream->TransferHeaders([&](const Http2Header& header, size_t i) {
12201223
headers_v[i * 2] = header.GetName(this).ToLocalChecked();
12211224
headers_v[i * 2 + 1] = header.GetValue(this).ToLocalChecked();
1225+
if (header.flags() & NGHTTP2_NV_FLAG_NO_INDEX)
1226+
sensitive_v[sensitive_count++] = headers_v[i * 2];
12221227
});
12231228
CHECK_EQ(stream->headers_count(), 0);
12241229

12251230
DecrementCurrentSessionMemory(stream->current_headers_length_);
12261231
stream->current_headers_length_ = 0;
12271232

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

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

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+
}

0 commit comments

Comments
 (0)