Skip to content

SSRF via HTTP redirect in webhook delivery (allow_redirects not set)

Critical
mguptahub published GHSA-mq87-52pf-hm3h Aug 3, 2026

Software

makeplane/plane

Affected versions

<= 1.3.1

Patched versions

1.4.0

Description

Summary

The Celery task that delivers Plane webhooks (apps/api/plane/bgtasks/webhook_task.py, line 337) calls requests.post() without allow_redirects=False and never re-validates the redirect target. The original webhook URL is run through validate_url(), which blocks private / loopback / link-local / reserved addresses — but the URL ultimately fetched after one or more 3xx hops is not checked. Any user who can sign up and create their own workspace (default config: ENABLE_SIGNUP=1, workspace creator is automatically ADMIN) can register a webhook to an attacker-controlled public endpoint that responds with a 302 Location: pointing at an internal address, causing the Plane worker to fetch internal resources — including cloud-metadata endpoints — and persist the response body in webhook_logs, where the same attacker can read it back through the workspace's webhook-logs API.

Details

Vulnerable code — apps/api/plane/bgtasks/webhook_task.py:328-337:

try:
    # Re-validate the webhook URL at send time to prevent DNS-rebinding attacks
    validate_url(
        webhook.url,
        allowed_ips=settings.WEBHOOK_ALLOWED_IPS,
        allowed_hosts=settings.WEBHOOK_ALLOWED_HOSTS,
    )

    # Send the webhook event
    response = requests.post(webhook.url, headers=headers, json=payload, timeout=30)
  • validate_url() (apps/api/plane/utils/ip_address.py) inspects only the original webhook.url.
  • requests.post() is called without allow_redirects=False; the default in the requests library is to follow 3xx redirects.
  • The redirect target is never passed back through validate_url().
  • save_webhook_log() (called at line 340) stores the response body verbatim in the webhook_logs table (or in MongoDB if configured) — see apps/api/plane/db/models/webhook.py (WebhookLog.response_body).

The project already implements the correct pattern elsewhere for the same class of risk: apps/api/plane/bgtasks/work_item_link_task.py::safe_get() uses allow_redirects=False plus per-hop validate_url_ip() with a MAX_REDIRECTS cap. The webhook dispatcher was not given the equivalent treatment.

Authorization model relevant to exploitability (apps/api/plane/app/views/webhook/base.py): all webhook endpoints require ROLE.ADMIN at WORKSPACE level. A freshly registered user becomes a workspace admin automatically by creating their own workspace (apps/api/plane/app/views/workspace/base.py:123-130, WorkspaceMember.objects.create(..., role=20)). With the default ENABLE_SIGNUP=1, any internet user against a default self-hosted instance can reach this code path with zero interaction with existing administrators.

Reading the exfiltrated data back: the same workspace admin can call GET /api/workspaces/<slug>/webhook-logs/<webhook_id>/ and the response includes response_body (WebhookLogSerializer.Meta.fields = "__all__"). The attacker therefore does not need any database or host access — exfiltration is fully API-driven.

PoC

video attached

plane_poc.mp4

Impact

Class: SSRF (CWE-918) bypassing an existing same-class guard via HTTP redirect, with response-body exfiltration into a log readable by the attacker through the application API.

Reachable from the worker on a default deployment:

  • AWS / GCP / OpenStack instance metadata at http://169.254.169.254/… and Azure IMDS — IAM role credentials, instance identity documents.
  • Plane's own internal API (api:8000), Postgres (plane-db:5432), MinIO (plane-minio:9000), RabbitMQ management UI (plane-mq:15672), MongoDB if configured, any other unauthenticated internal admin panel on the worker's network.
  • IPv6 loopback / link-local services (::1, fe80::/10).

Who is impacted:

  • All Plane self-hosted deployments and the Plane-managed cloud, on any version up to and including the affected commit.
  • On AWS/GCP/Azure-hosted instances the attacker obtains IAM credentials for the worker's identity, enabling pivot into the cloud account (read/modify/delete of cloud resources accessible to that role — hence C:H / I:H / A:H on the secondary scope).

Pre-conditions: ability to authenticate to the instance and to create a workspace. On default configuration (ENABLE_SIGNUP=1), this collapses to "any internet user who can reach the Plane URL". No interaction with an existing administrator is required.

Patch: add allow_redirects=False to the requests.post(...) call at webhook_task.py:337 (minimal fix; webhooks should not follow redirects — consistent with GitHub, Stripe, Slack, etc.). If redirect-following is desired, mirror the existing safe_get() pattern from work_item_link_task.py with per-hop validate_url() and MAX_REDIRECTS.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits