Skip to content

WASI 0.2: open-at allows paths to escape preopened directories #458

Description

@str8outtaheap

Summary

Walrus implements WASI 0.2 descriptor.open-at by appending the component-supplied path to the host path of a preopened directory. It does not reject parent-directory segments or ensure the resolved file remains below that preopen.

The PoC maps one host directory as /sandbox, opens ../outside.txt, and reads the file from the parent directory. An untrusted component can use .. to read files outside the directory exposed through --mapdirs. It can access any file readable by the Walrus process if it knows or guesses the path.

Affected Version

Walrus at commit 182b5f2e79801a50bfd69b94286252770aea8f1b, tested on Ubuntu 24.04 aarch64.

Steps to Reproduce

Build the shell with ASan:

cmake -S . -B out/asan-release/aarch64 -G Ninja \
  -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
  -DCMAKE_BUILD_TYPE=Release \
  -DWALRUS_ARCH=aarch64 \
  -DWALRUS_HOST=linux \
  -DWALRUS_MODE=release \
  -DWALRUS_OUTPUT=shell \
  -DWALRUS_ASAN=ON

cmake --build out/asan-release/aarch64 --parallel

Create a mapped directory and a file outside it:

mkdir -p /tmp/walrus-poc/preopen
printf 'HOST_SECRET_1234' > /tmp/walrus-poc/outside.txt

Run the PoC:

ASAN_OPTIONS=detect_leaks=0 \
./out/asan-release/aarch64/walrus \
  --mapdirs /tmp/walrus-poc/preopen /sandbox \
  poc/wasi-preopen-path-traversal-poc.wasm

Expected Result

descriptor.open-at should reject ../outside.txt because it resolves outside the preopened directory. The component should not receive any bytes from the outside file.

Actual Result

HOST_SECRET_1234

Root Cause

fileSystemDescriptorOpenAt02 builds a host path by joining two strings and passes it directly to uv_fs_open:

std::string path = asDirectory(handle)->realPath();
path.append("/");
CanonOptions::UtfData utfData;
options->validateString(state, pathStart, pathSize, &utfData);
if (options->encoding() == ComponentCanonOptions::Utf8) {
    path.append(reinterpret_cast<const char*>(utfData.buffer()), utfData.length());
} else {
    std::vector<uint8_t> utf8String(utfData.utf8Length());
    utfData.toUtf8String(utf8String.data());
    path.append(reinterpret_cast<const char*>(utf8String.data()), utf8String.size());
}

uv_fs_t req;
int descriptor = uv_fs_open(NULL, &req, path.c_str(), openFlags, 0666, NULL);

Neither this function nor the directory resource rejects .., handles symlink escape, or checks that the opened object remains beneath the preopened directory.

Suggested Fix

Resolve component paths relative to the preopened directory descriptor and enforce WASI path rules one component at a time. Reject absolute paths and any .. or symlink traversal that would escape the preopen. Avoid string-prefix checks on resolved paths because they are vulnerable to prefix confusion and filesystem races.

wasi-preopen-path-traversal-poc.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions