mirror of
https://github.com/makeplane/plane.git
synced 2026-07-10 04:25:24 +02:00
* fix(api): harden webhook & link-unfurl SSRF (advisory clusters A/B/C)
Resolves three overlapping SSRF advisory clusters around webhook delivery
and work-item link unfurling:
- Cluster A (private-IP validation + PATCH bypass): the webhook PATCH
handler passed context={request: request} (the request object as the
dict key) so the loopback/disallowed-domain guard silently no-op'd —
now context={"request": request}. Hardened IP classification
(is_blocked_ip) to also block multicast, unspecified, CGNAT
(100.64.0.0/10), and IPv4 embedded in IPv6 transition addresses
(IPv4-mapped, NAT64, 6to4, Teredo), robust across Python versions.
- Cluster B (DNS-rebinding TOCTOU): validators resolved DNS, then
requests resolved it again at connect time. New pinned-IP client
(plane/utils/url_security.py) resolves+validates once and connects to
the validated IP literal so urllib3 performs no second lookup, while
preserving Host header, TLS SNI and certificate verification against
the real hostname.
- Cluster C (redirect SSRF): webhook delivery never follows redirects;
the link crawler follows them manually, re-resolving + re-validating +
re-pinning every hop.
Also: pin requests==2.33.0 in base.txt (imported directly; the pinning
adapter needs the >=2.32 get_connection_with_tls_context hook), and log
webhook URL-validation rejections to WebhookLog instead of swallowing
them.
Tests: new test_url_security.py (pinning, rebinding, redirect
re-validation, IP edge cases, TLS SNI) + updated link-task tests.
Full unit suite: 178 passed.
* fix(api): block OAuth avatar SSRF + add per-advisory SSRF regression tests
Verified every SSRF-class advisory against the current code. The webhook /
link / favicon reports — including the published CVE-2026-30242 and
CVE-2026-39843 and the newer "still bypassable" reports (DNS rebinding
GHSA-3856/-fgcv/-9292/-whh3/-4mjx/-6p39/-fv24/-8wvv, IP-classification gaps
GHSA-75fg, redirect GHSA-6v37/-jw6g/-mq87) — are resolved by the pinned-IP
client + hardened classifier in this branch.
The one SSRF family still unresolved was the OAuth avatar path:
download_and_upload_avatar() fetched the provider-supplied avatar_url with a
raw requests.get (no IP validation, default redirect following), so an
attacker-controlled avatar could reach internal addresses and be exfiltrated
via the static-asset endpoint (GHSA-cv9p-325g-wmv5, and the avatar hop of the
Gitea SSRF GHSA-hx79-5pj5-qh42). It now uses pinned_fetch_following_redirects,
which validates + pins every hop and blocks internal targets.
Adds test_ssrf_advisories.py: a per-advisory regression map covering webhook
IP validation, the PATCH context-key guard, webhook DNS rebinding, webhook
redirect, favicon redirect + rebinding, and OAuth avatar SSRF.
docker compose test: 199 unit tests pass.
* fix(api): address PR review feedback on the SSRF pinned client
- url_security: preserve URL-embedded credentials (user:pass@host) as Basic
Auth instead of silently dropping them when rewriting to the IP literal
(Copilot); bracket IPv6-literal hostnames in the Host header (Copilot);
add stream=True support that keeps the session open until the response is
closed, and release intermediate redirect hops.
- ip_address / work_item_link_task: treat UnicodeError (IDNA failures) from
getaddrinfo as a resolution failure, not an uncaught exception (CodeRabbit).
- authentication/adapter/base: stream the avatar download so the size cap
actually bounds memory, upload the size-bounded buffer (not response.content),
and always close the response (CodeRabbit, major).
- tests: cover auth preservation, IPv6 Host bracketing, IDNA handling, and
streamed session lifetime; drop an unused import.
docker compose test: 204 unit tests pass.
78 lines
1.4 KiB
Plaintext
78 lines
1.4 KiB
Plaintext
# base requirements
|
|
|
|
# django
|
|
Django==4.2.30
|
|
# rest framework
|
|
djangorestframework==3.15.2
|
|
# postgres
|
|
psycopg==3.3.0
|
|
psycopg-binary==3.3.0
|
|
psycopg-c==3.3.0
|
|
dj-database-url==2.1.0
|
|
# redis
|
|
redis==5.0.4
|
|
django-redis==5.4.0
|
|
# cors
|
|
django-cors-headers==4.3.1
|
|
# celery
|
|
celery==5.4.0
|
|
django_celery_beat==2.6.0
|
|
django-celery-results==2.5.1
|
|
# file serve
|
|
whitenoise==6.11.0
|
|
# fake data
|
|
faker==25.0.0
|
|
# filters
|
|
django-filter==24.2
|
|
# json model
|
|
jsonmodels==2.7.0
|
|
# storage
|
|
django-storages==1.14.2
|
|
# user management
|
|
django-crum==0.7.9
|
|
# web server
|
|
uvicorn==0.29.0
|
|
# sockets
|
|
channels==4.1.0
|
|
# ai
|
|
openai==1.63.2
|
|
# slack
|
|
slack-sdk==3.27.1
|
|
# apm
|
|
scout-apm==3.1.0
|
|
# xlsx generation
|
|
openpyxl==3.1.2
|
|
# logging
|
|
python-json-logger==4.0.0
|
|
# html parser
|
|
beautifulsoup4==4.12.3
|
|
# analytics
|
|
posthog==3.5.0
|
|
# crypto
|
|
cryptography==46.0.7
|
|
# html validator
|
|
lxml==6.1.0
|
|
# s3
|
|
boto3==1.34.96
|
|
# http client (pinned to address CVE-2026-44431 and CVE-2026-44432)
|
|
urllib3>=2.7.0
|
|
# requests — used directly for webhook delivery & link unfurling; pinned to
|
|
# >=2.32 for the get_connection_with_tls_context adapter hook (SSRF IP pinning)
|
|
requests==2.33.0
|
|
# password validator
|
|
zxcvbn==4.4.28
|
|
# timezone
|
|
pytz==2024.1
|
|
# jwt
|
|
PyJWT==2.12.0
|
|
# OpenTelemetry
|
|
opentelemetry-api==1.28.1
|
|
opentelemetry-sdk==1.28.1
|
|
opentelemetry-instrumentation-django==0.49b1
|
|
opentelemetry-exporter-otlp==1.28.1
|
|
opentelemetry-exporter-otlp-proto-grpc==1.28.1
|
|
# OpenAPI Specification
|
|
drf-spectacular==0.28.0
|
|
# html sanitizer
|
|
nh3==0.2.18
|