c++poco-librariesdirectory-traversal

Prevent directory traversal attack in Poco Server Application


I have a Poco server application project. This server application works as web server. Now I want to harden it against directory traversal attacks and I'm looking for the best way to secure that the files the server serves are from inside wwwRoot. The structure is

project
|-src
| |-main.cpp
|-CMakeLists.txt
|-conanfile.txt
|-build
  |-...

E.g. when I use build as wwwRoot, conanfile.txt is outside wwwRoot but with localhost:8080/../conanfile.txt I can open it. I know that I can search the path for dot segments but I heard that it's not really secure since there are hacks like encoded paths. Since Poco is a framework for server applications I assume, that there already is such a function to check if filePath is inside wwwRoot but I can't find it.

The src/main.cpp contains:

#include <Poco/Net/HTTPServer.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Util/ServerApplication.h>
#include <Poco/Net/HTTPRequestHandler.h>
#include <Poco/Net/HTTPRequestHandlerFactory.h>
#include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Path.h>
#include <string>

class FileHandler: public Poco::Net::HTTPRequestHandler {
public:
    explicit FileHandler(const Poco::Path &wwwRoot);
protected:
    void handleRequest(Poco::Net::HTTPServerRequest &request,
                       Poco::Net::HTTPServerResponse &response) override;
private:
    Poco::Path wwwRoot;
};

FileHandler::FileHandler(const Poco::Path &aWwwRoot) : wwwRoot(aWwwRoot) {}

void FileHandler::handleRequest(Poco::Net::HTTPServerRequest &request,
                   Poco::Net::HTTPServerResponse &response) {
    Poco::Util::Application &app = Poco::Util::Application::instance();
    app.logger().information("FileHandler Request from " + request.clientAddress().toString() + ": " + request.getURI());


    Poco::Path path(wwwRoot.absolute(), request.getURI());
    std::string filePath(path.toString());
    app.logger().information(filePath);
    response.sendFile(filePath, "text/html");
}

class HTTPRequestHandlerFactory: public Poco::Net::HTTPRequestHandlerFactory {
public:
    explicit HTTPRequestHandlerFactory(const Poco::Path &wwwRoot);
protected:
    Poco::Net::HTTPRequestHandler* createRequestHandler(
            const Poco::Net::HTTPServerRequest& request) override;
private:
    Poco::Path wwwRoot;
};

HTTPRequestHandlerFactory::HTTPRequestHandlerFactory(const Poco::Path &aWwwRoot) : wwwRoot(aWwwRoot) {}

Poco::Net::HTTPRequestHandler* HTTPRequestHandlerFactory::createRequestHandler(
        const Poco::Net::HTTPServerRequest &) {
    return new FileHandler(wwwRoot);
}

class ServerApplication : public Poco::Util::ServerApplication {
protected:
    void initialize(Application& self) override;
    int main(const std::vector<std::string> &args) override;
};

void ServerApplication::initialize(Application& self) {
    loadConfiguration();
    Poco::Util::ServerApplication::initialize(self);
}

int ServerApplication::main(const std::vector<std::string> &) {

    Poco::Net::ServerSocket svs(8080);
    Poco::Net::HTTPServer srv(new HTTPRequestHandlerFactory(Poco::Path(".").absolute()),
                              svs, new Poco::Net::HTTPServerParams);
    srv.start();
    waitForTerminationRequest();
    srv.stop();
    return Application::EXIT_OK;
}

int main(int argc, char **argv) {
  ServerApplication app;
  return app.run(argc, argv);
}

The CMakeLists.txt contains:

cmake_minimum_required (VERSION 3.5.1)
project (Sandbox)

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Debug)
endif()

set(CMAKE_CXX_FLAGS "-Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
set(CMAKE_CXX_STANDARD 14)

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS)

add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE CONAN_PKG::Poco)

The conanfile.txt contains:

[requires]
Poco/1.9.0@pocoproject/stable

[generators]
cmake

In build the project is built


Solution

  • You'll have to decode the request path and build the local path step by step and check for ".." path segments. Here's a snippet that does this, and also handles some of the other things you should take care of when serving files. You'll need to write the mapContentType() method, or do something equivalent.

    Poco::URI uri(request.getURI());
    std::string decodedPath = uri.getPath();
    
    Poco::Path requestPath(decodedPath, Poco::Path::PATH_UNIX);
    Poco::Path localPath(wwwRoot.absolute());
    localPath.makeDirectory();
    
    bool valid = true;
    for (int i = 0; valid && i < requestPath.depth(); i++)
    {
        if (requestPath[i] != "..")
            localPath.pushDirectory(requestPath[i]);
        else
            valid = false;
    }
    if (valid)
    {
        localPath.setFileName(requestPath.getFileName());
        Poco::File requestedFile(localPath.toString());
        if (requestedFile.exists())
        {
            std::string contentType = mapContentType(localPath.getExtension());
    
            if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_HEAD)
            {
                response.set("Last-Modified", Poco::DateTimeFormatter::format(dateTime, Poco::DateTimeFormat::HTTP_FORMAT));
                response.setContentLength64(requestedFile.getSize());
                response.setContentType(contentType);
                response.send();
            }
            else if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET)
            {
                response.sendFile(localPath.toString(), contentType);
            }
            else
            {
                response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED);
                response.send();
            }
        }
        else
        {
            response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_NOT_FOUND);
            response.send();
        }
    }
    else
    {
        response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_NOT_FOUND);
        response.send();
    }