diff --git a/httplib.h b/httplib.h index 4fb64699..ac49c1cd 100644 --- a/httplib.h +++ b/httplib.h @@ -1029,18 +1029,27 @@ inline std::string resolve_relative_location(const std::string &location, // A ':' in the first segment means the value has a scheme. if (location.find(':') < location.find_first_of("/?#")) { return location; } - if (location[0] == '#') { return base.substr(0, base.find('#')) + location; } + // A path starting with "//" would be re-parsed as an authority. Prefix "/." + // (as the URL Standard does); remove_dot_segments strips it later. + auto on_this_host = [](std::string path) -> std::string { + if (path.compare(0, 2, "//") == 0) { path.insert(0, "/."); } + return path; + }; + + if (location[0] == '#') { + return on_this_host(base.substr(0, base.find('#')) + location); + } auto base_path = base.substr(0, base.find_first_of("?#")); - if (location[0] == '?') { return base_path + location; } + if (location[0] == '?') { return on_this_host(base_path + location); } if (base_path.empty() || base_path[0] != '/') { base_path = "/"; } auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location; // Remove "." and ".." segments from the merged path. auto path_end = (std::min)(merged.find_first_of("?#"), merged.size()); - return remove_dot_segments(merged.substr(0, path_end)) + - merged.substr(path_end); + return on_this_host(remove_dot_segments(merged.substr(0, path_end)) + + merged.substr(path_end)); } } // namespace detail diff --git a/test/test.cc b/test/test.cc index 56df680d..e6b893e7 100644 --- a/test/test.cc +++ b/test/test.cc @@ -20725,6 +20725,13 @@ TEST(RedirectTest, ResolveRelativeLocation) { detail::resolve_relative_location(c.first, "/b/c/d;p?q")) << c.first; } + + // A result starting with "//" is still a path, not a network-path reference + EXPECT_EQ("/.//g", + detail::resolve_relative_location("../../..//g", "/b/c/d;p?q")); + EXPECT_EQ("/.//g/x", detail::resolve_relative_location("x", "//g/h")); + EXPECT_EQ("/.//g/h?y", detail::resolve_relative_location("?y", "//g/h")); + EXPECT_EQ("/.//g/h#s", detail::resolve_relative_location("#s", "//g/h")); } TEST(RedirectTest, RemoveDotSegments) { @@ -20812,6 +20819,54 @@ TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) { } } +// "..//host:port/x" resolves to the path "//host:port/x" on the same host +// (RFC 3986 5.2), as curl and the URL Standard have it, not to another host. +TEST(RedirectTest, RelativeLocationStaysOnHost) { + Server other; + std::atomic other_hits{0}; + other.Get(".*", [&](const Request & /*req*/, Response &res) { + other_hits++; + res.set_content("other", "text/plain"); + }); + auto other_port = other.bind_to_any_port("127.0.0.1"); + auto other_thread = std::thread([&]() { other.listen_after_bind(); }); + + Server svr; + svr.Get("/dir/page", [](const Request &req, Response &res) { + res.set_redirect(req.get_param_value("to")); + }); + svr.Get(".*", [](const Request &req, Response &res) { + res.set_content(req.path, "text/plain"); + }); + auto port = svr.bind_to_any_port(HOST); + auto thread = std::thread([&]() { svr.listen_after_bind(); }); + + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + other.stop(); + other_thread.join(); + ASSERT_FALSE(svr.is_running()); + ASSERT_FALSE(other.is_running()); + }); + + svr.wait_until_ready(); + other.wait_until_ready(); + + auto authority = "127.0.0.1:" + std::to_string(other_port); + for (const auto &to : + {"..//" + authority + "/x", "../..//" + authority + "/x"}) { + Client cli(HOST, port); + cli.set_follow_location(true); + + auto res = cli.Get("/dir/page?to=" + encode_query_component(to)); + ASSERT_TRUE(res) << to << ": " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status) << to; + EXPECT_EQ("//" + authority + "/x", res->body) << to; + } + EXPECT_EQ(0, other_hits.load()); +} + #ifdef CPPHTTPLIB_SSL_ENABLED TEST(RedirectTest, Issue2185_Online) { SSLClient client("github.com");