From 0d6018a2297d1e0f4473c9a927766e1690da9346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 3 Jul 2026 22:46:32 -0300 Subject: [PATCH 1/3] http: create header field names as internalized strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guilherme Araújo --- src/node_http_parser.cc | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index d99b61780fad6c..358f6cbe85bd9e 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -65,6 +65,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; @@ -233,6 +234,17 @@ struct StringPtr { return String::Empty(env->isolate()); } + Local ToInternalizedString(Environment* env) const { + if (size_ != 0) { + return String::NewFromOneByte(env->isolate(), + reinterpret_cast(str_), + NewStringType::kInternalized, + size_) + .ToLocalChecked(); + } + return String::Empty(env->isolate()); + } + // Strip trailing OWS (SPC or HTAB) from string. Local ToTrimmedString(Environment* env) { while (size_ > 0 && IsOWS(str_[size_ - 1])) { @@ -940,7 +952,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()); + // 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()); } From 91ca169dae87186196113e8e2953d58dd88b4212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sat, 4 Jul 2026 12:50:16 -0300 Subject: [PATCH 2/3] http: only internalize short header field names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guilherme Araújo --- src/node_http_common.h | 4 +++- src/node_http_parser.cc | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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 358f6cbe85bd9e..f2a99a0c2f3bc2 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -28,6 +28,7 @@ #include "llhttp.h" #include "memory_tracker-inl.h" #include "node_external_reference.h" +#include "node_http_common.h" #include "stream_base-inl.h" #include "v8.h" @@ -235,14 +236,15 @@ struct StringPtr { } Local ToInternalizedString(Environment* env) const { - if (size_ != 0) { + // Only internalize short names to avoid pressuring the string table. + if (size_ != 0 && size_ < kMaxInternalizedHeaderNameLength) { return String::NewFromOneByte(env->isolate(), reinterpret_cast(str_), NewStringType::kInternalized, size_) .ToLocalChecked(); } - return String::Empty(env->isolate()); + return ToString(env); } // Strip trailing OWS (SPC or HTAB) from string. From dfafda814d0fd36c76226ea61a0fcd9b4a5e2016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 16 Jul 2026 14:12:43 -0300 Subject: [PATCH 3/3] http: internalize only know headers --- src/node_http_parser.cc | 116 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index f2a99a0c2f3bc2..a09b8f0460d4cb 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -28,13 +28,13 @@ #include "llhttp.h" #include "memory_tracker-inl.h" #include "node_external_reference.h" -#include "node_http_common.h" #include "stream_base-inl.h" #include "v8.h" #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 @@ -109,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) {} @@ -236,13 +336,9 @@ struct StringPtr { } Local ToInternalizedString(Environment* env) const { - // Only internalize short names to avoid pressuring the string table. - if (size_ != 0 && size_ < kMaxInternalizedHeaderNameLength) { - return String::NewFromOneByte(env->isolate(), - reinterpret_cast(str_), - NewStringType::kInternalized, - size_) - .ToLocalChecked(); + if (size_ <= kMaxKnownHeaderNameLength && IsKnownHeaderName(str_, size_)) { + return OneByteString( + env->isolate(), str_, size_, NewStringType::kInternalized); } return ToString(env); } @@ -954,7 +1050,7 @@ class Parser : public AsyncWrap, public StreamListener { Local headers_v[kMaxHeaderFieldsCount * 2]; for (size_t i = 0; i < num_values_; ++i) { - // Field names repeat across requests, so internalize them. + // 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()); }