Skip to main content
Vulnerability Database/CVE-2026-104973

CVE-2026-104973: Plane Project Management SSRF Vulnerability

CVE-2026-104973 is a Server-Side Request Forgery flaw in Plane that allows DNS rebinding attacks to bypass SSRF protections. This article covers the technical details, affected versions, impact, and mitigation.

Published:

CVE-2026-104973 Overview

CVE-2026-104973 is a Server-Side Request Forgery (SSRF) vulnerability in Plane, an open-source project management tool. The flaw exists in versions prior to 1.4.0 and bypasses the earlier fix for CVE-2026-30242. Webhook IP validation runs only at creation time inside apps/api/plane/app/serializers/webhook.py. The delivery worker in apps/api/plane/bgtasks/webhook_task.py performs a second, independent DNS resolution without re-validating the resolved address. Attackers controlling a hostname's DNS records can use DNS rebinding to point the second lookup at internal infrastructure, defeating the SSRF guard. The weakness is categorized under [CWE-918].

Critical Impact

Authenticated attackers can abuse Plane webhooks to reach internal services, cloud metadata endpoints, or other restricted network resources, enabling information disclosure and lateral access to internal infrastructure.

Affected Products

  • Plane (makeplane/plane) versions prior to 1.4.0
  • Component: apps/api/plane/bgtasks/webhook_task.py (delivery task)
  • Component: apps/api/plane/app/serializers/webhook.py (validation path)

Discovery Timeline

  • 2026-10-05 - CVE-2026-104973 published to NVD
  • 2026-10-07 - Last updated in NVD database

Technical Details for CVE-2026-104973

Vulnerability Analysis

Plane validates webhook destinations when a user creates or updates a webhook. The serializer resolves the target hostname and rejects private, loopback, or link-local addresses. However, the asynchronous delivery task resolves the hostname again at send time using the standard HTTP client. This creates a Time-of-Check Time-of-Use (TOCTOU) gap between validation and request issuance.

An attacker who controls DNS for an external hostname can serve a public IP during webhook creation and a private IP during delivery. The delivery worker trusts the second resolution and dispatches the HTTP request to the newly resolved internal address. The result is an authenticated SSRF that reaches resources behind the Plane API server's network boundary, including cloud instance metadata services and internal admin interfaces.

Root Cause

The underlying defect is split validation. IP allow-listing executes only in the WebhookSerializer path, while the Celery delivery task in webhook_task.py performs its own DNS lookup through requests without pinning the resolved address or re-applying the private-range check. Any gap between the two lookups allows DNS rebinding to swap the destination.

Attack Vector

Exploitation requires an authenticated user with permission to create or edit webhooks. The attacker registers a webhook pointing to a domain whose authoritative DNS server returns short-TTL records that alternate between a public IP (to pass validation) and an internal IP such as 169.254.169.254 or 127.0.0.1 (to steer delivery). When Plane triggers webhook delivery, the second resolution returns the internal target and the signed webhook payload is sent there.

The following patch excerpts show how maintainers hardened the request path by routing outbound HTTP through a pinned, redirect-aware fetch helper that re-validates each hop.

python
 import uuid
 from io import BytesIO

-import requests
 from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.core.validators import validate_email
+from plane.utils.url_security import pinned_fetch_following_redirects

 # Django imports
 from django.utils import timezone

Source: GitHub Commit 04622ce1188

python
         serializer = WebhookSerializer(
             webhook,
             data=request.data,
-            context={request: request},
+            context={"request": request},
             partial=True,
             fields=(
                 "id",

Source: GitHub Commit 04622ce1188

Detection Methods for CVE-2026-104973

Indicators of Compromise

  • Outbound HTTP requests from the Plane API or Celery worker hosts to RFC1918 ranges, 127.0.0.0/8, or 169.254.169.254.
  • Webhook configurations whose hostnames resolve to different IP addresses within a short interval or carry unusually low TTLs.
  • Repeated webhook delivery attempts in webhook_task.py logs targeting the same unusual hostname.
  • DNS query logs showing split-horizon responses for the same domain queried by the Plane backend.

Detection Strategies

  • Instrument the webhook delivery worker to log every resolved destination IP alongside the originally validated IP for comparison.
  • Alert on any egress from Plane backend subnets to cloud metadata endpoints or internal service CIDRs.
  • Correlate webhook creation events with subsequent DNS lookups that return private addresses for the same hostname.

Monitoring Recommendations

  • Capture DNS resolver telemetry from Plane application hosts and retain query/answer pairs for retrospective analysis.
  • Enable network flow logging on the subnets hosting the Plane API and background workers.
  • Monitor Plane audit logs for new or modified webhook endpoints created by non-administrative accounts.

How to Mitigate CVE-2026-104973

Immediate Actions Required

  • Upgrade Plane to version 1.4.0 or later, which replaces the raw requests client with pinned_fetch_following_redirects.
  • Audit existing webhook configurations and remove entries pointing to suspicious or externally controlled hostnames.
  • Restrict webhook creation and editing to trusted roles until the patch is deployed.

Patch Information

The fix is delivered in Plane 1.4.0. See GitHub Release v1.4.0, GitHub Pull Request #9163, and the GitHub Security Advisory GHSA-whh3-5g95-4qhc. The patch introduces a shared url_security helper that pins the resolved IP for the actual connection and re-validates addresses across redirects, closing the TOCTOU gap.

Workarounds

  • Block outbound traffic from Plane backend hosts to internal CIDRs, loopback, and cloud metadata IPs at the network layer.
  • Force webhook egress through an authenticated forward proxy that enforces IP allow-listing on each request.
  • Configure the host resolver or an egress firewall to reject responses that resolve public hostnames to private addresses.
bash
# Example egress restriction using iptables on the Plane worker host
iptables -A OUTPUT -m owner --uid-owner plane -d 169.254.169.254 -j REJECT
iptables -A OUTPUT -m owner --uid-owner plane -d 10.0.0.0/8 -j REJECT
iptables -A OUTPUT -m owner --uid-owner plane -d 172.16.0.0/12 -j REJECT
iptables -A OUTPUT -m owner --uid-owner plane -d 192.168.0.0/16 -j REJECT
iptables -A OUTPUT -m owner --uid-owner plane -d 127.0.0.0/8 -j REJECT

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.