CVE-2022-25298 (C++, published 2022-02-18T13:15Z)
Description: value: This affects the package sprinfall/webcc before 0.3.0. It is possible to traverse directories to fetch arbitrary files from the server.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Repository: https://github.com/sprinfall/webcc
Files changed:
examples/CMakeLists.txt
webcc/body.cc
webcc/fs.h
webcc/request_parser.cc
webcc/request_parser.h
webcc/router.cc
webcc/router.h
webcc/server.cc
webcc/server.h
webcc/string.cc
webcc/string.h
webcc/url.cc
webcc/url.h
webcc/utility.cc
webcc/utility.h
Commit message: fix static file serving security issue; fix url path encoding issue


Vulnerable code:
    add_executable(url_unicode url_unicode.cc encoding.cc encoding.h)
#include "encoding.h"

#include <Windows.h>

namespace {

// Wrapper for Windows API MultiByteToWideChar.
std::wstring MB2WC(const std::string& input, unsigned int code_page) {
  if (input.empty()) {
    return L"";
  }

  int length = ::MultiByteToWideChar(code_page, 0, &input[0],
                                     static_cast<int>(input.size()),
                                     NULL, 0);

  std::wstring output(length, '\0');

  ::MultiByteToWideChar(code_page, 0, &input[0], static_cast<int>(input.size()),
                        &output[0], static_cast<int>(output.size()));

  return output;
}

// Wrapper for Windows API WideCharToMultiByte.
std::string WC2MB(const std::wstring& input, unsigned int code_page) {
  if (input.empty()) {
    return "";
  }

  // There do have other code pages which require the flags to be 0, e.g.,
  // 50220, 50211, and so on. But they are not included in our charset
  // dictionary. So, only consider 65001 (UTF-8) and 54936 (GB18030).
  DWORD flags = 0;
  if (code_page != 65001 && code_page != 54936) {
    flags = WC_NO_BEST_FIT_CHARS | WC_COMPOSITECHECK | WC_DEFAULTCHAR;
  }

  int length = ::WideCharToMultiByte(code_page, flags, &input[0],
                                     static_cast<int>(input.size()), NULL, 0,
                                     NULL, NULL);

  std::string output(length, '\0');

  ::WideCharToMultiByte(code_page, flags, &input[0],
                        static_cast<int>(input.size()), &output[0],
                        static_cast<int>(output.size()), NULL, NULL);

  return output;
}

}  // namespace

std::string Utf16ToUtf8(const std::wstring& utf16_string) {
  return WC2MB(utf16_string, CP_UTF8);
}

std::wstring Utf8ToUtf16(const std::string& utf8_string) {
  return MB2WC(utf8_string, CP_UTF8);
}
#ifndef ENCODING_H_
#define ENCODING_H_

#include <string>

// Convert UTF16 to UTF8.
std::string Utf16ToUtf8(const std::wstring& utf16_string);

// Convert UTF8 to UTF16.
std::wstring Utf8ToUtf16(const std::string& utf8_string);

#endif  // ENCODING_H_
  os << prefix << "<file: " << path_.string() << ">" << std::endl;
  bool matched = view_matcher_(request_->method(), request_->url().path(),
                               &stream_);

             request_->url().path().c_str());

bool Router::MatchView(const std::string& method, const std::string& url,

      if (std::regex_match(url, match, route.url_regex)) {
      if (boost::iequals(route.url, url)) {
  // Match the view by HTTP method and URL (path).
  bool MatchView(const std::string& method, const std::string& url,
                               const std::string& url, bool* stream) {
  if (Router::MatchView(method, url, stream)) {
    fs::path path = doc_root_ / url;
  fs::path path = doc_root_ / request->url().path();
  // Match the view by HTTP method and URL (path).
  bool MatchViewOrStatic(const std::string& method, const std::string& url,
bool Decode(string_view encoded, std::string* raw) {
  for (auto iter = encoded.begin(); iter != encoded.end(); ++iter) {
    if (*iter == '%') {
      if (++iter == encoded.end()) {
        // Invalid URI string, two hexadecimal digits must follow '%'.
        return false;
      }

      int h_decimal = 0;
      if (!HexToDecimal(*iter, &h_decimal)) {
        return false;
      }

      if (++iter == encoded.end()) {
        // Invalid URI string, two hexadecimal digits must follow '%'.
        return false;
      }

      int l_decimal = 0;
      if (!HexToDecimal(*iter, &l_decimal)) {
        return false;
      }

      raw->push_back(static_cast<char>((h_decimal << 4) + l_decimal));

    } else if (*iter > 127 || *iter < 0) {
      // Invalid encoded URI string, must be entirely ASCII.
      return false;
    } else {
      raw->push_back(*iter);
    }
  }

  return true;
}

// Unsafe decode.
// Return the original string on failure.
std::string DecodeUnsafe(string_view encoded) {
  std::string raw;
  if (Decode(encoded, &raw)) {
    return raw;
  }
  return ToString(encoded);
}

        parameters_.push_back({ DecodeUnsafe(key), DecodeUnsafe(value) });

Fixed code:
    add_executable(url_unicode url_unicode.cc)
  os << prefix << "<file: " << path_.u8string() << ">" << std::endl;
using std::filesystem::absolute;
using std::filesystem::canonical;
using boost::filesystem::absolute;
using boost::filesystem::canonical;
  // Decode the URL path before match.
  std::string url_path = Url::DecodeUnsafe(request_->url().path());
  bool matched = view_matcher_(request_->method(), url_path, &stream_);
             url_path.c_str());
// Parameters: http_method, url_path, [out]stream
bool Router::MatchView(const std::string& method, const std::string& url_path,
      if (std::regex_match(url_path, match, route.url_regex)) {
      if (boost::iequals(route.url, url_path)) {
  // Match the view by HTTP method and URL path.
  bool MatchView(const std::string& method, const std::string& url_path,
#include "boost/algorithm/string/trim.hpp"

  CheckDocRoot();
void Server::CheckDocRoot() {
  try {
    if (!fs::exists(doc_root_) || !fs::is_directory(doc_root_)) {
      LOG_ERRO("Doc root is not an existing directory!");
      return;
    }

    if (doc_root_.is_relative()) {
      doc_root_ = fs::absolute(doc_root_);
    }

    doc_root_ = fs::canonical(doc_root_);

  } catch (fs::filesystem_error& e) {
    LOG_ERRO("Doc root error: %s", e.what());
    doc_root_.clear();
  }

  LOG_INFO("Doc root: %s", doc_root_.u8string().c_str());
}

                               const std::string& url_path, bool* stream) {
  if (Router::MatchView(method, url_path, stream)) {
    fs::path sub_path = utility::TranslatePath(url_path);
    //LOG_INFO("Translated URL path: %s", sub_path.u8string().c_str());
    fs::path path = doc_root_ / sub_path;
  std::string url_path = Url::DecodeUnsafe(request->url().path());
  fs::path sub_path = utility::TranslatePath(url_path);
  fs::path path = doc_root_ / sub_path;
  // Check if doc root is valid.
  // Absolute it if necessary.
  void CheckDocRoot();

  // Match the view by HTTP method and URL path.
  // The |url_path| has already been decoded.
  // The |url_path| is UTF8 encoded by itself, and this is taken into account
  // when match the static files.
  bool MatchViewOrStatic(const std::string& method, const std::string& url_path,
#if (defined(_WIN32) || defined(_WIN64))
#include <Windows.h>
#endif

#if (defined(_WIN32) || defined(_WIN64))

// Wrapper for Windows API MultiByteToWideChar.
static std::wstring MB2WC(const std::string& input, unsigned int code_page) {
  if (input.empty()) {
    return L"";
  }

  int length = ::MultiByteToWideChar(code_page, 0, &input[0],
                                     static_cast<int>(input.size()),
                                     NULL, 0);

  std::wstring output(length, '\0');

  ::MultiByteToWideChar(code_page, 0, &input[0], static_cast<int>(input.size()),
                        &output[0], static_cast<int>(output.size()));

  return output;
}

// Wrapper for Windows API WideCharToMultiByte.
static std::string WC2MB(const std::wstring& input, unsigned int code_page) {
  if (input.empty()) {
    return "";
  }

  // There do have other code pages which require the flags to be 0, e.g.,
  // 50220, 50211, and so on. But they are not included in our charset
  // dictionary. So, only consider 65001 (UTF-8) and 54936 (GB18030).
  DWORD flags = 0;
  if (code_page != 65001 && code_page != 54936) {
    flags = WC_NO_BEST_FIT_CHARS | WC_COMPOSITECHECK | WC_DEFAULTCHAR;
  }

  int length = ::WideCharToMultiByte(code_page, flags, &input[0],
                                     static_cast<int>(input.size()), NULL, 0,
                                     NULL, NULL);

  std::string output(length, '\0');

  ::WideCharToMultiByte(code_page, flags, &input[0],
                        static_cast<int>(input.size()), &output[0],
                        static_cast<int>(output.size()), NULL, NULL);

  return output;
}

std::string Utf16To8(const std::wstring& utf16_string) {
  return WC2MB(utf16_string, CP_UTF8);
}

std::wstring Utf8To16(const std::string& utf8_string) {
  return MB2WC(utf8_string, CP_UTF8);
}

#endif  // defined(_WIN32) || defined(_WIN64)

#if (defined(_WIN32) || defined(_WIN64))
std::string Utf16To8(const std::wstring& utf16_string);
std::wstring Utf8To16(const std::string& utf8_string);
#endif

bool Url::Decode(string_view encoded, std::string* raw) {
  for (auto iter = encoded.begin(); iter != encoded.end(); ++iter) {
    if (*iter == '%') {
      if (++iter == encoded.end()) {
        // Invalid URI string, two hexadecimal digits must follow '%'.
        return false;
      }

      int h_decimal = 0;
      if (!HexToDecimal(*iter, &h_decimal)) {
        return false;
      }

      if (++iter == encoded.end()) {
        // Invalid URI string, two hexadecimal digits must follow '%'.
        return false;
      }

      int l_decimal = 0;
      if (!HexToDecimal(*iter, &l_decimal)) {
        return false;
      }

      raw->push_back(static_cast<char>((h_decimal << 4) + l_decimal));

    } else if (*iter > 127 || *iter < 0) {
      // Invalid encoded URI string, must be entirely ASCII.
      return false;
    } else {
      raw->push_back(*iter);
    }
  }

  return true;
}

std::string Url::DecodeUnsafe(string_view encoded) {
  std::string raw;
  if (Decode(encoded, &raw)) {
    return raw;
  }
  return ToString(encoded);
}

        parameters_.push_back(
            { Url::DecodeUnsafe(key), Url::DecodeUnsafe(value) });
  static bool Decode(string_view encoded, std::string* raw);

  // Unsafe decode.
  // Return the original string on failure.
  static std::string DecodeUnsafe(string_view encoded);

#include "boost/algorithm/string.hpp"

fs::path TranslatePath(const std::string& utf8_url_path) {
#if (defined(_WIN32) || defined(_WIN64))
  std::wstring url_path = Utf8To16(utf8_url_path);
  std::vector<std::wstring> words;
  boost::split(words, url_path, boost::is_any_of(L"/"),
               boost::token_compress_on);
#else
  std::vector<std::string> words;
  boost::split(words, utf8_url_path, boost::is_any_of("/"),
               boost::token_compress_on);
#endif  // defined(_WIN32) || defined(_WIN64)

  fs::path path;
  for (auto& word : words) {
    // Ignore . and ..
#if (defined(_WIN32) || defined(_WIN64))
    if (word == L"." || word == L"..") {
#else
    if (word == "." || word == "..") {
#endif
      continue;
    }

    fs::path p{ word };

    // Ignore C:\\, C:, path\\sub, ...
    // parent_path() is similar to Python os.path.dirname().
    if (!p.parent_path().empty()) {
      continue;
    }

    path /= p;
  }

  return path;
}

// Translate a /-separated URL path to the local (relative) path.
// Examples:
//   (Non-Windows)
//   "/path/to/file" -> "path/to/file"
//   "/path/./to/../file" -> "path/to/file" (. and .. are ignored)
//   "/path//to//file" -> "path/to/file"
//   (Windows)
//   "/path/to/file" -> "path\to\file"
//   "/path\\sub/to/file" -> "to\file" (path\\sub is ignored)
//   "/C:\\test/path" -> "path" (C:\\test is ignored)
// Reference: Python http/server.py translate_path()
fs::path TranslatePath(const std::string& utf8_url_path);