ekofyi
Security Research6 min read

Windmill's nsjail Sandbox Has a Gaping Hole: /etc Mounted Read-Write by Default

CVE-2026-47107 exposes a critical sandbox escape path in Windmill's nsjail configuration where /etc is bind-mounted read-write, letting authenticated users overwrite passwd, resolv.conf, and more. Here's how it works and what to do.

Your Sandbox Isn't Sandboxing Anything If /etc Is Writable

Let me paint you a picture: you're running Windmill, the open-source workflow engine, and you feel good because scripts execute inside an nsjail sandbox. Isolation. Security. Defense in depth. Except the sandbox has been shipping with /etc bind-mounted read-write into the jail since... well, for a while. Any authenticated user who can run a script can overwrite /etc/passwd, /etc/resolv.conf, /etc/nsswitch.conf, or any other file in that directory.

This is CVE-2026-47107, scored CVSS 8.1 HIGH, and it's the kind of vulnerability that makes you question whether the sandbox was ever actually tested from an attacker's perspective. If you're running Windmill < 1.703.2 in any environment where users aren't 100% trusted (so... every environment), you need to patch immediately.

The fix landed in version 1.703.2, but the real lesson here is about default configurations and the false sense of security that sandboxes provide when they're misconfigured at the foundation.

What Happened

Windmill uses nsjail to isolate script execution — Python, TypeScript, Go, Bash, whatever your workflows run. nsjail is Google's lightweight process isolation tool that uses Linux namespaces, seccomp-bpf, and bind mounts to create sandboxed environments. It's solid technology when configured correctly.

The problem is in Windmill's nsjail configuration files. The /etc directory from the host (or container) is bind-mounted into the sandbox without specifying read-only restrictions. In nsjail's protobuf config format, bind mounts default to read-write unless you explicitly set is_ro: true. Windmill's configs didn't set that flag for /etc.

This means any authenticated user who can execute a script through Windmill — which is the entire point of the platform — gets write access to /etc inside the namespace. Depending on how the jail is configured and what's shared, this can lead to privilege escalation, DNS hijacking within the container, or full sandbox escape.

The vulnerability was assigned CVE-2026-47107 and affects all versions of Windmill prior to 1.703.2.

Technical Deep-Dive: How the Attack Works

Let's look at what a vulnerable nsjail configuration looks like. In Windmill's config (typically found in the nsjail protobuf configs), you'd see something like:

protobuf
mount {
  src: "/etc"
  dst: "/etc"
  is_bind: true
  # Notice: no is_ro: true
}

When is_ro is omitted, nsjail defaults to read-write. That's the entire vulnerability. One missing line.

Now here's what an attacker can do from inside a Windmill script. Say you have a Python script running in the sandbox:

python
# Running inside Windmill as any authenticated user
import os

# Overwrite /etc/passwd to add a root-equivalent user
with open('/etc/passwd', 'a') as f:
    f.write('backdoor:x:0:0:backdoor:/root:/bin/bash\n')

# Or hijack DNS resolution
with open('/etc/resolv.conf', 'w') as f:
    f.write('nameserver 10.13.37.1\n')  # attacker-controlled DNS

# Or mess with NSS to redirect authentication lookups
with open('/etc/nsswitch.conf', 'w') as f:
    f.write('passwd: files ldap\nhosts: files dns\n')

The DNS hijacking angle is particularly nasty. If other processes in the same namespace (or container, depending on deployment) use the same /etc/resolv.conf, you've just redirected all their DNS queries to an attacker-controlled server. That's credential theft, MITM on API calls, the works.

The /etc/passwd manipulation is interesting too. While modern systems use /etc/shadow for actual password hashes, many applications still parse /etc/passwd for UID mapping. Adding a UID 0 entry can confuse tooling and, in some configurations, grant elevated access.

The root cause is straightforward: nsjail's security model requires explicit opt-in to restrictions. If you don't say "read-only," it's read-write. Windmill's developers presumably bind-mounted /etc so that scripts could read DNS configuration, CA certificates, timezone data, and other standard system files. But they forgot (or didn't realize) that the mount needed to be locked down.

This is a pattern I see constantly in container and sandbox configurations. The developer thinks "I need the script to be able to resolve DNS, so I'll mount /etc." They test it, it works, they move on. Nobody asks "but can the script write to /etc?" until an attacker does exactly that.

Impact: Who Should Be Worried

Windmill is used as a workflow automation platform — think internal tooling, data pipelines, scheduled jobs, developer self-service. It's designed for multi-user environments where different team members write and execute scripts. That's exactly the threat model where this vulnerability is devastating.

Any authenticated user — not just admins — can exploit this. In organizations using Windmill for internal automation, that could be dozens or hundreds of users. A disgruntled employee, a compromised account, or even a supply chain attack through a shared workflow template could trigger this.

The blast radius depends on deployment. If Windmill runs in a container (which is common), the impact might be contained to that container's /etc. But if it's running on bare metal or if the container shares namespaces with other services, you're looking at lateral movement opportunities. Either way, within the Windmill execution context, an attacker owns the network configuration.

What To Do Right Now

First, upgrade to Windmill 1.703.2 or later. That's the fix. Do it today.

bash
# If running via Docker
docker pull ghcr.io/windmill-labs/windmill:1.703.2

# If using Helm
helm repo update
helm upgrade windmill windmill/windmill --set image.tag=1.703.2

If you can't upgrade immediately, you can patch the nsjail configuration manually. Find the nsjail config files (check Windmill's installation directory or container image) and add is_ro: true to the /etc mount:

protobuf
mount {
  src: "/etc"
  dst: "/etc"
  is_bind: true
  is_ro: true
}

You should also audit your nsjail configs for other mounts that might be missing read-only flags. Run this to check what's currently mounted read-write in your sandbox:

bash
# Inside a running Windmill container, check mount options
grep -v "ro" /proc/self/mounts | grep -E "^/"

Warning: If you've been running a vulnerable version in a multi-tenant environment, assume compromise. Check /etc/passwd, /etc/resolv.conf, and other critical files inside your Windmill containers for unauthorized modifications. Review execution logs for scripts that write to /etc.

Finally, consider whether your Windmill deployment actually needs /etc mounted at all. If scripts only need DNS resolution, you can mount just /etc/resolv.conf and /etc/hosts as individual read-only bind mounts instead of the entire directory.

The Bigger Picture

This vulnerability is a textbook example of why "we use a sandbox" is not a security statement. A sandbox is only as strong as its configuration, and default configurations are almost never secure enough for adversarial environments. nsjail is a solid tool — this isn't nsjail's fault. It's a configuration error that persisted because nobody audited the mount permissions from an attacker's perspective.

Every time you see a bind mount in a sandbox or container config, ask yourself: does this need to be writable? The answer is almost always no. Default-deny on write permissions for bind mounts should be a standard practice in any isolation configuration. If you're building systems that use nsjail, gVisor, or even just Docker bind mounts — go audit your configs right now. I guarantee you'll find at least one mount that's more permissive than it needs to be.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.