Skip to content

fix: replace IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist#8884

Merged
sriramveeraghanta merged 2 commits intopreviewfrom
fix/webhook-allowed-ips-allowlist
Apr 20, 2026
Merged

fix: replace IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist#8884
sriramveeraghanta merged 2 commits intopreviewfrom
fix/webhook-allowed-ips-allowlist

Conversation

@sriramveeraghanta
Copy link
Copy Markdown
Member

@sriramveeraghanta sriramveeraghanta commented Apr 12, 2026

Summary

  • Replaces the IS_SELF_MANAGED flag-based private IP toggle with an explicit WEBHOOK_ALLOWED_IPS env variable (comma-separated IPs/CIDRs)
  • All private/internal IPs are now blocked by default for webhook URLs, regardless of deployment type
  • Self-managed deployments can allowlist specific networks (e.g. 10.0.0.0/8,192.168.1.0/24) instead of blanket-allowing all private IPs
  • Adds a shared validate_url function in ip_address.py and deduplicates inline SSRF checks in the webhook serializer
  • Adds unit tests for the new allowlist behavior

Test plan

  • Verify default behavior blocks private IPs (no env set → empty allowlist)
  • Verify self-managed deployments can set WEBHOOK_ALLOWED_IPS=10.0.0.0/8 to allow internal webhook targets
  • Verify IPs outside the allowlist are still blocked
  • Verify existing unit tests pass

Summary by CodeRabbit

  • New Features

    • Configurable IP allowlisting for webhooks via an environment setting (supports IPs and CIDR ranges).
  • Improvements

    • Centralized webhook URL validation with stricter DNS/IP resolution checks and normalized domain-blocking rules.
    • Added an extra validation step immediately before sending outbound webhooks.
  • Tests

    • Added unit tests covering allowlist and DNS/IP validation scenarios.

… allowlist

Instead of blanket-allowing all private IPs on self-managed deployments,
webhook URL validation now blocks all private/internal IPs by default and
only permits specific networks listed in the WEBHOOK_ALLOWED_IPS env
variable (comma-separated IPs/CIDRs).
Copilot AI review requested due to automatic review settings April 12, 2026 18:29
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Centralizes webhook URL security by adding a shared validate_url() utility and a serializer _validate_webhook_url() wrapper, parsing WEBHOOK_ALLOWED_IPS from settings, re-validating URLs at send time, and adding tests for allowlist semantics.

Changes

Cohort / File(s) Summary
Validation Utility
apps/api/plane/utils/ip_address.py
Added validate_url(url, allowed_ips=None) to enforce http(s), resolve DNS, and block private/loopback/reserved/link-local IPs unless covered by allowlist networks.
Settings / Config
apps/api/plane/settings/common.py
Added WEBHOOK_ALLOWED_IPS parsing from env (comma-separated IPs/CIDRs) with logging for invalid entries; exports list of ip_network objects.
Serializer
apps/api/plane/app/serializers/webhook.py
Replaced duplicated inline SSRF/IP checks with WebhookSerializer._validate_webhook_url(url) that delegates to validate_url(...) and normalizes domain checks against plane.so and request host.
Background Task
apps/api/plane/bgtasks/webhook_task.py
Added a pre-request re-validation call to validate_url(...) using settings.WEBHOOK_ALLOWED_IPS before issuing the outbound requests.post(...).
Tests
apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
Added TestValidateUrlAllowlist suite mocking DNS to exercise allowlist behavior (private CIDR allow, deny when out-of-range, loopback allowlist).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WebhookSerializer
  participant validate_url
  participant DNS as DNS_Resolver
  participant DB as Webhook_DB
  participant Task as Webhook_Task
  participant HTTP as External_Endpoint

  Client->>WebhookSerializer: create/update webhook (url)
  WebhookSerializer->>validate_url: _validate_webhook_url(url, allowed_ips)
  validate_url->>DNS_Resolver: resolve hostname
  DNS_Resolver-->>validate_url: IP(s)
  validate_url-->>WebhookSerializer: ok / raise ValidationError
  WebhookSerializer->>Webhook_DB: save Webhook
  Webhook_DB-->>Task: enqueue/send event
  Task->>validate_url: validate_url(url, allowed_ips) (re-check before send)
  validate_url->>DNS_Resolver: resolve hostname
  validate_url-->>Task: ok / raise ValueError (abort)
  Task->>HTTP: requests.post(url, payload)
  HTTP-->>Task: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hop through DNS and allowlist rows,
I check each IP where the webhook goes,
From settings parsed to tasks that send,
Safe hops ensured from end to end —
A rabbit's guard, so cables pose.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: replacing IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist, which directly corresponds to the core objective of the PR.
Description check ✅ Passed The PR description covers the main objectives, test scenarios, and changes, but lacks a formal Type of Change selection and doesn't fully match the provided template structure with all required sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/webhook-allowed-ips-allowlist

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/api/plane/app/serializers/webhook.py Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens SSRF protections for webhook endpoints by blocking private/internal IP resolution by default and introducing an explicit WEBHOOK_ALLOWED_IPS allowlist for self-managed deployments. It centralizes URL-to-IP validation in plane.utils.ip_address.validate_url and updates the webhook serializer to reuse it, with unit tests covering the allowlist behavior.

