diff --git a/src/node_http_common.h b/src/node_http_common.h index 5afb6b97becc4d..6836e0075b01ec 100644 --- a/src/node_http_common.h +++ b/src/node_http_common.h @@ -16,6 +16,8 @@ class Environment; #define DEFAULT_MAX_HEADER_LIST_PAIRS 128u #define DEFAULT_MAX_HEADER_LENGTH 8192 +constexpr size_t kMaxInternalizedHeaderNameLength = 64; + #define HTTP_SPECIAL_HEADERS(V) \ V(STATUS, ":status") \ V(METHOD, ":method") \ @@ -432,7 +434,7 @@ class NgRcBufPointer : public MemoryRetainer { return v8::String::Empty(env->isolate()); } - if (ptr.IsInternalizable() && len < 64) { + if (ptr.IsInternalizable() && len < kMaxInternalizedHeaderNameLength) { v8::MaybeLocal ret = GetInternalizedString(env, ptr); ptr.reset(); return ret; diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index d99b61780fad6c..a09b8f0460d4cb 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -33,7 +33,8 @@ #include // free() #include // strdup(), strchr() - +#include +#include // This is a binding to llhttp (https://github.com/nodejs/llhttp) // The goal is to decouple sockets from parsing for more javascript-level @@ -65,6 +66,7 @@ using v8::Isolate; using v8::Local; using v8::LocalVector; using v8::MaybeLocal; +using v8::NewStringType; using v8::Number; using v8::Object; using v8::ObjectTemplate; @@ -107,6 +109,106 @@ inline bool IsOWS(char c) { return c == ' ' || c == '\t'; } +constexpr std::string_view kKnownHeaderNames[] = { + "Accept-Encoding", + "Accept-Language", + "Accept-Ranges", + "Accept", + "Access-Control-Allow-Credentials", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Expose-Headers", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Age", + "Authorization", + "Cache-Control", + "Connection", + "Content-Disposition", + "Content-Encoding", + "Content-Length", + "Content-Type", + "Cookie", + "Date", + "ETag", + "Forwarded", + "Host", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "Last-Modified", + "Link", + "Location", + "Range", + "Referer", + "Server", + "Set-Cookie", + "Strict-Transport-Security", + "Transfer-Encoding", + "TE", + "Upgrade-Insecure-Requests", + "Upgrade", + "User-Agent", + "Vary", + "X-Content-Type-Options", + "X-Frame-Options", + "Keep-Alive", + "Proxy-Connection", + "X-XSS-Protection", + "Alt-Svc", + "Content-Security-Policy", + "Early-Data", + "Expect-CT", + "Origin", + "Purpose", + "Timing-Allow-Origin", + "X-Forwarded-For", + "Priority", + "Accept-Charset", + "Access-Control-Max-Age", + "Allow", + "Content-Language", + "Content-Location", + "Content-MD5", + "Content-Range", + "DNT", + "Expect", + "Expires", + "From", + "If-Match", + "If-Unmodified-Since", + "Max-Forwards", + "Prefer", + "Proxy-Authenticate", + "Proxy-Authorization", + "Refresh", + "Retry-After", + "Trailer", + "Tk", + "Via", + "Warning", + "WWW-Authenticate", + "HTTP2-Settings", +}; + +constexpr size_t kMaxKnownHeaderNameLength = [] { + size_t max = 0; + for (std::string_view name : kKnownHeaderNames) { + if (name.size() > max) max = name.size(); + } + return max; +}(); + +// Only known header field names in their canonical Train-Case form may be +// internalized. Internalizing arbitrary client-controlled names would let +// peers pollute the isolate-wide string table. +bool IsKnownHeaderName(const char* str, size_t size) { + static const std::unordered_set known_names( + std::begin(kKnownHeaderNames), std::end(kKnownHeaderNames)); + return known_names.contains(std::string_view(str, size)); +} + class BindingData : public BaseObject { public: BindingData(Realm* realm, Local obj) : BaseObject(realm, obj) {} @@ -233,6 +335,14 @@ struct StringPtr { return String::Empty(env->isolate()); } + Local ToInternalizedString(Environment* env) const { + if (size_ <= kMaxKnownHeaderNameLength && IsKnownHeaderName(str_, size_)) { + return OneByteString( + env->isolate(), str_, size_, NewStringType::kInternalized); + } + return ToString(env); + } + // Strip trailing OWS (SPC or HTAB) from string. Local ToTrimmedString(Environment* env) { while (size_ > 0 && IsOWS(str_[size_ - 1])) { @@ -940,7 +1050,8 @@ class Parser : public AsyncWrap, public StreamListener { Local headers_v[kMaxHeaderFieldsCount * 2]; for (size_t i = 0; i < num_values_; ++i) { - headers_v[i * 2] = fields_[i].ToString(env()); + // Known field names repeat across requests, so internalize them. + headers_v[i * 2] = fields_[i].ToInternalizedString(env()); headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env()); }