mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-23 11:54:42 +00:00
Refuse to serve a path segment ending in a dot or a space
Windows trims a trailing '.' or space from a file name, so a mount point there served a file under a path other than the one a pre-routing handler checked. A mount point now serves no path with a segment ending in '.' or a space, on every platform; "." and ".." are among them. The README no longer lists such names among the ones the file system can still alias.
This commit is contained in:
1 parent
70bed02bc9
commit
b4b24589ad
3 files changed
+19
-9
No files matched your search
@@ -406,7 +406,7 @@ ret = svr.remove_mount_point("/");
|
|||||||
ret = svr.remove_mount_point("/public");
|
ret = svr.remove_mount_point("/public");
|
||||||
```
|
```
|
||||||
|
|
||||||
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows a name with a trailing `.` or space, or an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
|
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, and no segment ending in `.` or a space, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// User defined file extension and MIME type mappings
|
// User defined file extension and MIME type mappings
|
||||||
|
|||||||
@@ -5988,9 +5988,10 @@ inline bool is_valid_close_payload(const std::string &payload) {
|
|||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
|
|
||||||
// A path a mount point serves: absolute, with no ".", ".." or empty segment
|
// A path a mount point serves: absolute, with no empty segment except a
|
||||||
// except a trailing '/', so the file served is the one that req.path names to
|
// trailing '/' and no segment ending in '.' or a space (which covers "." and
|
||||||
// the handlers that ran before it.
|
// ".." and names Windows trims into another file's name), so the file served
|
||||||
|
// is the one that req.path names to the handlers that ran before it.
|
||||||
inline bool is_valid_path(const std::string &path) {
|
inline bool is_valid_path(const std::string &path) {
|
||||||
if (path.empty() || path[0] != '/') { return false; }
|
if (path.empty() || path[0] != '/') { return false; }
|
||||||
|
|
||||||
@@ -6001,8 +6002,8 @@ inline bool is_valid_path(const std::string &path) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto len = i - beg;
|
auto len = i - beg;
|
||||||
if ((len == 0 && i < path.size()) || (len == 1 && path[beg] == '.') ||
|
if ((len == 0 && i < path.size()) ||
|
||||||
(len == 2 && path[beg] == '.' && path[beg + 1] == '.')) {
|
(len > 0 && (path[i - 1] == '.' || path[i - 1] == ' '))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
beg = i + 1;
|
beg = i + 1;
|
||||||
|
|||||||
+12
-3
@@ -566,6 +566,7 @@ find_content_type(const std::string &path,
|
|||||||
const std::string &default_content_type);
|
const std::string &default_content_type);
|
||||||
bool can_compress_content_type(const std::string &content_type);
|
bool can_compress_content_type(const std::string &content_type);
|
||||||
bool is_prohibited_header_name(const std::string &name);
|
bool is_prohibited_header_name(const std::string &name);
|
||||||
|
bool is_valid_path(const std::string &path);
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
bool match_hostname(const std::string &pattern, const std::string &hostname);
|
bool match_hostname(const std::string &pattern, const std::string &hostname);
|
||||||
#endif
|
#endif
|
||||||
@@ -693,6 +694,12 @@ TEST(FindContentTypeTest, ComparesWholeExtensions) {
|
|||||||
EXPECT_EQ("", detail::find_content_type("/a.l5", {}, ""));
|
EXPECT_EQ("", detail::find_content_type("/a.l5", {}, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(IsValidPathTest, RejectsSegmentsEndingInDotOrSpace) {
|
||||||
|
for (auto path : {"/a.", "/a ", "/dir./a", "/dir /a", "/..."}) {
|
||||||
|
EXPECT_FALSE(detail::is_valid_path(path)) << path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(CanCompressContentTypeTest, ComparesWholeTypes) {
|
TEST(CanCompressContentTypeTest, ComparesWholeTypes) {
|
||||||
EXPECT_TRUE(detail::can_compress_content_type("application/json"));
|
EXPECT_TRUE(detail::can_compress_content_type("application/json"));
|
||||||
EXPECT_TRUE(detail::can_compress_content_type("text/plain; charset=utf-8"));
|
EXPECT_TRUE(detail::can_compress_content_type("text/plain; charset=utf-8"));
|
||||||
@@ -12607,8 +12614,9 @@ TEST(MountTest, MultibytesPathName) {
|
|||||||
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
|
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mount point serves no path with an empty, "." or ".." segment, so a guard
|
// A mount point serves no path with an empty segment or one ending in '.' or
|
||||||
// in the pre-routing handler sees the path of every file that is served
|
// a space, so a guard in the pre-routing handler sees the path of every file
|
||||||
|
// that is served
|
||||||
TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
|
TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
|
||||||
Server svr;
|
Server svr;
|
||||||
ASSERT_TRUE(svr.set_mount_point("/", "./www"));
|
ASSERT_TRUE(svr.set_mount_point("/", "./www"));
|
||||||
@@ -12633,7 +12641,8 @@ TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
|
|||||||
Client cli(HOST, port);
|
Client cli(HOST, port);
|
||||||
for (auto path :
|
for (auto path :
|
||||||
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
|
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
|
||||||
"/dir//test.html", "/dir/../dir/test.html", "/%2e/dir/test.html"}) {
|
"/dir//test.html", "/dir/../dir/test.html", "/%2e/dir/test.html",
|
||||||
|
"/dir/test.html.", "/dir/test.html%20"}) {
|
||||||
SCOPED_TRACE(path);
|
SCOPED_TRACE(path);
|
||||||
auto res = cli.Get(path);
|
auto res = cli.Get(path);
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|||||||
Reference in new issue
Block a user