diff options
-rw-r--r-- | doc/api/http2.md | 38 | ||||
-rw-r--r-- | lib/http2.js | 2 | ||||
-rw-r--r-- | lib/internal/http2/core.js | 11 | ||||
-rw-r--r-- | lib/internal/http2/util.js | 21 | ||||
-rw-r--r-- | src/node_http2.cc | 21 | ||||
-rw-r--r-- | src/node_http2.h | 1 | ||||
-rw-r--r-- | src/node_http_common-inl.h | 8 | ||||
-rw-r--r-- | src/node_http_common.h | 2 | ||||
-rw-r--r-- | test/parallel/test-http2-sensitive-headers.js | 47 | ||||
-rw-r--r-- | test/parallel/test-http2-util-headers-list.js | 37 | ||||
-rw-r--r-- | test/parallel/test-http2-zero-length-header.js | 3 |
11 files changed, 166 insertions, 25 deletions
diff --git a/doc/api/http2.md b/doc/api/http2.md index 1272531e71..46a0cd3727 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -2595,6 +2595,17 @@ added: v8.4.0 Returns a [HTTP/2 Settings Object][] containing the deserialized settings from the given `Buffer` as generated by `http2.getPackedSettings()`. +### `http2.sensitiveHeaders` +<!-- YAML +added: REPLACEME +--> + +* {symbol} + +This symbol can be set as a property on the HTTP/2 headers object with an array +value in order to provide a list of headers considered sensitive. +See [Sensitive headers][] for more details. + ### Headers object Headers are represented as own-properties on JavaScript objects. The property @@ -2643,6 +2654,32 @@ server.on('stream', (stream, headers) => { }); ``` +#### Sensitive headers + +HTTP2 headers can be marked as sensitive, which means that the HTTP/2 +header compression algorithm will never index them. This can make sense for +header values with low entropy and that may be considered valuable to an +attacker, for example `Cookie` or `Authorization`. To achieve this, add +the header name to the `[http2.sensitiveHeaders]` property as an array: + +```js +const headers = { + ':status': '200', + 'content-type': 'text-plain', + 'cookie': 'some-cookie', + 'other-sensitive-header': 'very secret data', + [http2.sensitiveHeaders]: ['cookie', 'other-sensitive-header'] +}; + +stream.respond(headers); +``` + +For some headers, such as `Authorization` and short `Cookie` headers, +this flag is set automatically. + +This property is also set for received headers. It will contain the names of +all headers marked as sensitive, including ones marked that way automatically. + ### Settings object <!-- YAML added: v8.4.0 @@ -3814,3 +3851,4 @@ following additional properties: [`tls.createServer()`]: tls.md#tls_tls_createserver_options_secureconnectionlistener [`writable.writableFinished`]: stream.md#stream_writable_writablefinished [error code]: #http2_error_codes_for_rst_stream_and_goaway +[Sensitive headers]: #http2_sensitive_headers diff --git a/lib/http2.js b/lib/http2.js index c3ecd82072..14b4f57acd 100644 --- a/lib/http2.js +++ b/lib/http2.js @@ -8,6 +8,7 @@ const { getDefaultSettings, getPackedSettings, getUnpackedSettings, + sensitiveHeaders, Http2ServerRequest, Http2ServerResponse } = require('internal/http2/core'); @@ -20,6 +21,7 @@ module.exports = { getDefaultSettings, getPackedSettings, getUnpackedSettings, + sensitiveHeaders, Http2ServerRequest, Http2ServerResponse }; diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 14c9fc4ac6..4f1a3f8b57 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -129,6 +129,7 @@ const { getSettings, getStreamState, isPayloadMeaningless, + kSensitiveHeaders, kSocket, kRequest, kProxySocket, @@ -312,7 +313,7 @@ function emit(self, ...args) { // create the associated Http2Stream instance and emit the 'stream' // event. If the stream is not new, emit the 'headers' event to pass // the block of headers on. -function onSessionHeaders(handle, id, cat, flags, headers) { +function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) { const session = this[kOwner]; if (session.destroyed) return; @@ -326,7 +327,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) { let stream = streams.get(id); // Convert the array of header name value pairs into an object - const obj = toHeaderObject(headers); + const obj = toHeaderObject(headers, sensitiveHeaders); if (stream === undefined) { if (session.closed) { @@ -2351,6 +2352,7 @@ function processHeaders(oldHeaders, options) { headers[key] = oldHeaders[key]; } } + headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders]; } const statusCode = @@ -2373,6 +2375,10 @@ function processHeaders(oldHeaders, options) { if (statusCode < 200 || statusCode > 599) throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]); + const neverIndex = headers[kSensitiveHeaders]; + if (neverIndex !== undefined && !ArrayIsArray(neverIndex)) + throw new ERR_INVALID_OPT_VALUE('headers[http2.neverIndex]', neverIndex); + return headers; } @@ -3333,6 +3339,7 @@ module.exports = { getDefaultSettings, getPackedSettings, getUnpackedSettings, + sensitiveHeaders: kSensitiveHeaders, Http2Session, Http2Stream, Http2ServerRequest, diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 00561e0895..10080dc71a 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -9,6 +9,8 @@ const { ObjectKeys, Set, String, + StringFromCharCode, + StringPrototypeToLowerCase, Symbol, } = primordials; @@ -25,11 +27,14 @@ const { hideStackFrames } = require('internal/errors'); +const kSensitiveHeaders = Symbol('nodejs.http2.sensitiveHeaders'); const kSocket = Symbol('socket'); const kProxySocket = Symbol('proxySocket'); const kRequest = Symbol('request'); const { + NGHTTP2_NV_FLAG_NONE, + NGHTTP2_NV_FLAG_NO_INDEX, NGHTTP2_SESSION_CLIENT, NGHTTP2_SESSION_SERVER, @@ -454,6 +459,9 @@ const assertValidPseudoHeaderTrailer = hideStackFrames((key) => { throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key); }); +const emptyArray = []; +const kNeverIndexFlag = StringFromCharCode(NGHTTP2_NV_FLAG_NO_INDEX); +const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE); function mapToHeaders(map, assertValuePseudoHeader = assertValidPseudoHeader) { let ret = ''; @@ -466,6 +474,8 @@ function mapToHeaders(map, let value; let isSingleValueHeader; let err; + const neverIndex = + (map[kSensitiveHeaders] || emptyArray).map(StringPrototypeToLowerCase); for (i = 0; i < keys.length; ++i) { key = keys[i]; value = map[key]; @@ -494,11 +504,12 @@ function mapToHeaders(map, throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key); singles.add(key); } + const flags = neverIndex.includes(key) ? kNeverIndexFlag : kNoHeaderFlags; if (key[0] === ':') { err = assertValuePseudoHeader(key); if (err !== undefined) throw err; - ret = `${key}\0${value}\0${ret}`; + ret = `${key}\0${value}\0${flags}${ret}`; count++; continue; } @@ -508,12 +519,12 @@ function mapToHeaders(map, if (isArray) { for (j = 0; j < value.length; ++j) { const val = String(value[j]); - ret += `${key}\0${val}\0`; + ret += `${key}\0${val}\0${flags}`; } count += value.length; continue; } - ret += `${key}\0${value}\0`; + ret += `${key}\0${value}\0${flags}`; count++; } @@ -552,7 +563,7 @@ const assertWithinRange = hideStackFrames( } ); -function toHeaderObject(headers) { +function toHeaderObject(headers, sensitiveHeaders) { const obj = ObjectCreate(null); for (var n = 0; n < headers.length; n += 2) { const name = headers[n]; @@ -593,6 +604,7 @@ function toHeaderObject(headers) { } } } + obj[kSensitiveHeaders] = sensitiveHeaders; return obj; } @@ -621,6 +633,7 @@ module.exports = { getSettings, getStreamState, isPayloadMeaningless, + kSensitiveHeaders, kSocket, kProxySocket, kRequest, diff --git a/src/node_http2.cc b/src/node_http2.cc index 4ac84d275d..9c077bea1e 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1215,22 +1215,29 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { // this way for performance reasons (it's faster to generate and pass an // array than it is to generate and pass the object). - std::vector<Local<Value>> headers_v(stream->headers_count() * 2); + MaybeStackBuffer<Local<Value>, 64> headers_v(stream->headers_count() * 2); + MaybeStackBuffer<Local<Value>, 32> sensitive_v(stream->headers_count()); + size_t sensitive_count = 0; + stream->TransferHeaders([&](const Http2Header& header, size_t i) { headers_v[i * 2] = header.GetName(this).ToLocalChecked(); headers_v[i * 2 + 1] = header.GetValue(this).ToLocalChecked(); + if (header.flags() & NGHTTP2_NV_FLAG_NO_INDEX) + sensitive_v[sensitive_count++] = headers_v[i * 2]; }); CHECK_EQ(stream->headers_count(), 0); DecrementCurrentSessionMemory(stream->current_headers_length_); stream->current_headers_length_ = 0; - Local<Value> args[5] = { - stream->object(), - Integer::New(isolate, id), - Integer::New(isolate, stream->headers_category()), - Integer::New(isolate, frame->hd.flags), - Array::New(isolate, headers_v.data(), headers_v.size())}; + Local<Value> args[] = { + stream->object(), + Integer::New(isolate, id), + Integer::New(isolate, stream->headers_category()), + Integer::New(isolate, frame->hd.flags), + Array::New(isolate, headers_v.out(), headers_v.length()), + Array::New(isolate, sensitive_v.out(), sensitive_count), + }; MakeCallback(env()->http2session_on_headers_function(), arraysize(args), args); } diff --git a/src/node_http2.h b/src/node_http2.h index 9e8521942e..9c7ffce2c4 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -116,7 +116,6 @@ using Nghttp2SessionCallbacksPointer = struct Http2HeadersTraits { typedef nghttp2_nv nv_t; - static const uint8_t kNoneFlag = NGHTTP2_NV_FLAG_NONE; }; struct Http2RcBufferPointerTraits { diff --git a/src/node_http_common-inl.h b/src/node_http_common-inl.h index 7cd4bdec99..6ddc99e7d4 100644 --- a/src/node_http_common-inl.h +++ b/src/node_http_common-inl.h @@ -55,13 +55,14 @@ NgHeaders<T>::NgHeaders(Environment* env, v8::Local<v8::Array> headers) { return; } - nva[n].flags = T::kNoneFlag; nva[n].name = reinterpret_cast<uint8_t*>(p); nva[n].namelen = strlen(p); p += nva[n].namelen + 1; nva[n].value = reinterpret_cast<uint8_t*>(p); nva[n].valuelen = strlen(p); p += nva[n].valuelen + 1; + nva[n].flags = *p; + p++; } } @@ -189,6 +190,11 @@ size_t NgHeader<T>::length() const { return name_.len() + value_.len(); } +template <typename T> +uint8_t NgHeader<T>::flags() const { + return flags_; +} + } // namespace node #endif // SRC_NODE_HTTP_COMMON_INL_H_ diff --git a/src/node_http_common.h b/src/node_http_common.h index c7e4d34af2..8017c0d7aa 100644 --- a/src/node_http_common.h +++ b/src/node_http_common.h @@ -460,6 +460,7 @@ struct NgHeaderBase : public MemoryRetainer { virtual std::string name() const = 0; virtual std::string value() const = 0; virtual size_t length() const = 0; + virtual uint8_t flags() const = 0; virtual std::string ToString() const; }; @@ -505,6 +506,7 @@ class NgHeader final : public NgHeaderBase<typename T::allocator_t> { inline std::string name() const override; inline std::string value() const override; inline size_t length() const override; + inline uint8_t flags() const override; void MemoryInfo(MemoryTracker* tracker) const override; diff --git a/test/parallel/test-http2-sensitive-headers.js b/test/parallel/test-http2-sensitive-headers.js new file mode 100644 index 0000000000..7d4d775a55 --- /dev/null +++ b/test/parallel/test-http2-sensitive-headers.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const makeDuplexPair = require('../common/duplexpair'); + +{ + const testData = '<h1>Hello World</h1>'; + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200, + 'cookie': 'donotindex', + 'not-sensitive': 'foo', + 'sensitive': 'bar', + // sensitiveHeaders entries are case-insensitive + [http2.sensitiveHeaders]: ['Sensitive'] + }); + stream.end(testData); + })); + + const { clientSide, serverSide } = makeDuplexPair(); + server.emit('connection', serverSide); + + const client = http2.connect('http://localhost:80', { + createConnection: common.mustCall(() => clientSide) + }); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers.cookie, 'donotindex'); + assert.deepStrictEqual(headers[http2.sensitiveHeaders], + ['cookie', 'sensitive']); + })); + + req.on('end', common.mustCall(() => { + clientSide.destroy(); + clientSide.end(); + })); + req.resume(); + req.end(); +} diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js index a964c361b7..6b9ce07907 100644 --- a/test/parallel/test-http2-util-headers-list.js +++ b/test/parallel/test-http2-util-headers-list.js @@ -9,6 +9,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const { mapToHeaders, toHeaderObject } = require('internal/http2/util'); +const { sensitiveHeaders } = require('http2'); const { internalBinding } = require('internal/test/binding'); const { HTTP2_HEADER_STATUS, @@ -102,8 +103,9 @@ const { assert.deepStrictEqual( mapToHeaders(headers), - [ [ ':path', 'abc', ':status', '200', 'abc', '1', 'xyz', '1', 'xyz', '2', - 'xyz', '3', 'xyz', '4', 'bar', '1', '' ].join('\0'), 8 ] + [ [ ':path', 'abc\0', ':status', '200\0', 'abc', '1\0', 'xyz', '1\0', + 'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', 'bar', '1\0', '' ].join('\0'), + 8 ] ); } @@ -118,8 +120,8 @@ const { assert.deepStrictEqual( mapToHeaders(headers), - [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2', - 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ] + [ [ ':status', '200\0', ':path', 'abc\0', 'abc', '1\0', 'xyz', '1\0', + 'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 7 ] ); } @@ -135,8 +137,8 @@ const { assert.deepStrictEqual( mapToHeaders(headers), - [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2', - 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ] + [ [ ':status', '200\0', ':path', 'abc\0', 'abc', '1\0', 'xyz', '1\0', + 'xyz', '2\0', 'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 7 ] ); } @@ -151,8 +153,8 @@ const { assert.deepStrictEqual( mapToHeaders(headers), - [ [ ':status', '200', ':path', 'abc', 'xyz', '1', 'xyz', '2', 'xyz', '3', - 'xyz', '4', '' ].join('\0'), 6 ] + [ [ ':status', '200\0', ':path', 'abc\0', 'xyz', '1\0', 'xyz', '2\0', + 'xyz', '3\0', 'xyz', '4\0', '' ].join('\0'), 6 ] ); } @@ -164,7 +166,7 @@ const { }; assert.deepStrictEqual( mapToHeaders(headers), - [ [ 'set-cookie', 'foo=bar', '' ].join('\0'), 1 ] + [ [ 'set-cookie', 'foo=bar\0', '' ].join('\0'), 1 ] ); } @@ -182,6 +184,23 @@ const { }); } +{ + const headers = { + 'abc': 1, + ':path': 'abc', + ':status': [200], + ':authority': [], + 'xyz': [1, 2, 3, 4], + [sensitiveHeaders]: ['xyz'] + }; + + assert.deepStrictEqual( + mapToHeaders(headers), + [ ':status\x00200\x00\x00:path\x00abc\x00\x00abc\x001\x00\x00' + + 'xyz\x001\x00\x01xyz\x002\x00\x01xyz\x003\x00\x01xyz\x004\x00\x01', 7 ] + ); +} + // The following are not allowed to have multiple values [ HTTP2_HEADER_STATUS, diff --git a/test/parallel/test-http2-zero-length-header.js b/test/parallel/test-http2-zero-length-header.js index 7b142d75f0..2e7876858a 100644 --- a/test/parallel/test-http2-zero-length-header.js +++ b/test/parallel/test-http2-zero-length-header.js @@ -14,7 +14,8 @@ server.on('stream', (stream, headers) => { ':method': 'GET', ':path': '/', 'bar': '', - '__proto__': null + '__proto__': null, + [http2.sensitiveHeaders]: [] }); stream.session.destroy(); server.close(); |