← Back to Feed CACHED · 2026-07-22 20:45:54 · CACHE_KEY CVE-2026-29059
CVE-2026-29059 · CWE-22 · Disclosed 2026-03-06

Windmill is an open-source developer platform for internal code: APIs

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

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.

"Vendor scored the file read only. The public exploit chains it to unauth RCE as root — and VulnCheck confirms in-the-wild abuse. Patch now."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Discover an exposed Windmill or Nextcloud Flow instance

The attacker fingerprints Windmill via Shodan dork "/_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.
Conditions required:
  • Network reachability to the Windmill HTTP listener or a Nextcloud instance hosting Flow
Where this breaks in practice:
  • Only ~170 direct instances internet-exposed
  • Most Windmill deployments live inside private VPCs behind SSO reverse proxies
Detection/coverage: Shodan/Censys queries above; Nextcloud Flow instances are invisible to standard scanners because the route registers as access_level=0 PUBLIC — hidden inside otherwise-authenticated hosts.
STEP 02

Trigger the unauthenticated path traversal (Windfall / manual curl)

The attacker issues 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.
Conditions required:
  • Vulnerable Windmill (<1.603.3) or Flow (<1.3.0)
  • HTTP reachable
Where this breaks in practice:
  • Empty SUPERADMIN_SECRET in some deployments — attacker must fall through to the PG heap path
Detection/coverage: WAFs with default OWASP CRS 3.x rules 930xxx flag ../ and %2f%2e%2e; triple-encoded variant frequently bypasses default rulesets.
STEP 03

Harvest credentials from /proc/1/environ or PostgreSQL heap files

Standalone Windmill: read /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.
Conditions required:
  • SUPERADMIN_SECRET set (optional) OR PostgreSQL colocated with the Windmill process
Where this breaks in practice:
  • If SUPERADMIN_SECRET is 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
Detection/coverage: None at the OS layer — file reads issued by the legitimate windmill process. Postgres audit logging does not fire because the reads never touch the DB engine.
STEP 04

Authenticate as super-admin and submit a preview job

POST the harvested bearer token or forged JWT to /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*.
Conditions required:
  • Valid super-admin token from step 3
  • At least one workspace exists (default install has one)
Where this breaks in practice:
  • None — code execution is the product's core capability
Detection/coverage: EDR sees a child process of windmill-worker; unless behavioral policy already flags anonymous job submissions, this looks legitimate.
STEP 05

Escape the worker container via mounted Docker socket

Default Windmill Compose files mount /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.
Conditions required:
  • Docker socket mount (default in docker-compose.yml)
Where this breaks in practice:
  • Kubernetes deployments without socket mount stop here — but retain full RCE inside the worker pod and any secrets it can reach
Detection/coverage: Falco docker.sock rules fire; most enterprises don't run Falco on internal developer-platform hosts.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusActive exploitation confirmed by VulnCheck; mass scanning against both direct Windmill and Nextcloud Flow proxy paths as of July 2026
Public PoC / weaponizationChocapikk/Windfall — full unauth-to-RCE chain by *Valentin Lobstein*, published with technical writeup
EPSS0.02584 (~76th percentile) — understated; will re-rank sharply after VulnCheck telemetry propagates
KEV statusNot 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 vectorAV: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 versionsWindmill v1.309.0 – v1.603.2; Nextcloud Flow v1.0.0 – v1.2.2
Fixed versionsWindmill 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 date2026-03-06 (GHSA); patch predates disclosure (January 2026)
Reporter*Valentin Lobstein* (Chocapikk), independent researcher — coordinated disclosure with Windmill Labs
04 · The Call

noisgate verdict.

Final Verdict
UPGRADED to CRITICAL (9.8/10)

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.

HIGH Exploitability and chain reliability (public PoC + in-the-wild reports)
HIGH Vendor severity mismatch (vendor rated info-disclosure only)
MEDIUM Total exposed population estimate (VulnCheck cites 170; Flow-behind-proxy invisibility means true number is higher)

Why this verdict

  • Vendor undersizing: The 7.5 score reflects only C:H/I:N/A:N. The publicly documented chain reaches I:H/A:H via 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/passwd and /proc/1/environ in 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.sock mount: 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.

05 · Compensating Control

What to do — in priority order.

  1. Upgrade Windmill to ≥ v1.603.3 (Flow to ≥ v1.3.0) — The only durable fix. Sanitizes filename in get_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.
  2. Block /api/w/*/jobs_u/get_log_file/* at the reverse proxy immediately — Add an Nginx/Traefik/Envoy location block 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.
  3. 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.
  4. Remove /var/run/docker.sock mount from worker containers — Edit docker-compose.yml to drop the - /var/run/docker.sock:/var/run/docker.sock line unless a specific workflow requires Docker-in-Docker. Breaks the container-escape step of the chain. Deploy within 3 days.
  5. 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.
  6. Audit /tmp/windmill/logs/ access logs for .. or %2e%2e or %252e — Grep the reverse-proxy access logs from 2026-01-01 forward for path-traversal indicators against jobs_u/get_log_file. Any hit = assume breach and pivot to IR. Do this today.
What doesn't work
  • Windmill's own RBAC / workspace permissions — the jobs_u route 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 ..%25252f Flow 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.
06 · Verification

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.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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
07 · Bottom Line

If you remember one thing.

TL;DR
Treat this as a live incident, not a patch ticket. This is unauthenticated RCE-on-the-wire against a code-execution platform, with a public weaponized chain and VulnCheck-confirmed exploitation as of July 2026 — the vendor's 7.5 label is a paperwork artifact, ignore it. Monday morning: (1) push a reverse-proxy 403 for /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

  1. VulnCheck / Hacker News — active exploitation report
  2. Chocapikk — Windfall technical writeup (exploit chain)
  3. Chocapikk/Windfall — public PoC repository
  4. SentinelOne vulnerability database entry
  5. OpenCVE — CVE-2026-29059 details
  6. GBHackers coverage — RCE PoC published
  7. OffSeq Threat Radar — CWE-22 classification
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.