Please report security problems privately by email to support@uprock.com. Do not open a public issue or pull request for a vulnerability.
Include as much of the following as you can:
- A description of the problem and its impact
- The affected component (
fuel-engine,fuel-jni,fuel-network) and commit or version - Steps to reproduce, or a proof-of-concept bundle or test case
- Any suggested fix
We will acknowledge your report, keep you updated while we investigate, and credit you when the fix is published unless you ask us not to.
In scope: the code in this repository, including bundle parsing and signature verification, the capability checks, the private-range network filter, the WASI extensions, the virtual filesystem and the JNI bridge.
Out of scope: builds made without FUEL_PUBLIC_KEY (development mode accepts unsigned bundles by design) and fuel-network, which is not used in production. The known issues below are not new findings, but reports that show a practical impact are still welcome.
These are true of the code as published and are not new findings. Most of them can only be reached by a bundle signed with a trusted key; the exception is the development-mode default, which lets anyone run code on a build made without a key.
The private-range filter (fuel-engine/src/wasi_ext/ssrf.rs) applies to raw sockets only: every TCP connect and UDP send is checked against the resolved address. Browser operations are not filtered. The URL passed to browser_navigate goes to the host's BrowserProvider unchecked, and redirects, subresources, fetch/XHR from page scripts and scripts run through browser:execute_js are not filtered either.
Impact: a bundle granted browser:navigate can make the host's browser load loopback, private-network (RFC 1918), link-local (including 169.254.169.254) and other reserved addresses reachable from the device, and a bundle that also holds browser:observe or browser:execute_js can read the responses.
Mitigation today: only bundles signed with a trusted key can run in a production build, and a bundle must declare browser capabilities in its manifest to use the browser at all. Hosts that implement BrowserProvider can block private and reserved destinations in their own navigation and request handling.
Planned fix: apply the socket private-range check to the navigate URL and block non-HTTP(S) schemes such as javascript:, data:, file: and content:.
If FUEL_PUBLIC_KEY is not set at build time, or fuel-jni is built with the dev-mode feature, the engine runs in development mode and accepts unsigned bundles. A plain cargo build is therefore fail-open, and nothing stops a build without the key.
browser_navigate accepts a list of scripts to inject before the page's own scripts run. This is arbitrary JavaScript, but it requires only browser:navigate, not browser:execute_js.
A bundle with a valid signature stays valid forever. The manifest's build timestamp and module version are parsed but not checked, and there is no minimum version, expiry or denylist.
Retiring a bundle by no longer releasing its key only reaches devices that don't already hold that key. Our Android app keeps the key-encryption keys it receives in memory (never on disk) until the app process exits, and keeps an unlocked bundle loaded for up to 10 minutes after its last use.
Not blocked today: NAT64 (64:ff9b::/96), 6to4 (2002::/16), Teredo (2001::/32), IPv4-compatible IPv6 (::a.b.c.d), 0.0.0.0/8 (only 0.0.0.0 itself is blocked), 192.0.0.0/24, 240.0.0.0/4, fec0::/10 and the IPv6 documentation range 2001:db8::/32. NAT64 matters most, since IPv6-only mobile networks use it to reach IPv4 hosts.
Manifest limits are clamped to the host maximum, but memory (max_memory_pages), stack size and stdout/stderr size are not enforced; stdout and stderr are unbounded in-memory pipes. Only /tmp has a size cap; /workspace and /site-packages are writable without one. zstd decompression of the payloads is not bounded by the declared size.
When the engine is called with a timeout of 0, it runs without an instruction budget. The JNI bridge replaces 0 with a 10-minute default, so this can't happen through it. The bridge does not cap positive timeouts itself; in our deployment the server limits task timeouts to 15 minutes.
The filesystem, clock, random and BLAS capabilities are listed but not enforced. TCP and UDP are a single switch: granting either wasi:sockets:tcp or wasi:sockets:udp enables every socket function. The JNI bridge uses the engine's default host policy, which grants every capability a manifest requests and allows all public network destinations.
proc_spawn always returns "not supported", so no bundle can start a process and the child-process limits are never reached. The manifest's process_allowlist is parsed but never read.
Key-encryption keys, data keys and decrypted payloads are freed normally (keys after loading, payloads when the engine closes), not zeroed.
nativeRun and nativeClose turn the long handle back into a pointer without checking it. Using a handle after close, or from two threads at once, is undefined behaviour. The Kotlin wrapper guards against double close but callers must not share an engine across threads.
fuel-network is not used in production and is out of scope, but note that it has no private-range filter and follows up to five redirects unchecked. Add both before using it.