Changes:

  • Add shared validate_url utility to block private/internal IP targets unless explicitly allowlisted.
  • Introduce WEBHOOK_ALLOWED_IPS setting parsed from an env var into ip_network entries.
  • Refactor webhook serializer SSRF checks to use the shared validator and add allowlist unit tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
apps/api/plane/utils/ip_address.py Adds shared validate_url SSRF/allowlist validator used by webhook URL validation.
apps/api/plane/settings/common.py Parses WEBHOOK_ALLOWED_IPS from env into a network allowlist for SSRF exceptions.
apps/api/plane/app/serializers/webhook.py Deduplicates SSRF checks by calling validate_url and normalizes domain comparisons.
apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py Adds unit tests verifying allowlist behavior for private/loopback addresses.

Comment thread apps/api/plane/utils/ip_address.py Outdated
Comment on lines +24 to 30
def _validate_webhook_url(self, url):
"""Validate a webhook URL against SSRF and disallowed domain rules."""
try:
ip_addresses = socket.getaddrinfo(hostname, None)
except socket.gaierror:
raise serializers.ValidationError({"url": "Hostname could not be resolved."})

if not ip_addresses:
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
validate_url(url, allowed_ips=settings.WEBHOOK_ALLOWED_IPS)
except ValueError as e:
raise serializers.ValidationError({"url": str(e)})

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook URL is validated only at create/update time, but webhook_send_task later posts to webhook.url without re-validating the resolved IP and (by default) will follow redirects. This leaves a DNS-rebinding/redirect path where a URL that was safe when saved can resolve/redirect to a private/internal IP at send time. Consider validating the target immediately before each send (using the same validate_url(..., allowed_ips=settings.WEBHOOK_ALLOWED_IPS) logic) and disabling redirects or validating each redirect hop.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +72


Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new allowlist tests don’t cover the mixed IPv4/IPv6 allowlist case (e.g., allowed_ips contains an IPv6 CIDR while the hostname resolves to an IPv4 address). Today this can surface as a TypeError during membership checks. Add a unit test that passes mixed-version networks and asserts validation still works (and blocks/permits correctly) without crashing.

Suggested change
def test_allowlist_permits_matching_ipv4_with_mixed_version_networks(self):
allowed = [
ipaddress.ip_network("2001:db8::/32"),
ipaddress.ip_network("192.168.1.0/24"),
]
with patch("plane.utils.ip_address.socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [(None, None, None, None, ("192.168.1.50", 0))]
validate_url("http://example.com", allowed_ips=allowed) # Should not raise
def test_allowlist_blocks_non_matching_ipv4_with_mixed_version_networks(self):
allowed = [
ipaddress.ip_network("2001:db8::/32"),
ipaddress.ip_network("192.168.1.0/24"),
]
with patch("plane.utils.ip_address.socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [(None, None, None, None, ("10.0.0.1", 0))]
with pytest.raises(ValueError, match="private/internal"):
validate_url("http://example.com", allowed_ips=allowed)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/app/serializers/webhook.py`:
- Around line 28-29: The except block currently re-raises
serializers.ValidationError with str(e) which can leak internals; instead log
the original exception server-side (e.g., using logger.exception or an error
reporting client) and raise a fixed, client-safe message like
serializers.ValidationError({"url": "Invalid URL"}) or similar user-facing text;
update the except ValueError as e handler (the block that currently raises
serializers.ValidationError({"url": str(e)})) to perform logging of e and return
the sanitized ValidationError to the client.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ab8cf65-b83d-4364-919f-f60c6bad2fed

📥 Commits

Reviewing files that changed from the base of the PR and between 39325d2 and be3b038.

📒 Files selected for processing (4)
  • apps/api/plane/app/serializers/webhook.py
  • apps/api/plane/settings/common.py
  • apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
  • apps/api/plane/utils/ip_address.py

Comment thread apps/api/plane/app/serializers/webhook.py Outdated
- Sanitize error messages to avoid leaking internal details to clients
- Guard against TypeError with mixed IPv4/IPv6 allowlist networks
- Re-validate webhook URL at send time to prevent DNS-rebinding
- Add unit tests for mixed-version IP network allowlists
@sriramveeraghanta sriramveeraghanta merged commit a8a16c8 into preview Apr 20, 2026
11 of 12 checks passed
@sriramveeraghanta sriramveeraghanta deleted the fix/webhook-allowed-ips-allowlist branch April 20, 2026 09:58
PhilippeCaira pushed a commit to PhilippeCaira/plane that referenced this pull request Apr 22, 2026
…plane#8884)

* fix: replace IS_SELF_MANAGED toggle with explicit WEBHOOK_ALLOWED_IPS allowlist

Instead of blanket-allowing all private IPs on self-managed deployments,
webhook URL validation now blocks all private/internal IPs by default and
only permits specific networks listed in the WEBHOOK_ALLOWED_IPS env
variable (comma-separated IPs/CIDRs).

* fix: address PR review comments for webhook SSRF protection

- Sanitize error messages to avoid leaking internal details to clients
- Guard against TypeError with mixed IPv4/IPv6 allowlist networks
- Re-validate webhook URL at send time to prevent DNS-rebinding
- Add unit tests for mixed-version IP network allowlists
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants