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.
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
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.
# 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.

