Skip to content

fix: replace IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist#8884

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

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

Conversation

@sriramveeraghanta

@sriramveeraghanta sriramveeraghanta commented Apr 12, 2026

Copy link
Copy Markdown
Member

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

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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)})

Copilot AI Apr 12, 2026

Copy link

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


Copilot AI Apr 12, 2026

Copy link

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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
eyriehq-admin added a commit to eyriehq/plane-plus that referenced this pull request May 19, 2026
* [WEB-6784] feat scrollbar in shortcuts modal (makeplane#8872)

* fix: update border for project timezone

* feat: added scrollbar in keyboard shortcuts modal

* fix: remove unnecessary changes

* fix: remove redundant overflow

* [WEB-6785] fix: update border for project timezone (makeplane#8870)

* chore: remove Intercom integration and chat support components (makeplane#8875)

Intercom is no longer used. This removes all related frontend components,
hooks, custom events, API config, types, and i18n keys.

* chore: update dependencies (Django, cryptography, axios, lodash) (makeplane#8880)

* chore: update dependencies (Django, cryptography, axios, lodash)

- Django 4.2.29 → 4.2.30
- cryptography 46.0.6 → 46.0.7
- axios 1.13.5 → 1.15.0
- lodash 4.17.23 → 4.18.0

* chore: update lodash from 4.18.0 to 4.18.1

* [WEB-6840] feat: skip role & use-case steps for self-hosted instances (makeplane#8890)

* chore(deps): bump pytest (makeplane#8891)

Bumps the pip group with 1 update in the /apps/api/requirements directory: [pytest](https://github.com/pytest-dev/pytest).


Updates `pytest` from 9.0.2 to 9.0.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](pytest-dev/pytest@9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* enhance sub-issue query performance with optimized annotations and subqueries (makeplane#8889)

* fix: enforce workspace membership on V2 asset endpoints (makeplane#8885)

WorkspaceFileAssetEndpoint had no authorization checks beyond
authentication, allowing any logged-in user to create, read, patch,
and delete assets in any workspace by slug. DuplicateAssetEndpoint
only authorized the destination workspace, letting users copy assets
from workspaces they don't belong to.

Add @allow_permission decorators to all WorkspaceFileAssetEndpoint
methods and scope DuplicateAssetEndpoint's source asset lookup to
workspaces where the caller is an active member.

Ref: GHSA-qw87-v5w3-6vxx

* fix: replace IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist (makeplane#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

* [SILO-1158] chore: add context for project in relations API (makeplane#8860)

* add context for project in relations API

* modify issue relation serializer

* fix: sanitize filenames in upload paths to prevent path traversal (makeplane#8879)

* fix: sanitize filenames in upload paths to prevent path traversal (GHSA-v57h-5999-w7xp)

Add server-side filename sanitization across all file upload endpoints
to prevent path traversal sequences (../) in user-supplied filenames
from being incorporated into S3 object keys. While S3 keys are flat
strings and not vulnerable to filesystem traversal, this adds
defense-in-depth and prevents S3 key pollution.

Changes:
- Add sanitize_filename() utility in path_validator.py
- Sanitize filenames in get_upload_path() for FileAsset and IssueAttachment models
- Sanitize name parameter in all upload view endpoints

* fix: address PR review feedback on filename sanitization

- Remove unused `import re`
- Normalize backslashes to forward slashes before os.path.basename()
  so Windows-style paths (e.g. ..\..\..\evil.txt) are handled on POSIX
- Strip whitespace before removing leading dots so " .env" is caught
- Return None instead of "unnamed" for empty input so existing
  `if not name` validation guards remain effective
- Add `or "unnamed"` fallback at call sites that lack a name guard

* fix: use random hex name as fallback in get_upload_path instead of "unnamed"

* fix: resolve ruff E501 line too long in DuplicateAssetEndpoint

* chore(ci): suppress CodeQL file coverage deprecation warning (makeplane#8916)

* chore(ci): suppress CodeQL file coverage deprecation warning

Explicitly opt into the new default behavior where CodeQL skips
computing file coverage information on pull requests for improved
analysis performance.

* Update .github/workflows/codeql.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: update CODEOWNERS for apps and deployments (makeplane#8919)

* chore: update CODEOWNERS for apps and deployments

Assign owners per app/area so reviews are routed to the right
maintainers.

* chore: update the codeowners

* chore: add Claude Code skills for PR descriptions and release notes (makeplane#8920)

* chore: add Claude Code skills for PR descriptions and release notes

* chore(skills): update release-notes branches to canary->master and example version to v1.3.0

* chore(skills): address PR review comments

- pr-description: infer base branch from PR metadata, fix Improvement wording, reference template's screenshot placeholder verbatim
- release-notes: add `text` language to unlabeled fenced code block

* chore: bump up the package version

* chore(deps): bump lxml (makeplane#8925)

Bumps the pip group with 1 update in the /apps/api/requirements directory: [lxml](https://github.com/lxml/lxml).


Updates `lxml` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](lxml/lxml@lxml-6.0.0...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump axios, uuid and add security overrides (makeplane#8930)

* chore(deps): bump axios, uuid and add security overrides

Bump axios 1.15.0 → 1.15.2 and uuid 13.0.0 → 14.0.0 in the catalog,
and add pnpm overrides pinning postcss >=8.5.10, follow-redirects
>=1.16.0, and routing axios/uuid through the catalog.

* fix: overrides

* fix: add WEBHOOK_ALLOWED_HOSTS allowlist for internal webhook targets (makeplane#9078)

* fix: add WEBHOOK_ALLOWED_HOSTS allowlist for internal webhook targets

The IP-based allowlist alone isn't practical for containerised deployments
where service IPs are dynamic. Adds a hostname-based bypass for trusted
internal services (e.g. Silo via docker-compose / k8s service DNS) and
makes the previously hardcoded ["plane.so"] domain blocklist configurable
via WEBHOOK_DISALLOWED_DOMAINS.

- validate_url accepts allowed_hosts (exact, case-insensitive match;
  skips DNS lookup for trusted names)
- WebhookSerializer wires both settings through and lets allowlisted
  hosts bypass the disallowed-domain check
- Exposes WEBHOOK_ALLOWED_HOSTS in aio/cli deployment env files

* fix: default WEBHOOK_DISALLOWED_DOMAINS to empty for self-hosted

* fix: pass WEBHOOK_ALLOWED_HOSTS to send-time webhook re-validation

* fix: pnpm path for Docker builds (makeplane#9079)

Add $PNPM_HOME/bin to PATH so corepack-installed pnpm binaries are
resolvable during Docker builds.

* fix(brand): replace upstream Plane logos and copy with Plane Plus in onboarding screens

- not-ready-view.tsx: swap gradient-logo.webp (3D Plane diamond) → EyrieHQ icon;
  "Welcome to Plane" → "Welcome to Plane Plus"
- tour/root.tsx: "Welcome to Plane, {name}" → "Welcome to Plane Plus, {name}";
  copy updated to match
- admin/instance-not-ready.tsx: drop PlaneTakeOffImage → EyrieHQ icon;
  "Welcome aboard Plane!" → "Welcome aboard Plane Plus!"
- admin/new-user-popup.tsx: drop TakeoffIcon SVGs + unused theme imports → EyrieHQ icon;
  "Welcome to Plane instance portal" → "Welcome to Plane Plus"

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: b-saikrishnakanth <130811169+b-saikrishnakanth@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Phạm Nguyên Phương <69796528+PhuongPN6689@users.noreply.github.com>
Co-authored-by: Saurabh Kumar <70131915+Saurabhkmr98@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: EyrieHQ <eyriehq@eyriehq.com>
Co-authored-by: Surya <surya@eyriehq.com>
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