A self-hosted Heroku clone forgot to check whose tenant you're in before handing over the server list
Coolify is an open-source PaaS that manages servers, applications, databases, and deployments — think Heroku/Vercel you run yourself. In versions prior to 4.0.0-beta.471, the server and project lookup endpoints fail to scope queries to the requesting user's team_id. Any authenticated user with a valid session can substitute another team's server ID or project ID into the URL and read that team's resources back. CWE-639 — classic Authorization Bypass Through User-Controlled Key (IDOR). CVSS vector AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N reflects confidentiality-only impact but with scope change, because the data crossing the boundary belongs to a different security principal (another team).
Vendor severity HIGH (7.7) is fair *for the multi-tenant deployments that are actually affected*. The catch: most Coolify installs in the wild are single-team (a solo dev or small shop running their own apps), so the population at risk is the subset of Coolify operators who invite users from outside their trust boundary — agencies hosting client environments, internal platform teams hosting multiple product squads, etc. For those operators, the leak isn't theoretical: project objects in Coolify carry environment variables, repo URLs, and references to server connection profiles that hold SSH credentials. That's why we keep this at HIGH despite the auth requirement.
4 steps from start to impact.
Obtain any authenticated session on the target Coolify instance
- Coolify instance reachable on the network the attacker is on
- Open registration OR existing low-priv account in any team
- Most production Coolify instances are behind VPN or basic auth proxy
- Self-hosted PaaS rarely has open registration — would-be tenant must be invited
Enumerate other teams' resource IDs
/servers/{id}) and projects (/projects/{uuid}). Attacker scripts a sequential walk (for integer IDs) or harvests UUIDs from prior responses, leaked references, or shared CI runs. Standard curl + jq loop; no custom tooling.- Knowledge of the API surface (documented at coolify.io/docs)
- ID format is enumerable or guessable
- UUIDs reduce blind enumeration to oracle-style attacks
- Rate limiting at the reverse proxy slows mass walks
/servers/* and /projects/* from a single session is the telltale; default Coolify telemetry does NOT alert on this.Read cross-team server and project objects via direct ID reference
Server::find($id) / Project::find($uuid) without the whereHas('team', currentTeam()) scope. Authenticated request to another team's resource returns 200 with full object — including server hostname, IP, connection settings, project name, repo URL, and references to associated environment variables and deployments.- Target IDs from step 2
- Bug present (i.e., target < 4.0.0-beta.471)
- The advisory specifies *server and project lookups* — secret/env-var endpoints may be separately scoped. Confirm what your version actually leaks before treating as 'full env-var exposure'.
Pivot from leaked metadata to deployment compromise
- Useful intel returned in step 3
- Reachable target server or upstream repo
- MFA on Git provider
- SSH key-based auth on managed servers (default Coolify config) prevents direct hostname-only pivot
The supporting signals.
| In-the-wild exploitation | None reported as of 2026-06-30. No GreyNoise tags, no campaigns, no incident reports. |
|---|---|
| Public PoC | GitHub Security Advisory GHSA (coollabsio/coolify) describes the flaw; no weaponized PoC repo observed but the bug is trivially reproducible with curl once you have any session. |
| EPSS | Not yet scored (CVE published 2026-06-29). Expect <1% within 30 days given auth requirement and lack of mass exposure. |
| KEV | Not listed. Unlikely to be added — confidentiality-only, auth-gated, narrow population. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N → 7.7 HIGH. Scope change reflects cross-tenant boundary. No I or A impact in the CVE itself. |
| Affected versions | All Coolify releases prior to 4.0.0-beta.471. |
| Fixed version | 4.0.0-beta.471 and later. No distro backports — Coolify ships as Docker images via coollabsio/coolify. |
| Exposure data | Shodan/Censys queries for coolify favicon hash typically return ~2,000–5,000 public Coolify dashboards globally. Of these, the subset that are multi-team is the real attack surface — likely a small minority. |
| Disclosed | 2026-06-29 via coollabsio GitHub Security Advisory. |
| Reporter | Coordinated disclosure via Coolify maintainers (coollabsio). Researcher attribution per the GHSA advisory. |
noisgate verdict.
Reassessed HIGH because the single decisive factor is the role multiplier: the affected component is an orchestration plane that brokers SSH keys, environment variables, and Git credentials — leaking another team's project metadata is a credible step toward fleet compromise on multi-tenant installs. The auth requirement (PR:L) and confidentiality-only impact prevent escalation to CRITICAL.
Why this verdict
- Auth-gated (PR:L): the attacker must already hold a valid session on the target instance. That eliminates the drive-by-internet population.
- Single-tenant installs are unaffected in practice: if your Coolify only has one team, there is no other team to enumerate. The bulk of self-hosted Coolify deployments fall here.
- Role multiplier — orchestration platform: Coolify is a deployment broker holding SSH connection profiles, env vars, and Git creds. Cross-tenant read on
ServerandProjectobjects can hand an attacker enough metadata to pivot to managed hosts. This is the reason we hold at HIGH instead of dropping to MEDIUM. - No I or A impact in the CVE itself: the chain ends at *read* of another team's resources. Modification, deletion, deploy-time RCE, and direct credential theft would require a chained second bug or are scope-limited to what the lookup returns.
- No KEV, no PoC weaponization, no campaign telemetry: purely a hygiene patch on the timeline the SLA dictates.
Why not higher?
Not CRITICAL because PR:L (authentication required), no integrity or availability impact, and the population that is actually multi-tenant is a minority of Coolify deployments. CRITICAL would require unauthenticated reachability or direct RCE/credential-theft impact on the typical install.
Why not lower?
Not MEDIUM because Coolify is canonically a high-value-role component (deployment orchestration with secret material). On the subset of installs that ARE multi-tenant (agencies, internal platform teams), the chain credibly extends from metadata read to managed-server pivot. Generic CWE-639 on a CRUD app would be MEDIUM; on a PaaS it earns the role multiplier.
What to do — in priority order.
- Upgrade to Coolify 4.0.0-beta.471 or later — The only complete fix. Pull the new
coollabsio/coolifyimage, run the documented upgrade flow. Per noisgate remediation SLA, complete within 180 days; for multi-tenant installs treat as ≤ 30 days. - Audit team membership and revoke unused accounts — The bug requires *any* authenticated session. Cull dormant invitees and former contractors today. This is the cheapest compensating control and within the noisgate mitigation SLA of 30 days for HIGH.
- Restrict Coolify dashboard to VPN / SSO-gated reverse proxy — If your instance is reachable on the public internet, front it with Cloudflare Access, Tailscale, or an SSO-aware reverse proxy. Deploy within 30 days to satisfy the mitigation SLA.
- Enable application-layer audit logging for /servers/* and /projects/* — Use the reverse proxy to log requested IDs alongside the session's user_id. Run a daily diff: any access where the project's owning team ≠ the requester's team is the exploit signature. Stand up within the 30-day window.
- Rotate SSH keys and env-var secrets stored in Coolify if multi-tenant — Assume leaked. Rotate before the next billing cycle. This is a remediation step, not a control — but a multi-tenant install that defers rotation is gambling on no prior exploitation having happened.
- WAFs / generic IDS rules: the request is structurally identical to a legitimate one — same path, same method, same headers, valid session cookie. Only the resource ID differs. No signature catches this.
- MFA: doesn't help — the attacker IS authenticated. MFA gates session creation, not authorization scope inside the app.
- Network segmentation alone: if all teams reach the same Coolify URL, segmentation doesn't prevent the IDOR. Per-team Coolify instances would, but that's a re-architecture, not a control.
Crowdsourced verification payload.
Run this from an auditor workstation with shell access to the Coolify host (or anywhere docker can reach the running container). Invoke as ./check_coolify.sh <coolify_container_name_or_id> — example: ./check_coolify.sh coolify. No special privileges beyond being in the docker group.
#!/usr/bin/env bash
# noisgate verification: CVE-2026-34592 — Coolify cross-team IDOR
# Outputs: VULNERABLE / PATCHED / UNKNOWN
set -u
CONTAINER="${1:-coolify}"
FIXED="4.0.0-beta.471"
if ! command -v docker >/dev/null 2>&1; then
echo "UNKNOWN — docker CLI not available"
exit 2
fi
if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then
echo "UNKNOWN — container '$CONTAINER' not found"
exit 2
fi
# Extract version from the running container. Coolify exposes /api/health and writes
# a versions.json; fall back to image tag.
VER="$(docker exec "$CONTAINER" sh -c 'cat /var/www/html/versions.json 2>/dev/null | grep -oE "\"coolify\"\s*:\s*\"[^\"]+\"" | head -1 | sed -E "s/.*:\s*\"([^\"]+)\".*/\\1/"' 2>/dev/null)"
if [ -z "$VER" ]; then
VER="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$CONTAINER" 2>/dev/null)"
fi
if [ -z "$VER" ]; then
VER="$(docker inspect --format '{{.Config.Image}}' "$CONTAINER" | awk -F: '{print $2}')"
fi
if [ -z "$VER" ]; then
echo "UNKNOWN — could not determine Coolify version"
exit 2
fi
# Lexicographic compare works for Coolify's semver-ish 4.0.0-beta.NNN scheme
# Normalize: strip leading 'v'
VER="${VER#v}"
verlte() { [ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" = "$1" ]; }
if verlte "$FIXED" "$VER"; then
echo "PATCHED — Coolify $VER (fixed in $FIXED)"
exit 0
else
echo "VULNERABLE — Coolify $VER < $FIXED — upgrade to $FIXED or later"
exit 1
fi
If you remember one thing.
Sources
- Coolify GHSA — coollabsio/coolify Security Advisories
- Coolify project — coollabsio/coolify GitHub
- CWE-639: Authorization Bypass Through User-Controlled Key
- The Hacker News — Coolify Discloses Critical Flaws
- heise online — Coolify critical vulnerabilities
- wz-it — Coolify CVE Overview 2025/2026
- Coolify documentation
- FIRST CVSS 3.1 specification
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.