* 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: 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
* 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
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
When patching instance configuration values, the raw values from
request.data were used directly without sanitization. This adds:
- Whitespace stripping via str().strip() to prevent leading/trailing
spaces from being stored
- Explicit None handling so that null values become empty strings
instead of the literal string "None"
* fix: prevent ORM field injection via segment parameter in analytics (GHSA-93x3-ghh7-72j3)
Centralize analytics field allowlists into VALID_ANALYTICS_FIELDS and
VALID_YAXIS constants in analytics_plot.py. Add defense-in-depth
validation in build_graph_plot() and extract_axis() so no caller can
pass arbitrary field references to Django F() expressions. Add missing
segment validation to SavedAnalyticEndpoint. Also fixes ExportAnalytics
using "estimate_point" instead of "estimate_point__value".
* fix: address PR review - remove unused imports and validate stored query params
Remove unused VALID_ANALYTICS_FIELDS and VALID_YAXIS imports from
analytic_plot_export.py. Add x_axis/y_axis allowlist validation in
SavedAnalyticEndpoint for stored query_dict values to prevent 500
errors from malformed saved analytics.
* fix: validate redirects in favicon fetching to prevent SSRF
The previous SSRF fix (GHSA-jcc6-f9v6-f7jw) only validated redirects for
the main page URL but not for the favicon fetch path. An attacker could
craft an HTML page with a favicon link that redirects to a private IP,
bypassing the IP validation and leaking internal network data as base64.
Extract a reusable `safe_get()` function that validates every redirect hop
against private/internal IPs and use it for both page and favicon fetches.
Resolves: GHSA-9fr2-pprw-pp9j
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback for SSRF favicon fix
- Fix off-by-one in redirect limit: only raise RuntimeError when the
response is still a redirect after MAX_REDIRECTS hops, not when the
final response is a successful 200
- Return final URL from safe_get() so favicon href resolution uses the
correct origin after redirects instead of the original URL
- Add unit tests for validate_url_ip and safe_get covering private IP
blocking, redirect-following, and redirect limit enforcement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restrict role modification in ProjectMemberViewSet.partial_update to
Admins only and enforce that requesters cannot modify or assign roles
equal to or higher than their own. Previously, Guests could demote
Admins by exploiting a missing lower-bound check on role changes.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bulk update date endpoint fetched issues by ID without filtering
by workspace or project, allowing any authenticated project member to
modify start_date and target_date of issues in any workspace/project
across the entire instance (IDOR - CWE-639).
Scoped the query to include workspace__slug and project_id filters,
consistent with other issue endpoints in the codebase.
Ref: GHSA-4q54-h4x9-m329
* chore(deps): replace dotenvx with dotenv and update dependency overrides
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: sort devDependencies in package.json files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#6746
API-driven issue updates (PUT update, PUT create-via-upsert, PATCH) were
missing `model_activity.delay()` calls, so webhooks were never dispatched
for changes made through the API. The web UI paths already include these
calls (e.g. in `post()` at L475), but the `put()` and `partial_update()`
methods only called `issue_activity.delay()`.
This adds `model_activity.delay()` immediately after each existing
`issue_activity.delay()` in these three code paths, using the same
signature as the existing call in `post()`.
Tested on Plane CE v1.2.1 self-hosted: API PATCH triggers
`webhook_send_task` in the Celery worker, confirming webhook delivery.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>