fix: add WEBHOOK_ALLOWED_HOSTS allowlist for internal webhook targets#9078
Conversation
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
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
Ports an enterprise fix to community, adding a WEBHOOK_ALLOWED_HOSTS hostname allowlist (and replacing the previously hardcoded plane.so block with a configurable WEBHOOK_DISALLOWED_DOMAINS) so self-managed deployments can register webhooks against internal service hostnames (docker-compose / k8s service DNS) that resolve to dynamic private IPs.
Changes:
- Adds
allowed_hostsparameter tovalidate_url(exact, case-insensitive match; skips DNS lookup so unresolvable internal names work). - Adds
WEBHOOK_ALLOWED_HOSTSandWEBHOOK_DISALLOWED_DOMAINSDjango settings and wires them throughWebhookSerializer(allowlisted hosts also bypass the disallowed-domain check). - Exposes
WEBHOOK_ALLOWED_HOSTS(andWEBHOOK_ALLOWED_IPS) in aio/cli env files and the cli docker-composex-app-env, plus unit tests for the new allowlist behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| apps/api/plane/utils/ip_address.py | Adds allowed_hosts bypass before DNS lookup. |
| apps/api/plane/settings/common.py | Parses WEBHOOK_ALLOWED_HOSTS / WEBHOOK_DISALLOWED_DOMAINS env vars. |
| apps/api/plane/app/serializers/webhook.py | Passes allowed_hosts to validate_url; uses configurable disallowed domains; bypasses domain check for allowlisted hosts. |
| apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py | Adds tests covering allowed-host bypass, case-insensitivity, DNS skip, exact-match enforcement, and empty allowlist. |
| deployments/cli/community/variables.env | Documents and exposes WEBHOOK_ALLOWED_IPS / WEBHOOK_ALLOWED_HOSTS. |
| deployments/cli/community/docker-compose.yml | Wires the two webhook env vars into x-app-env. |
| deployments/aio/community/variables.env | Documents and exposes the two webhook env vars for the aio image. |
| validate_url( | ||
| url, | ||
| allowed_ips=settings.WEBHOOK_ALLOWED_IPS, | ||
| allowed_hosts=settings.WEBHOOK_ALLOWED_HOSTS, | ||
| ) |
There was a problem hiding this comment.
Good catch — fixed in aadc28a. The send-time re-validation in webhook_task.py now also passes allowed_hosts=settings.WEBHOOK_ALLOWED_HOSTS so allowlisted hostnames survive both creation and send.
Summary
Ports webhook allowlist changes from the enterprise repo to community. PR #8884 dropped the implicit private-IP allow on self-managed deployments, which breaks integrations that register webhooks against internal service hostnames (docker-compose / k8s service DNS resolves to a private IP and is now rejected). Container service IPs are dynamic, so a CIDR allowlist isn't a practical fix.
WEBHOOK_ALLOWED_HOSTSenv var (comma-separated hostnames) that bypasses the IP-based SSRF check atvalidate_url. Matching is exact and case-insensitive — subdomains of an allowed host stay blocked. Bypassed hostnames skip the DNS lookup too, so allowlisting a name that isn't resolvable from the API container still works.WEBHOOK_DISALLOWED_DOMAINSenv var (empty by default for self-hosted) replacing the previously hardcoded["plane.so"]list. Allowlisted hosts also bypass this check so legitimate sibling services that share a parent domain with Plane can still receive webhooks.WebhookSerializer.WEBHOOK_ALLOWED_HOSTSin aio/cli deployment env files and the cli docker-composex-app-env.Operator notes
Self-managed deployments that register integration webhooks against an internal service hostname must add it to the env:
Test plan
WEBHOOK_ALLOWED_HOSTS=<host>→ webhook withhttp://<host>:3000/...is accepted (was 400 after fix: replace IS_SELF_MANAGED with WEBHOOK_ALLOWED_IPS allowlist #8884).WEBHOOK_ALLOWED_HOSTS→ private-IP targets still rejected.http://attacker.<host>.internal/) is still rejected.pytest apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py -k TestValidateUrlAllowlist.