makeplane/plane · warning · ValidationError
Local URLs are not allowed.
Error message
Local URLs are not allowed.
What it means
Django ValidationError raised by `validate_domain` (webhook.py:25) when the parsed URL's netloc is exactly `localhost` or `127.0.0.1`. The check is intentionally narrow - it only blocks those two literals, so other loopback/private addresses (e.g. `0.0.0.0`, `::1`, `127.0.0.2`, `169.254.169.254`, internal RFC1918 IPs) are NOT blocked, which is a known SSRF gap.
Source
Thrown at apps/api/plane/db/models/webhook.py:31
# Module imports
from plane.db.models import BaseModel, ProjectBaseModel
def generate_token():
return "plane_wh_" + uuid4().hex
def validate_schema(value):
parsed_url = urlparse(value)
if parsed_url.scheme not in ["http", "https"]:
raise ValidationError("Invalid schema. Only HTTP and HTTPS are allowed.")
def validate_domain(value):
parsed_url = urlparse(value)
domain = parsed_url.netloc
if domain in ["localhost", "127.0.0.1"]:
raise ValidationError("Local URLs are not allowed.")
class Webhook(BaseModel):
workspace = models.ForeignKey("db.Workspace", on_delete=models.CASCADE, related_name="workspace_webhooks")
url = models.URLField(validators=[validate_schema, validate_domain], max_length=1024)
is_active = models.BooleanField(default=True)
secret_key = models.CharField(max_length=255, default=generate_token)
project = models.BooleanField(default=False)
issue = models.BooleanField(default=False)
module = models.BooleanField(default=False)
cycle = models.BooleanField(default=False)
issue_comment = models.BooleanField(default=False)
is_internal = models.BooleanField(default=False)
version = models.CharField(default="v1", max_length=50)
def __str__(self):
return f"{self.workspace.slug} {self.url}"
View on GitHub (pinned to 1c8a60f858)
Solutions
- Use a non-localhost hostname or a public DNS name for the webhook target.
- If developing locally, expose the receiver via a tunnel (e.g. a public ngrok/cloudflare-tunnel URL) so the netloc is not localhost/127.0.0.1.
- To actually prevent SSRF, harden `validate_domain` to also reject 0.0.0.0, ::1, 127.0.0.0/8, 169.254.169.254, and RFC1918 ranges - the current allow-list is incomplete.
- Resolve the host and reject if it points to a private/loopback IP.
Example fix
# before - blocked
def validate_domain(value):
domain = urlparse(value).netloc
if domain in ["localhost", "127.0.0.1"]:
raise ValidationError("Local URLs are not allowed.")
# after - broader SSRF guard
import ipaddress, socket
def validate_domain(value):
domain = urlparse(value).netloc.split(":")[0]
try:
ip = ipaddress.ip_address(domain)
except ValueError:
ip = ipaddress.ip_address(socket.gethostbyname(domain))
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValidationError("Local/private URLs are not allowed.") Defensive patterns
Strategy: validation
Validate before calling
from urllib.parse import urlparse
def is_blocked_local_url(url: str) -> bool:
# mirrors the NARROW current check in webhook.py:27
# NOTE: only these two literals are blocked; 0.0.0.0, ::1, private IPs are NOT
return urlparse(url).netloc in ('localhost', '127.0.0.1')
# stronger guard for callers that need real SSRF protection:
import ipaddress, socket
def is_local_or_private(url: str) -> bool:
host = urlparse(url).netloc.split(':')[0]
try:
ip = ipaddress.ip_address(host)
except ValueError:
ip = ipaddress.ip_address(socket.gethostbyname(host))
return ip.is_loopback or ip.is_private or ip.is_link_local Try / catch
from django.core.exceptions import ValidationError
try:
webhook.full_clean()
except ValidationError as e:
if 'Local URLs' in str(e):
use_public_tunnel() # e.g. ngrok Prevention
- Use a public hostname for webhook targets.
- Do NOT rely on validate_domain alone for SSRF defense - harden it to block all loopback/private/link-local IPs.
- Resolve and re-check the IP after DNS lookup to catch DNS rebinding.
When it happens
Trigger: Creating/updating a Webhook whose URL's host is literally `localhost` or `127.0.0.1` (e.g. `http://localhost:9000/hook` or `http://127.0.0.1/hook`). Any other host string passes, including `0.0.0.0`, `[::1]`, `localhost.localdomain`, or any private IP.
Common situations: Local development against a webhook receiver on the same machine; security testing that expects full SSRF protection but finds only two hosts blocked; CI environments where the callback host resolves to localhost under a different name.
Related errors
- Invalid schema. Only HTTP and HTTPS are allowed.
- Missing required fields.
- Invalid file type. Please select an image.
- Invalid expression: empty or null data
- AND group must contain at least one condition
AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12).
Data as JSON: /api/errors/10b77207b1c79ff1.
Report an issue: GitHub.