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
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
Summary
Walrus implements WASI 0.2
descriptor.open-atby 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.04aarch64.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 --parallelCreate a mapped directory and a file outside it:
Run the PoC:
Expected Result
descriptor.open-atshould reject../outside.txtbecause it resolves outside the preopened directory. The component should not receive any bytes from the outside file.Actual Result
Root Cause
fileSystemDescriptorOpenAt02builds a host path by joining two strings and passes it directly touv_fs_open: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