From 455166314b0e3b1d3e23f70ec50bde996294abfb Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 16:39:00 -0400 Subject: [PATCH] Keep a %u escape that is not a character decode_path_component dropped a %u escape of a surrogate, joining what was on either side of it, so /ad%uD800min became /admin after a filter had seen something else. Keep such an escape as it is. --- httplib.h | 10 ++++++++-- test/test.cc | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index df7ce281..37491fdf 100644 --- a/httplib.h +++ b/httplib.h @@ -11612,8 +11612,14 @@ inline std::string decode_path_component(const std::string &component) { // so to_utf8 writes at most 3 bytes. buff[4] is safe. char buff[4]; size_t len = detail::to_utf8(val, buff); - if (len > 0) { result.append(buff, len); } - i += 5; // 'u0000' + if (len > 0) { + result.append(buff, len); + i += 5; // 'u0000' + } else { + // A surrogate is no character. Dropping it would join what is on + // either side of it, so the escape is kept as it is. + result += component[i]; + } } else { result += component[i]; } diff --git a/test/test.cc b/test/test.cc index f23d2766..9c9b9e44 100644 --- a/test/test.cc +++ b/test/test.cc @@ -459,8 +459,10 @@ TEST(DecodePathTest, UnicodeEncoding) { EXPECT_EQ(U8("あ"), decode_path_component("%u3042")); // %uFFFF = max 4-digit hex (3-byte UTF-8, must not overflow buff[4]) EXPECT_FALSE(decode_path_component("%uFFFF").empty()); - // %uD800 = surrogate (invalid, silently dropped) - EXPECT_EQ("", decode_path_component("%uD800")); + // %uD800 = surrogate (invalid, kept as it is rather than joining its + // neighbours) + EXPECT_EQ("%uD800", decode_path_component("%uD800")); + EXPECT_EQ("/ad%uD800min", decode_path_component("/ad%uD800min")); } TEST(DecodeQueryTest, RejectsNonHexEscapes) {