diff --git a/include/swoole.h b/include/swoole.h index b2ea1573c7..8654594fd4 100644 --- a/include/swoole.h +++ b/include/swoole.h @@ -289,6 +289,14 @@ static inline unsigned int swoole_strcasect(const char *pstr, size_t plen, const return (plen >= slen) && (strncasecmp(pstr, sstr, slen) == 0); } +static inline unsigned int swoole_str_starts_with(const char *pstr, size_t plen, const char *sstr, size_t slen) { + return (plen >= slen) && (strncmp(pstr, sstr, slen) == 0); +} + +static inline unsigned int swoole_str_istarts_with(const char *pstr, size_t plen, const char *sstr, size_t slen) { + return (plen >= slen) && (strncasecmp(pstr, sstr, slen) == 0); +} + static inline const char *swoole_strnstr(const char *haystack, uint32_t haystack_length, const char *needle, diff --git a/include/swoole_static_handler.h b/include/swoole_static_handler.h index a7d248202c..c35d7bf78d 100644 --- a/include/swoole_static_handler.h +++ b/include/swoole_static_handler.h @@ -67,6 +67,15 @@ class StaticHandler { bool get_dir_files(); bool set_filename(const std::string &filename); + bool catch_error() { + if (last) { + status_code = SW_HTTP_NOT_FOUND; + return true; + } else { + return false; + } + } + bool has_index_file() { return !index_file.empty(); } @@ -77,7 +86,7 @@ class StaticHandler { std::string get_date(); - inline time_t get_file_mtime() { + time_t get_file_mtime() { #ifdef __MACH__ return file_stat.st_mtimespec.tv_sec; #else @@ -87,11 +96,11 @@ class StaticHandler { std::string get_date_last_modified(); - inline const char *get_filename() { + const char *get_filename() { return filename; } - inline const char *get_boundary() { + const char *get_boundary() { if (boundary.empty()) { boundary = std::string(SW_HTTP_SERVER_BOUNDARY_PREKEY); swoole_random_string(boundary, SW_HTTP_SERVER_BOUNDARY_TOTAL_SIZE - sizeof(SW_HTTP_SERVER_BOUNDARY_PREKEY)); @@ -99,7 +108,7 @@ class StaticHandler { return boundary.c_str(); } - inline const char *get_content_type() { + const char *get_content_type() { if (tasks.size() > 1) { content_type = std::string("multipart/byteranges; boundary=") + get_boundary(); return content_type.c_str(); @@ -108,31 +117,53 @@ class StaticHandler { } } - inline const char *get_mimetype() { + const char *get_mimetype() { return swoole::mime_type::get(get_filename()).c_str(); } - inline std::string get_filename_std_string() { + std::string get_filename_std_string() { return std::string(filename, l_filename); } - inline size_t get_filesize() { + bool get_absolute_path(); + + size_t get_filesize() { return file_stat.st_size; } - inline const std::vector &get_tasks() { + const std::vector &get_tasks() { return tasks; } - inline bool is_dir() { + bool is_dir() { return S_ISDIR(file_stat.st_mode); } - inline size_t get_content_length() { + bool is_link() { + return S_ISLNK(file_stat.st_mode); + } + + bool is_file() { + return S_ISREG(file_stat.st_mode); + } + + bool is_absolute_path() { + return swoole_strnpos(filename, l_filename, SW_STRL("..")) == -1; + } + + bool is_located_in_document_root() { + const std::string &document_root = serv->get_document_root(); + const size_t l_document_root = document_root.length(); + + return l_filename > l_document_root && filename[l_document_root] == '/' && + swoole_str_starts_with(filename, l_filename, document_root.c_str(), l_document_root); + } + + size_t get_content_length() { return content_length; } - inline const char *get_end_part() { + const char *get_end_part() { return end_part.c_str(); } diff --git a/src/server/static_handler.cc b/src/server/static_handler.cc index d95d7036e0..e0015bfad2 100644 --- a/src/server/static_handler.cc +++ b/src/server/static_handler.cc @@ -45,7 +45,7 @@ bool StaticHandler::is_modified(const std::string &date_if_modified_since) { } else if (strptime(date_tmp, SW_HTTP_ASCTIME_DATE, &tm3) != nullptr) { date_format = SW_HTTP_ASCTIME_DATE; } - return date_format && mktime(&tm3) - (int) serv->timezone_ >= get_file_mtime(); + return date_format && mktime(&tm3) - (time_t) serv->timezone_ >= get_file_mtime(); } bool StaticHandler::is_modified_range(const std::string &date_range) { @@ -87,6 +87,16 @@ std::string StaticHandler::get_date_last_modified() { return std::string(date_last_modified); } +bool StaticHandler::get_absolute_path() { + char abs_path[PATH_MAX]; + if (!realpath(filename, abs_path)) { + return false; + } + strncpy(filename, abs_path, sizeof(abs_path)); + l_filename = strlen(filename); + return true; +} + bool StaticHandler::hit() { char *p = filename; const char *url = request_url.c_str(); @@ -102,13 +112,14 @@ bool StaticHandler::hit() { size_t n = params ? params - url : url_length; const std::string &document_root = serv->get_document_root(); + const size_t l_document_root = document_root.length(); - memcpy(p, document_root.c_str(), document_root.length()); - p += document_root.length(); + memcpy(p, document_root.c_str(), l_document_root); + p += l_document_root; if (serv->locations->size() > 0) { for (auto i = serv->locations->begin(); i != serv->locations->end(); i++) { - if (swoole_strcasect(url, url_length, i->c_str(), i->size())) { + if (swoole_str_istarts_with(url, url_length, i->c_str(), i->size())) { last = true; } } @@ -117,8 +128,8 @@ bool StaticHandler::hit() { } } - if (document_root.length() + n >= PATH_MAX) { - return false; + if (l_document_root + n >= PATH_MAX) { + return catch_error(); } memcpy(p, url, n); @@ -132,50 +143,27 @@ bool StaticHandler::hit() { l_filename = http_server::url_decode(filename, p - filename); filename[l_filename] = '\0'; - if (swoole_strnpos(filename, n, SW_STRL("..")) == -1) { - goto _detect_mime_type; - } - - char real_path[PATH_MAX]; - if (!realpath(filename, real_path)) { - if (last) { - status_code = SW_HTTP_NOT_FOUND; - return true; - } else { - return false; - } - } - - if (real_path[document_root.length()] != '/') { - return false; - } - - if (swoole_streq(real_path, strlen(real_path), document_root.c_str(), document_root.length()) != 0) { - return false; - } - -// non-static file -_detect_mime_type: -// file does not exist -check_stat: + // The file does not exist if (lstat(filename, &file_stat) < 0) { - if (last) { - status_code = SW_HTTP_NOT_FOUND; - return true; - } else { - return false; - } + return catch_error(); } - if (S_ISLNK(file_stat.st_mode)) { - char buf[PATH_MAX]; - ssize_t byte = ::readlink(filename, buf, sizeof(buf) - 1); - if (byte <= 0) { - return false; + // The filename is relative path, allows for the resolution of symbolic links. + // This path is formed by concatenating the document root and that is permitted for access. + if (is_absolute_path()) { + if (is_link()) { + // Use the realpath function to resolve a symbolic link to its actual path. + if (!get_absolute_path()) { + return catch_error(); + } + if (lstat(filename, &file_stat) < 0) { + return catch_error(); + } + } + } else { + if (!get_absolute_path() || !is_located_in_document_root()) { + return catch_error(); } - buf[byte] = 0; - swoole_strlcpy(filename, buf, sizeof(filename)); - goto check_stat; } if (serv->http_index_files && !serv->http_index_files->empty() && is_dir()) { @@ -186,11 +174,11 @@ bool StaticHandler::hit() { return true; } - if (!swoole::mime_type::exists(filename) && !last) { + if (!mime_type::exists(filename) && !last) { return false; } - if (!S_ISREG(file_stat.st_mode)) { + if (!is_file()) { return false; } @@ -271,23 +259,23 @@ bool StaticHandler::get_dir_files() { return true; } -bool StaticHandler::set_filename(const std::string &filename) { - char *p = this->filename + l_filename; +bool StaticHandler::set_filename(const std::string &_filename) { + char *p = filename + l_filename; if (*p != '/') { *p = '/'; p += 1; } - memcpy(p, filename.c_str(), filename.length()); - p += filename.length(); + memcpy(p, _filename.c_str(), _filename.length()); + p += _filename.length(); *p = 0; - if (lstat(this->filename, &file_stat) < 0) { + if (lstat(filename, &file_stat) < 0) { return false; } - if (!S_ISREG(file_stat.st_mode)) { + if (!is_file()) { return false; } @@ -301,7 +289,7 @@ void StaticHandler::parse_range(const char *range, const char *if_range) { if (range && '\0' != *range) { const char *p = range; // bytes= - if (!SW_STRCASECT(p, strlen(range), "bytes=")) { + if (!SW_STR_ISTARTS_WITH(p, strlen(range), "bytes=")) { _task.offset = 0; _task.length = content_length = get_filesize(); tasks.push_back(_task); @@ -327,7 +315,7 @@ void StaticHandler::parse_range(const char *range, const char *if_range) { } while (*p >= '0' && *p <= '9') { - if (start >= cutoff && (start > cutoff || (size_t)(*p - '0') > cutlim)) { + if (start >= cutoff && (start > cutoff || (size_t) (*p - '0') > cutlim)) { status_code = SW_HTTP_RANGE_NOT_SATISFIABLE; return; } @@ -364,7 +352,7 @@ void StaticHandler::parse_range(const char *range, const char *if_range) { } while (*p >= '0' && *p <= '9') { - if (end >= cutoff && (end > cutoff || (size_t)(*p - '0') > cutlim)) { + if (end >= cutoff && (end > cutoff || (size_t) (*p - '0') > cutlim)) { status_code = SW_HTTP_RANGE_NOT_SATISFIABLE; return; } diff --git a/tests/swoole_http_server/static_handler/read_link_2.phpt b/tests/swoole_http_server/static_handler/read_link_2.phpt new file mode 100644 index 0000000000..6ce49e3866 --- /dev/null +++ b/tests/swoole_http_server/static_handler/read_link_2.phpt @@ -0,0 +1,59 @@ +--TEST-- +swoole_http_server/static_handler: link to a file outside the document root +--SKIPIF-- + +--FILE-- +parentFunc = function () use ($pm, $doc_root, $image_dir, $image_link) { + Swoole\Coroutine\run(function () use ($pm, $doc_root, $image_dir, $image_link) { + $data = httpGetBody("http://127.0.0.1:{$pm->getFreePort()}/{$image_dir}/image.jpg"); + Assert::assert(md5($data) === md5_file(TEST_IMAGE)); + }); + $pm->kill(); + echo "DONE\n"; +}; +$pm->childFunc = function () use ($pm, $doc_root, $image_dir, $image_link) { + $http = new Server('127.0.0.1', $pm->getFreePort(), SWOOLE_BASE); + $http->set([ + 'log_file' => '/dev/null', + 'open_http2_protocol' => true, + 'enable_static_handler' => true, + 'document_root' => $doc_root, + 'static_handler_locations' => ['/image'] + ]); + $http->on('workerStart', function () use ($pm) { + $pm->wakeup(); + }); + $http->on('request', function (Request $request, Response $response) { + }); + $http->start(); +}; +$pm->childFirst(); +$pm->run(); +$cleanup_fn(); +?> +--EXPECT-- +DONE diff --git a/tests/swoole_http_server/static_handler/read_link_file.phpt b/tests/swoole_http_server/static_handler/read_link_file.phpt index 14046892a4..f7bc7b2893 100644 --- a/tests/swoole_http_server/static_handler/read_link_file.phpt +++ b/tests/swoole_http_server/static_handler/read_link_file.phpt @@ -29,7 +29,7 @@ $pm->childFunc = function () use ($pm) { 'log_file' => '/dev/null', 'open_http2_protocol' => true, 'enable_static_handler' => true, - 'document_root' => dirname(dirname(dirname(__DIR__))) . '/', + 'document_root' => dirname(__DIR__, 3) . '/', 'static_handler_locations' => ['/examples'] ]); $http->on('workerStart', function () use ($pm) { diff --git a/tests/swoole_http_server/static_handler/relative_path_2.phpt b/tests/swoole_http_server/static_handler/relative_path_2.phpt new file mode 100644 index 0000000000..26c06d14c2 --- /dev/null +++ b/tests/swoole_http_server/static_handler/relative_path_2.phpt @@ -0,0 +1,49 @@ +--TEST-- +swoole_http_server/static_handler: static handler with relative path [2] +--SKIPIF-- + +--FILE-- +parentFunc = function () use ($pm) { + foreach ([false, true] as $http2) { + Swoole\Coroutine\run(function () use ($pm, $http2) { + $data = httpGetBody("http://127.0.0.1:{$pm->getFreePort()}/../examples/test.jpg", ['http2' => $http2]); + Assert::notEmpty($data); + Assert::same(md5($data), md5_file(TEST_IMAGE)); + + $data = httpGetBody("http://127.0.0.1:{$pm->getFreePort()}/../docs/swoole-logo.svg", ['http2' => $http2]); + Assert::eq("hello world", $data); + }); + } + echo "DONE\n"; + $pm->kill(); +}; +$pm->childFunc = function () use ($pm) { + $http = new Server('127.0.0.1', $pm->getFreePort(), SWOOLE_BASE); + $http->set([ + 'log_file' => '/dev/null', + 'open_http2_protocol' => true, + 'enable_static_handler' => true, + 'document_root' => SOURCE_ROOT_PATH . '/examples', + ]); + $http->on('workerStart', function () use ($pm) { + $pm->wakeup(); + }); + $http->on('request', function (Request $request, Response $response) use ($http) { + $response->end("hello world"); + }); + $http->start(); +}; +$pm->childFirst(); +$pm->run(); +?> +--EXPECT-- +DONE diff --git a/tests/swoole_http_server/static_handler/relative_path_3.phpt b/tests/swoole_http_server/static_handler/relative_path_3.phpt new file mode 100644 index 0000000000..a174c88cd4 --- /dev/null +++ b/tests/swoole_http_server/static_handler/relative_path_3.phpt @@ -0,0 +1,60 @@ +--TEST-- +swoole_http_server/static_handler: doc root with same prefix +--SKIPIF-- + +--FILE-- +parentFunc = function () use ($pm, $doc1_root, $doc2_root) { + Swoole\Coroutine\run(function () use ($pm, $doc1_root, $doc2_root) { + $data = httpGetBody("http://127.0.0.1:{$pm->getFreePort()}/../docroot/image.jpg"); + Assert::assert(md5($data) === md5_file(TEST_IMAGE)); + + $data = httpGetBody("http://127.0.0.1:{$pm->getFreePort()}/../docroot2/uuid.txt"); + Assert::isEmpty($data); + }); + $pm->kill(); + echo "DONE\n"; +}; +$pm->childFunc = function () use ($pm, $doc1_root, $doc2_root) { + $http = new Server('127.0.0.1', $pm->getFreePort(), SWOOLE_BASE); + $http->set([ + 'log_file' => '/dev/null', + 'open_http2_protocol' => true, + 'enable_static_handler' => true, + 'document_root' => $doc1_root, + ]); + $http->on('workerStart', function () use ($pm) { + $pm->wakeup(); + }); + $http->on('request', function (Request $request, Response $response) { + }); + $http->start(); +}; +$pm->childFirst(); +$pm->run(); +$cleanup_fn(); +?> +--EXPECT-- +DONE