Skip to content

Commit

Permalink
http2: add support for sensitive headers
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
addaleax authored and cjihrig committed Jul 22, 2020
1 parent f1578e4 commit 5adf8e5
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 27 deletions.
41 changes: 40 additions & 1 deletion doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,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
Expand Down Expand Up @@ -2509,6 +2520,33 @@ server.on('stream', (stream, headers) => {
});
```

<a id="http2-sensitive-headers"></a>
#### 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
Expand Down Expand Up @@ -3696,5 +3734,6 @@ following additional properties:
[`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket
[`tls.connect()`]: tls.html#tls_tls_connect_options_callback
[`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener
[error code]: #error_codes
[`writable.writableFinished`]: stream.html#stream_writable_writablefinished
[error code]: #error_codes
[Sensitive headers]: #http2-sensitive-headers
2 changes: 2 additions & 0 deletions lib/http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
sensitiveHeaders,
Http2ServerRequest,
Http2ServerResponse
} = require('internal/http2/core');
Expand All @@ -20,6 +21,7 @@ module.exports = {
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
sensitiveHeaders,
Http2ServerRequest,
Http2ServerResponse
};
11 changes: 9 additions & 2 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const {
getSettings,
getStreamState,
isPayloadMeaningless,
kSensitiveHeaders,
kSocket,
kRequest,
kProxySocket,
Expand Down Expand Up @@ -303,7 +304,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;
Expand All @@ -317,7 +318,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) {
Expand Down Expand Up @@ -2232,6 +2233,7 @@ function processHeaders(oldHeaders) {
headers[key] = oldHeaders[key];
}
}
headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders];
}

const statusCode =
Expand All @@ -2251,6 +2253,10 @@ function processHeaders(oldHeaders) {
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;
}

Expand Down Expand Up @@ -3166,6 +3172,7 @@ module.exports = {
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
sensitiveHeaders: kSensitiveHeaders,
Http2Session,
Http2Stream,
Http2ServerRequest,
Expand Down
20 changes: 16 additions & 4 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
ObjectCreate,
ObjectKeys,
Set,
StringPrototypeToLowerCase,
Symbol,
} = primordials;

Expand All @@ -25,11 +26,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,

Expand Down Expand Up @@ -443,6 +447,9 @@ const assertValidPseudoHeaderTrailer = hideStackFrames((key) => {
throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
});

const emptyArray = [];
const kNeverIndexFlag = String.fromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
const kNoHeaderFlags = String.fromCharCode(NGHTTP2_NV_FLAG_NONE);
function mapToHeaders(map,
assertValuePseudoHeader = assertValidPseudoHeader) {
let ret = '';
Expand All @@ -455,6 +462,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];
Expand Down Expand Up @@ -483,11 +492,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;
}
Expand All @@ -500,12 +510,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++;
}

Expand Down Expand Up @@ -544,7 +554,7 @@ const assertWithinRange = hideStackFrames(
}
);

function toHeaderObject(headers) {
function toHeaderObject(headers, sensitiveHeaders) {
const obj = ObjectCreate(null);
for (var n = 0; n < headers.length; n = n + 2) {
const name = headers[n];
Expand Down Expand Up @@ -585,6 +595,7 @@ function toHeaderObject(headers) {
}
}
}
obj[kSensitiveHeaders] = sensitiveHeaders;
return obj;
}

Expand Down Expand Up @@ -614,6 +625,7 @@ module.exports = {
getSettings,
getStreamState,
isPayloadMeaningless,
kSensitiveHeaders,
kSocket,
kProxySocket,
kRequest,
Expand Down
21 changes: 14 additions & 7 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1213,22 +1213,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);
}
Expand Down
1 change: 0 additions & 1 deletion src/node_http2.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ using Nghttp2SessionCallbacksPointer =

struct Http2HeadersTraits {
typedef nghttp2_nv nv_t;
static const uint8_t kNoneFlag = NGHTTP2_NV_FLAG_NONE;
};

struct Http2RcBufferPointerTraits {
Expand Down
8 changes: 7 additions & 1 deletion src/node_http_common-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
}

Expand Down Expand Up @@ -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_
2 changes: 2 additions & 0 deletions src/node_http_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion src/quic/node_quic_http3_application.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ struct Http3RcBufferPointerTraits {

struct Http3HeadersTraits {
typedef nghttp3_nv nv_t;
static const uint8_t kNoneFlag = NGHTTP3_NV_FLAG_NONE;
};

using Http3ConnectionPointer = DeleteFnPtr<nghttp3_conn, nghttp3_conn_del>;
Expand Down
47 changes: 47 additions & 0 deletions test/parallel/test-http2-sensitive-headers.js
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit 5adf8e5

Please sign in to comment.