Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2: add support for sensitive headers #34145

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 33 additions & 1 deletion doc/api/http2.md
Expand Up @@ -2440,6 +2440,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 @@ -2488,6 +2499,26 @@ 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',
[http2.sensitiveHeaders]: ['cookie']
};

stream.respond(headers);
```

### Settings object
<!-- YAML
added: v8.4.0
Expand Down Expand Up @@ -3676,5 +3707,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
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
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 (typeof neverIndex !== 'undefined' && !ArrayIsArray(neverIndex))
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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
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 {
NGHTTP3_NV_FLAG_NONE,
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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(NGHTTP3_NV_FLAG_NONE);
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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].map(StringPrototypeToLowerCase) || emptyArray;
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
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
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
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
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
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
43 changes: 43 additions & 0 deletions test/parallel/test-http2-sensitive-headers.js
@@ -0,0 +1,43 @@
'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',
[http2.sensitiveHeaders]: ['Cookie']
});
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']);
}));

req.on('end', common.mustCall(() => {
clientSide.destroy();
clientSide.end();
}));
req.resume();
req.end();
}