Bug: HTTP server serves files outside documentRoot via relative path segments in directory mode

Author: Pcmhacker-piroCreated Sep 18, 2026Updated Sep 18, 2026

Summary

The embedded HTTP static file server does not sanitize relative path segments (../) in URL requests when running in directory resource mode. An HTTP client can traverse outside the application document root and read files that should not be served.

Affected Component

  • server/neuserver.cpphandleHTTP() passes the raw URL resource to the router without path validation (line 239-242).
  • server/router.cppserve() URL-decodes the path but does not verify it stays within documentRoot (lines 441-466).
  • resources.cppgetFile() in directory mode resolves via simple string concatenation (appPath + filename) without canonicalization (line 215).

Steps to Reproduce

  1. Create a starter app: npx @neutralinojs/neu create test-app && cd test-app
  2. Launch the Neutralinojs binary in directory/cloud mode:
    bash
    ./bin/neutralino-mac_arm64 --load-dir-res --path=. --mode=cloud --port=8899
  3. Send an HTTP request with ../ path segments:
    bash
    curl -s "http://localhost:8899/../neutralino.config.json" --path-as-is
  4. The server returns HTTP 200 OK with the full contents of neutralino.config.json, which is outside the configured documentRoot (/resources/).

Expected Behavior

The server should reject or sanitize requests that resolve outside the document root, returning 403 Forbidden or 404 Not Found.

Actual Behavior

The server follows ../ path segments and serves any file accessible by the process, including files outside the application directory.

Suggested Fix

Canonicalize the resolved file path using std::filesystem::weakly_canonical() and verify the result starts with the canonical document root before serving:

cpp
namespace fs = std::filesystem;
auto rootPath = fs::weakly_canonical(settings::joinAppPath(neuserver::getDocumentRoot()));
auto targetPath = fs::weakly_canonical(settings::joinAppPath(path));

auto [rootEnd, _] = std::mismatch(rootPath.begin(), rootPath.end(), targetPath.begin());
if (rootEnd != rootPath.end()) {
    return router::Response{ websocketpp::http::status_code::forbidden, "text/plain", "Forbidden" };
}

Environment

  • Neutralinojs version: v6.9.0
  • OS: macOS (ARM64)
  • Resource mode: directory (--load-dir-res)
  • App mode: cloud (also reproducible in other modes)

Source: neutralinojs/neutralinojs