Skip to content

Commit 252dbaa

Browse files
authored
Remove dot segments from absolute redirect locations (#2648)
resolve_relative_location removed "." and ".." segments only from a relative-path Location. A Location that starts with "/" or an absolute URI was requested as is, so a redirect to /dir/../target went out as /dir/../target and a server that does not normalize the path answered 404. RFC 3986 section 5.2.2 applies remove_dot_segments to the target path of every reference that has a path, and Go and urllib3 do the same. The loop now lives in detail::remove_dot_segments, and ClientImpl::redirect applies it to an absolute path as well. Relative locations resolve as before.
1 parent 8710fbd commit 252dbaa

2 files changed

Lines changed: 75 additions & 20 deletions

File tree

‎httplib.h‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,30 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
995995
return true;
996996
}
997997

998+
// Removes "." and ".." segments from a path that starts with '/' (RFC 3986
999+
// section 5.2.4).
1000+
inline std::string remove_dot_segments(const std::string &path) {
1001+
std::string out;
1002+
size_t i = 0;
1003+
while (i < path.size()) {
1004+
auto next = (std::min)(path.find('/', i + 1), path.size());
1005+
auto segment = path.substr(i + 1, next - i - 1);
1006+
auto is_last = next == path.size();
1007+
if (segment == "." || segment == "..") {
1008+
if (segment == "..") {
1009+
out.erase((std::min)(out.rfind('/'), out.size()));
1010+
}
1011+
if (is_last) { out += '/'; }
1012+
} else {
1013+
out += '/';
1014+
out += segment;
1015+
}
1016+
i = next;
1017+
}
1018+
if (out.empty()) { out = "/"; }
1019+
return out;
1020+
}
1021+
9981022
// Resolves a relative-path or query-only Location value against the path of
9991023
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
10001024
// references starting with '/' are returned unchanged.
@@ -1015,26 +1039,8 @@ inline std::string resolve_relative_location(const std::string &location,
10151039

10161040
// Remove "." and ".." segments from the merged path.
10171041
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
1018-
std::string path;
1019-
size_t i = 0;
1020-
while (i < path_end) {
1021-
auto next = (std::min)(merged.find('/', i + 1), path_end);
1022-
auto segment = merged.substr(i + 1, next - i - 1);
1023-
auto is_last = next == path_end;
1024-
if (segment == "." || segment == "..") {
1025-
if (segment == "..") {
1026-
path.erase((std::min)(path.rfind('/'), path.size()));
1027-
}
1028-
if (is_last) { path += '/'; }
1029-
} else {
1030-
path += '/';
1031-
path += segment;
1032-
}
1033-
i = next;
1034-
}
1035-
if (path.empty()) { path = "/"; }
1036-
1037-
return path + merged.substr(path_end);
1042+
return remove_dot_segments(merged.substr(0, path_end)) +
1043+
merged.substr(path_end);
10381044
}
10391045

10401046
} // namespace detail
@@ -16405,6 +16411,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
1640516411
if (next_scheme.empty()) { next_scheme = scheme; }
1640616412
if (next_host.empty()) { next_host = host_; }
1640716413
if (next_path.empty()) { next_path = "/"; }
16414+
// An absolute or absolute-path location can have dot segments too.
16415+
if (next_path[0] == '/') {
16416+
next_path = detail::remove_dot_segments(next_path);
16417+
}
1640816418

1640916419
auto path = std::move(next_path) + next_query;
1641016420

‎test/test.cc‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20593,6 +20593,51 @@ TEST(RedirectTest, ResolveRelativeLocation) {
2059320593
}
2059420594
}
2059520595

20596+
TEST(RedirectTest, RemoveDotSegments) {
20597+
// RFC 3986 section 5.2.4
20598+
EXPECT_EQ("/a/g", detail::remove_dot_segments("/a/b/c/./../../g"));
20599+
EXPECT_EQ("/mid/6", detail::remove_dot_segments("/mid/content=5/../6"));
20600+
EXPECT_EQ("/g", detail::remove_dot_segments("/./g"));
20601+
EXPECT_EQ("/g", detail::remove_dot_segments("/../g"));
20602+
EXPECT_EQ("/", detail::remove_dot_segments("/.."));
20603+
EXPECT_EQ("/a/", detail::remove_dot_segments("/a/b/.."));
20604+
EXPECT_EQ("/a/b", detail::remove_dot_segments("/a/b"));
20605+
}
20606+
20607+
TEST(RedirectTest, AbsoluteLocationIsNormalized) {
20608+
Server svr;
20609+
20610+
svr.Get("/absolute-path", [](const Request & /*req*/, Response &res) {
20611+
res.set_redirect("/dir/../target?x=1");
20612+
});
20613+
svr.Get("/absolute-uri", [](const Request & /*req*/, Response &res) {
20614+
res.set_redirect("http://localhost:" + std::to_string(PORT) +
20615+
"/dir/./../target");
20616+
});
20617+
svr.Get("/target", [](const Request &req, Response &res) {
20618+
res.set_content(req.path, "text/plain");
20619+
});
20620+
20621+
auto th = std::thread([&]() { svr.listen("localhost", PORT); });
20622+
auto se = detail::scope_exit([&] {
20623+
svr.stop();
20624+
th.join();
20625+
ASSERT_FALSE(svr.is_running());
20626+
});
20627+
20628+
svr.wait_until_ready();
20629+
20630+
Client cli("localhost", PORT);
20631+
cli.set_follow_location(true);
20632+
20633+
for (auto path : {"/absolute-path", "/absolute-uri"}) {
20634+
auto res = cli.Get(path);
20635+
ASSERT_TRUE(res) << path << ": " << to_string(res.error());
20636+
EXPECT_EQ(StatusCode::OK_200, res->status) << path;
20637+
EXPECT_EQ("/target", res->body) << path;
20638+
}
20639+
}
20640+
2059620641
TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
2059720642
Server svr;
2059820643

0 commit comments

Comments
 (0)