The vendor patched a paper cut; the public exploit turns it into an unauthenticated root shell
Windmill's /api/w/{workspace}/jobs_u/get_log_file/{filename} endpoint concatenates the filename parameter directly into format!("{TMP_DIR}/logs/{file_p}") without sanitization. The jobs_u route prefix means no authentication is required. Any attacker who can reach the HTTP listener can walk out of /tmp/windmill with ..%2f sequences and read arbitrary files as the Windmill process user. Affected: v1.309.0 through v1.603.2; fixed in v1.603.3 (January 2026). The bundled Nextcloud Flow proxy (v1.0.0–v1.2.2) is reachable via a triple-encoded traversal (..%25252f) that survives httpx normalization — fixed in Flow v1.3.0.
The vendor's CVSS of 7.5 (C:H/I:N/A:N) treats this as a pure disclosure bug. That framing is wrong. Chocapikk's public *Windfall* exploit demonstrates the realistic chain: read /proc/1/environ to steal SUPERADMIN_SECRET, or read PostgreSQL heap files (base/{oid}/17149) to lift jwt_secret and forge an admin JWT, then POST to /api/w/{ws}/jobs/run/preview to execute bash/Python — and because most deployments mount /var/run/docker.sock, that job breaks the container and owns the host. Vendor-severity HIGH understates it; this is unauthenticated RCE with active exploitation confirmed by VulnCheck.
5 steps from start to impact.
Discover an exposed Windmill or Nextcloud Flow instance
"/_app/immutable/entry" && "windmill" or enumerates Nextcloud installs with the Flow app registered. VulnCheck telemetry shows ~170 directly exposed instances across 24 countries — small population but the target list is trivial to build.- Network reachability to the Windmill HTTP listener or a Nextcloud instance hosting Flow
- Only ~170 direct instances internet-exposed
- Most Windmill deployments live inside private VPCs behind SSO reverse proxies
access_level=0 PUBLIC — hidden inside otherwise-authenticated hosts.Trigger the unauthenticated path traversal (Windfall / manual curl)
GET /api/w/anything/jobs_u/get_log_file/..%2F..%2F..%2F..%2F..%2F..%2Fproc%2F1%2Fenviron. Any workspace name works because the route resolves before workspace validation. Against Flow, triple-encode as ..%25252f to bypass Nextcloud's PHP decode and httpx forwarding. Weaponized as Chocapikk/Windfall.- Vulnerable Windmill (<1.603.3) or Flow (<1.3.0)
- HTTP reachable
- Empty
SUPERADMIN_SECRETin some deployments — attacker must fall through to the PG heap path
../ and %2f%2e%2e; triple-encoded variant frequently bypasses default rulesets.Harvest credentials from /proc/1/environ or PostgreSQL heap files
/proc/1/environ to pull SUPERADMIN_SECRET — Windmill compares it as an unhashed bearer token. Flow deployments: read /nc_app_flow_data/pgsql/base/{oid}/17149 (global_settings) to lift jwt_secret, then forge a JWT with is_super_admin=true. Filenodes are hardcoded per the Windfall research.SUPERADMIN_SECRETset (optional) OR PostgreSQL colocated with the Windmill process
- If
SUPERADMIN_SECRETis unset AND Postgres runs on a separate host, this specific credential path fails — but the attacker can still exfiltrate secrets, source code, and workflow definitions
windmill process. Postgres audit logging does not fire because the reads never touch the DB engine.Authenticate as super-admin and submit a preview job
/api/w/{workspace}/jobs/run/preview with a bash or Python payload. Windmill executes it inside a worker container as root. This is the intended platform feature — running arbitrary code is what Windmill *does*.- Valid super-admin token from step 3
- At least one workspace exists (default install has one)
- None — code execution is the product's core capability
windmill-worker; unless behavioral policy already flags anonymous job submissions, this looks legitimate.Escape the worker container via mounted Docker socket
/var/run/docker.sock into the worker. The attacker's preview job invokes docker run -v /:/host --privileged to spawn a root shell on the underlying host. Fleet compromise if the host is a shared automation node.- Docker socket mount (default in
docker-compose.yml)
- Kubernetes deployments without socket mount stop here — but retain full RCE inside the worker pod and any secrets it can reach
docker.sock rules fire; most enterprises don't run Falco on internal developer-platform hosts.The supporting signals.
| In-the-wild status | Active exploitation confirmed by VulnCheck; mass scanning against both direct Windmill and Nextcloud Flow proxy paths as of July 2026 |
|---|---|
| Public PoC / weaponization | Chocapikk/Windfall — full unauth-to-RCE chain by *Valentin Lobstein*, published with technical writeup |
| EPSS | 0.02584 (~76th percentile) — understated; will re-rank sharply after VulnCheck telemetry propagates |
| KEV status | Not KEV-listed as of 2026-07-23 despite VulnCheck's active-exploitation call — CISA lag is a known reality; do not wait |
| CVSS 3.1 vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (vendor) — misses the RCE chain; real vector is closer to AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (9.8–10.0) |
| Affected versions | Windmill v1.309.0 – v1.603.2; Nextcloud Flow v1.0.0 – v1.2.2 |
| Fixed versions | Windmill v1.603.3 (January 2026); Nextcloud Flow v1.3.0. No distro backports — Windmill is upstream-only |
| Exposure | ~170 direct instances across 24 countries per VulnCheck. Shodan: "/_app/immutable/entry" && "windmill". Flow instances invisible to fingerprinting — true population is larger |
| Disclosure date | 2026-03-06 (GHSA); patch predates disclosure (January 2026) |
| Reporter | *Valentin Lobstein* (Chocapikk), independent researcher — coordinated disclosure with Windmill Labs |
noisgate verdict.
Upgraded from vendor HIGH 7.5 to CRITICAL 9.8 because the single decisive factor is a public, weaponized unauthenticated-to-RCE chain against a component that executes arbitrary code by design — the vendor scored the file-read primitive in isolation, but Windfall demonstrates it is a full pre-auth root RCE. Active exploitation confirmed by VulnCheck compounds this; small exposed population (~170) does not depress severity for defenders who are in that population.
Why this verdict
- Vendor undersizing: The 7.5 score reflects only
C:H/I:N/A:N. The publicly documented chain reachesI:H/A:Hvia forged super-admin JWT →/jobs/run/preview→ root code execution. Baseline correction alone lifts this to 9.8. - Active exploitation: VulnCheck telemetry (published July 2026) shows opportunistic scanning and successful reads of
/etc/passwdand/proc/1/environin the wild. Not KEV-listed yet, but that is a paperwork gap, not a threat-model gap. - No auth, no user interaction, no chain to another vuln: A single HTTP GET reaches the primitive. Every prerequisite after that (SUPERADMIN_SECRET or PG heap read) is satisfied by the same primitive.
- Role multiplier — Windmill is a workflow/automation orchestrator (CI/CD-adjacent): Compromising it yields all stored connection credentials (S3, databases, cloud IAM, Slack tokens, git deploy keys) and the ability to inject code into scheduled jobs — supply-chain pivot into every system Windmill orchestrates. This is a canonical high-value role.
- Role multiplier — default
/var/run/docker.sockmount: Fleet-scale host compromise from a single unauth GET on default Compose deployments. Kubernetes users escape only their pod but still own all Windmill-held secrets. - Population caveat: ~170 internet-exposed instances is small — but Windmill's typical deployment sits behind an SSO proxy on internal networks, where any post-initial-access attacker (phished laptop, compromised VPN) trivially reaches it. Internal exposure vastly exceeds the 170 figure.
Why not higher?
There is no CRITICAL+ bucket. Score capped at 9.8 rather than 10.0 because Scope:Changed is arguable (worker container ≠ trust boundary in all deployments) and because the Kubernetes-without-socket configuration reduces the host-compromise step to a pod-scoped RCE.
Why not lower?
Downgrade to HIGH would require ignoring the public weaponized chain and the VulnCheck exploitation evidence — both of which are documented and current. The small internet-exposed population does not save internal-only deployments; a single phished workstation puts the attacker on the same network as the Windmill listener, at which point the chain runs unchanged.
What to do — in priority order.
- Upgrade Windmill to ≥ v1.603.3 (Flow to ≥ v1.3.0) — The only durable fix. Sanitizes
filenameinget_log_file. Deploy within 3 days per noisgate CRITICAL mitigation SLA; complete rollout to all instances (including forgotten dev/staging) within 90 days per remediation SLA. - Block
/api/w/*/jobs_u/get_log_file/*at the reverse proxy immediately — Add an Nginx/Traefik/Envoylocationblock returning 403 for that path pattern. Zero legitimate external consumers; log-fetch is UI-only. Deploy within hours — this is your temporary shield until the upgrade lands. - Rotate every secret Windmill has ever seen — Assume super-admin compromise for any instance exposed between v1.309.0 release and your patch time. Rotate
SUPERADMIN_SECRET,jwt_secret, all workspace-stored resource credentials (S3, DB, cloud IAM, git deploy keys, webhook tokens). Complete within 7 days — attackers exfiltrate credentials silently and use them weeks later. - Remove
/var/run/docker.sockmount from worker containers — Editdocker-compose.ymlto drop the- /var/run/docker.sock:/var/run/docker.sockline unless a specific workflow requires Docker-in-Docker. Breaks the container-escape step of the chain. Deploy within 3 days. - Egress-filter Windmill worker nodes — Restrict outbound to the specific endpoints your workflows need (git, S3, internal APIs). Denies exfiltration and second-stage C2 even if RCE lands. Deploy within 30 days.
- Audit
/tmp/windmill/logs/access logs for..or%2e%2eor%252e— Grep the reverse-proxy access logs from 2026-01-01 forward for path-traversal indicators againstjobs_u/get_log_file. Any hit = assume breach and pivot to IR. Do this today.
- Windmill's own RBAC / workspace permissions — the
jobs_uroute bypasses workspace auth entirely; RBAC is not evaluated. - Placing Windmill behind SSO (Okta/Azure AD proxy) — helps for external attackers only. Any post-initial-access attacker on the internal network reaches the listener directly.
- Default OWASP CRS 3.x WAF rules — the triple-encoded
..%25252fFlow bypass evades default 930xxx rules; you need a targeted deny on the specific route. - Unsetting
SUPERADMIN_SECRET— closes one credential path but leaves the PostgreSQL heap-file path, source code, and workflow secret exfiltration wide open. - EDR on the Windmill host — the primary primitive is a file read by the legitimate Windmill process; there is no malicious process to flag until the RCE stage.
Crowdsourced verification payload.
Run on an auditor workstation with curl installed against each Windmill URL you want to check. Invoke as ./check-cve-2026-29059.sh https://windmill.example.com. No credentials required — the check exploits the same unauth primitive to prove exposure. Safe read of /etc/hostname only.
#!/usr/bin/env bash
# noisgate verification for CVE-2026-29059 (Windmill unauth path traversal)
# Usage: ./check-cve-2026-29059.sh https://windmill.example.com
# Exit codes: 0=PATCHED 1=VULNERABLE 2=UNKNOWN
set -u
TARGET="${1:-}"
if [[ -z "$TARGET" ]]; then
echo "Usage: $0 <windmill-base-url>" >&2
exit 2
fi
TARGET="${TARGET%/}"
# 1. Try to read /etc/hostname via the vulnerable endpoint. Any workspace name works.
PAYLOAD="..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fhostname"
URL="${TARGET}/api/w/noisgate-probe/jobs_u/get_log_file/${PAYLOAD}"
RESP=$(curl -sk --max-time 10 -o /tmp/nsg_body.$$ -w "%{http_code}" "$URL") || {
echo "UNKNOWN: could not reach $TARGET"
rm -f /tmp/nsg_body.$$
exit 2
}
BODY=$(head -c 512 /tmp/nsg_body.$$ 2>/dev/null)
rm -f /tmp/nsg_body.$$
# A patched instance returns 400/403/404 or a sanitized error.
# A vulnerable instance returns 200 with the file contents (hostname is short alphanumeric).
if [[ "$RESP" == "200" ]] && [[ -n "$BODY" ]] && [[ ! "$BODY" =~ (error|not.found|invalid|sanitiz) ]] && [[ ${#BODY} -lt 256 ]]; then
echo "VULNERABLE: $TARGET returned file contents via traversal"
echo " first-bytes: $(echo "$BODY" | tr -d '\n' | cut -c1-80)"
exit 1
fi
# 2. Fallback: fingerprint version from /version endpoint
VER=$(curl -sk --max-time 10 "${TARGET}/api/version" 2>/dev/null | grep -oE '1\.[0-9]+\.[0-9]+' | head -1)
if [[ -n "$VER" ]]; then
# Compare against 1.603.3
IFS='.' read -r A B C <<<"$VER"
if (( A > 1 )) || (( B > 603 )) || { (( B == 603 )) && (( C >= 3 )); }; then
echo "PATCHED: $TARGET running Windmill $VER (>= 1.603.3)"
exit 0
else
echo "VULNERABLE: $TARGET running Windmill $VER (< 1.603.3)"
exit 1
fi
fi
echo "UNKNOWN: $TARGET responded HTTP $RESP but no version banner and traversal probe inconclusive"
exit 2
If you remember one thing.
/api/w/*/jobs_u/get_log_file/* on every Windmill instance within hours — the noisgate mitigation SLA for CRITICAL is ≤ 3 days but exploitation-in-the-wild overrides that to *immediately*; (2) grep proxy access logs from Jan 2026 forward for .. / %2e%2e / %252e on that path and open an IR ticket for any hit; (3) upgrade to Windmill 1.603.3 / Flow 1.3.0 across production, staging, and every forgotten dev instance within the noisgate remediation SLA of ≤ 90 days — aim for 14; (4) rotate every credential Windmill has stored (workspace resources, SUPERADMIN_SECRET, jwt_secret, deploy keys) within 7 days on the assumption that exposed instances are already compromised; (5) remove the docker.sock mount from worker containers so the next bug of this class doesn't become fleet compromise.Sources
- VulnCheck / Hacker News — active exploitation report
- Chocapikk — Windfall technical writeup (exploit chain)
- Chocapikk/Windfall — public PoC repository
- SentinelOne vulnerability database entry
- OpenCVE — CVE-2026-29059 details
- GBHackers coverage — RCE PoC published
- OffSeq Threat Radar — CWE-22 classification
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.