Skip to content

fix: harden webhook/link/OAuth-avatar SSRF (advisory clusters A/B/C/E)#9163

Merged
sriramveeraghanta merged 3 commits into
previewfrom
fix/webhook-ssrf-clusters-abc
May 30, 2026
Merged

fix: harden webhook/link/OAuth-avatar SSRF (advisory clusters A/B/C/E)#9163
sriramveeraghanta merged 3 commits into
previewfrom
fix/webhook-ssrf-clusters-abc

Conversation

@sriramveeraghanta
Copy link
Copy Markdown
Member

@sriramveeraghanta sriramveeraghanta commented May 29, 2026

Description

Resolves three overlapping SSRF advisory clusters affecting webhook delivery and work-item link unfurling. The root fix is a single shared pinned-IP HTTP client so the address we validate is exactly the address we connect to.

  • Cluster A — private-IP validation + PATCH bypass

    • WebhookEndpoint.patch passed context={request: request} — the request object as the dict key — so self.context.get("request") was always None and the loopback/disallowed-domain guard silently no-op'd on update. Fixed to context={"request": request} (GHSA-6485-m23r-fx8q).
    • Hardened IP classification (is_blocked_ip) to also block multicast, unspecified, CGNAT 100.64.0.0/10 (not is_private on Python 3.12), and IPv4 addresses embedded in IPv6 transition formats (IPv4-mapped ::ffff:, NAT64, 6to4, Teredo). Fails closed and is robust across Python versions rather than relying on 3.12-specific ipaddress semantics.
  • Cluster B — DNS-rebinding TOCTOU (the previously-unpatched core)

    • Validators resolved DNS, then requests resolved it again at connect time, letting an attacker swap in an internal IP between the two lookups.
    • New plane/utils/url_security.py resolves + validates once, then connects to the validated IP literal so urllib3 performs no second DNS lookup (TOCTOU closed by construction), while preserving the original hostname for the Host header, TLS SNI, and certificate verification (PinnedIPAdapter injects server_hostname; assert_hostname left at None so cert checking stays on).
  • Cluster C — SSRF via HTTP redirect following

    • Webhook delivery never auto-follows redirects (allow_redirects=False); the link crawler follows them manually, re-resolving + re-validating + re-pinning every hop (a relative Location is resolved against the logical hostname URL, not the IP-literal URL).

Implementation notes:

  • Pinned requests==2.33.0 in base.txt (it is imported directly by production code, and the pinning adapter needs the >=2.32 get_connection_with_tls_context hook).
  • trust_env=False + proxies disabled on the pinned session so an ambient HTTP(S)_PROXY can't tunnel around the pin.
  • Trusted WEBHOOK_ALLOWED_HOSTS entries skip the block check but are still pinned, so a trusted hostname can't be rebound to a different internal target.
  • Webhook URL-validation rejections are now recorded in WebhookLog (visible to admins) instead of being silently swallowed.

This work was hardened by an adversarial review pass (independent attacker/verifier agents) which caught and fixed: the allowed_hosts path missing pinning, a NAT64 prefix-mask inconsistency, and silent ValueError handling.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Improvement (change that would cause existing functionality to not work as expected)

Screenshots and Media (if applicable)

N/A — backend SSRF hardening, no UI changes.

Test Scenarios

  • Webhook to a public endpoint still delivers, with Host/TLS/cert verified against the real hostname (connection pinned to the resolved IP).
  • Webhook whose URL resolves to an internal/blocked IP (e.g. 127.0.0.1, 169.254.169.254, 100.64.0.1, ::ffff:169.254.169.254) is rejected and the rejection is recorded in WebhookLog.
  • DNS-rebinding: a hostname returning a public IP at validation and a private IP at connect time still connects only to the validated IP.
  • Webhook PATCH updating the URL re-applies the loopback/disallowed-domain guard (previously bypassed via the bad context key).
  • Work-item link unfurl to a public URL fetches title/favicon; a link (or redirect hop) pointing to a private IP is blocked; a redirect loop hits the hop cap.
  • Unit suite: docker compose -f docker-compose-test.yml run --rm api-tests pytest -m unit178 passed. New test_url_security.py covers pinning, rebinding, redirect re-validation, IP edge cases, and TLS SNI injection.

References

  • Advisory clusters A / B / C (webhook & link SSRF), related to CVE-2026-30242.
  • GHSA-6485-m23r-fx8q (PATCH serializer context-key bypass).
  • Cluster B GHSAs: GHSA-whh3-5g95-4qhc, GHSA-4mjx-q738-87cf, GHSA-6p39-x6q9-h3g5, GHSA-9292-pvg4-7hvm, GHSA-fv24-3845-646g, GHSA-8wvv-p676-hcw4.
  • Cluster C GHSAs: GHSA-6v37-328w-j2wv, GHSA-jw6g-h7h5-rfc6, GHSA-mq87-52pf-hm3h.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed serializer context handling for PATCH webhook requests.
  • Security Improvements

    • Hardened SSRF protections for outbound HTTP: DNS-pinning, per-hop validation, and safer redirect handling for webhooks, link previews, favicon probes, and OAuth avatar downloads.
    • Webhook delivery now rejects invalid webhook URLs early with clear logging and avoids unsafe network calls.
  • Chores

    • Pinned HTTP client dependency for consistent network/TLS behavior.
  • Tests

    • Added comprehensive unit tests covering URL/SSRF defenses and related flows.

Review Change Stack

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.
Copilot AI review requested due to automatic review settings May 29, 2026 14:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4c2e2a1-e634-47e8-a39f-bb7ca784be8d

📥 Commits

Reviewing files that changed from the base of the PR and between ca2215d and 7932bd4.

📒 Files selected for processing (6)
  • apps/api/plane/authentication/adapter/base.py
  • apps/api/plane/bgtasks/work_item_link_task.py
  • apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py
  • apps/api/plane/tests/unit/bg_tasks/test_url_security.py
  • apps/api/plane/utils/ip_address.py
  • apps/api/plane/utils/url_security.py
💤 Files with no reviewable changes (1)
  • apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/plane/authentication/adapter/base.py
  • apps/api/plane/bgtasks/work_item_link_task.py
  • apps/api/plane/utils/url_security.py

📝 Walkthrough

Walkthrough

Centralizes SSRF defenses: adds explicit IP deny-list and resolver, implements DNS-pinned HTTP adapter and per-hop re-validation, integrates pinned fetches into webhook sending, link unfurling, and avatar downloads, and adds extensive unit tests covering pinning, redirects, and edge cases.

Changes

SSRF Defense Infrastructure & Integration

Layer / File(s) Summary
IP Security Classification & Validation Foundation
apps/api/plane/utils/ip_address.py
Introduces _BLOCKED_NETWORKS, _embedded_ipv4, is_blocked_ip, and resolve_and_validate; validate_url delegates IP checks to the resolver.
DNS-Pinned HTTP Adapter & Safe Fetch Helpers
apps/api/plane/utils/url_security.py
Adds PinnedIPAdapter, _request_to_ip, _fetch_validated_hop, pinned_fetch, and pinned_fetch_following_redirects to perform DNS-pinned requests with per-hop re-resolve/re-validate/re-pin and disabled environment proxies.
URL Security Test Coverage
apps/api/plane/tests/unit/bg_tasks/test_url_security.py
Adds tests validating IP classification, resolution+validation, pinned-request semantics, redirect re-validation, adapter TLS behavior, and validate_url hardening.
Link Unfurling Task Refactoring
apps/api/plane/bgtasks/work_item_link_task.py, apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
validate_url_ip strips IPv6 zone IDs and uses is_blocked_ip; safe_get delegates to pinned_fetch_following_redirects; favicon probing uses pinned_fetch("HEAD"); tests updated for pinning and redirect semantics.
Webhook Delivery Hardening
apps/api/plane/bgtasks/webhook_task.py
Replaces direct requests.post with pinned_fetch (enforcing WEBHOOK_ALLOWED_IPS/HOSTS) and adds except ValueError to log URL rejections as non-retryable 400s.
OAuth Avatar Download Hardening
apps/api/plane/authentication/adapter/base.py
Replaces direct avatar requests.get with pinned_fetch_following_redirects("GET", ...) to validate and pin each hop before downloading and buffering.
Dependencies & Minor Fix
apps/api/requirements/base.txt, apps/api/requirements/test.txt, apps/api/plane/app/views/webhook/base.py
Pins requests==2.33.0 in base requirements, removes it from test requirements, and fixes webhook serializer context key to {\"request\": request}.

Sequence Diagram (high-level request flow)

sequenceDiagram
  participant Client
  participant PinnedFetch
  participant Resolver
  participant Adapter
  participant RemoteServer
  Client->>PinnedFetch: pinned_fetch(method, url, headers...)
  PinnedFetch->>Resolver: resolve_and_validate(hostname)
  Resolver-->>PinnedFetch: validated IPs
  PinnedFetch->>Adapter: request to IP literal (Host=SNI=hostname)
  Adapter->>RemoteServer: TCP/TLS to IP (SNI=hostname)
  RemoteServer-->>Adapter: HTTP response (200/3xx)
  Adapter-->>PinnedFetch: Response
  alt 3xx
    PinnedFetch->>Resolver: resolve_and_validate(new-host)
    PinnedFetch->>Adapter: new pinned request to validated IP
  end
  PinnedFetch-->>Client: final response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • dheeru0198
  • pablohashescobar

Poem

🐰 A rabbit hops through pinned DNS routes,

Where private IPs meet closed little boots.
Each redirect is checked, each hop re-verified,
TLS keeps the name while the socket's pied.
Safe fetches dance — SSRF dreams denied.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: hardening SSRF vulnerabilities in webhook/link/OAuth-avatar with specific advisory cluster references.
Description check ✅ Passed The PR description provides comprehensive coverage of changes across all three advisory clusters, implementation details, test scenarios, and relevant security advisories, addressing all key template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-ssrf-clusters-abc

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.

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 hardens outbound HTTP usage in the API (webhook delivery and work-item link unfurling) against multiple SSRF vectors by eliminating DNS-rebinding TOCTOU, broadening blocked-IP coverage (including IPv6 transition formats), and ensuring validation is correctly applied on webhook updates.

Changes:

  • Introduces a shared “pinned IP” requests client (pinned_fetch*) that resolves + validates once, then connects directly to the validated IP literal while preserving Host/SNI/cert verification semantics.
  • Hardens IP blocking/validation (is_blocked_ip, resolve_and_validate) to cover additional unsafe ranges and IPv4 embedded in IPv6 transition formats; updates validate_url() to use the shared resolver/validator.
  • Moves requests==2.33.0 to production requirements and updates unit tests to cover pinning, redirect re-validation, and edge-case IP ranges.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
apps/api/requirements/base.txt Pins requests==2.33.0 in production to support the adapter hook used for IP pinning.
apps/api/requirements/test.txt Removes requests from test-only deps now that it’s required by production code.
apps/api/plane/utils/url_security.py Adds SSRF-safe pinned-IP HTTP client and manual redirect following with per-hop revalidation/repinning.
apps/api/plane/utils/ip_address.py Adds hardened blocked-IP detection and shared resolve_and_validate() used by both validators and pinning client.
apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py Updates/extends unit coverage for hardened IP blocking and pinned redirect-safe fetching behavior.

Comment thread apps/api/plane/utils/url_security.py
Comment thread apps/api/plane/utils/url_security.py
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

🧹 Nitpick comments (1)
apps/api/plane/utils/url_security.py (1)

211-234: ⚡ Quick win

Cross-host credential stripping is unnecessary for current callers; optional hardening if future callers pass auth headers.

Current repo usage only supplies a User-Agent header to pinned_fetch_following_redirects (via safe_get in work_item_link_task.py), and there are no call sites/tests passing Authorization/Cookie/Proxy-Authorization. The proposed header-dropping logic is therefore not needed for present behavior, though it could be kept as defense-in-depth for future credential-bearing usages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/utils/url_security.py` around lines 211 - 234, The
redirect-following loop in pinned_fetch_following_redirects currently includes
cross-host credential-stripping logic that is unnecessary for current callers
(safe_get via work_item_link_task.py) which only pass a User-Agent; remove the
header-dropping behavior or make it conditional/opt-in so we don't strip
Authorization/Cookie/Proxy-Authorization for present uses. Locate
pinned_fetch_following_redirects and its callsites (safe_get) and either remove
the cross-host header modification or add an explicit flag/parameter (e.g.,
strip_credentials=True) and default it to False, updating callers accordingly to
preserve current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/utils/ip_address.py`:
- Around line 136-139: The except block around socket.getaddrinfo in
resolve_and_validate currently only catches socket.gaierror, so UnicodeError
from IDNA/Unicode hostname processing escapes and is not converted to a
ValueError; update the except to also catch UnicodeError (e.g., except
(socket.gaierror, UnicodeError):) so any IDNA/Unicode failures when calling
socket.getaddrinfo(hostname, None) are converted into the same
ValueError("Hostname could not be resolved") and will be handled by
webhook_send_task as a URL rejection.

---

Nitpick comments:
In `@apps/api/plane/utils/url_security.py`:
- Around line 211-234: The redirect-following loop in
pinned_fetch_following_redirects currently includes cross-host
credential-stripping logic that is unnecessary for current callers (safe_get via
work_item_link_task.py) which only pass a User-Agent; remove the header-dropping
behavior or make it conditional/opt-in so we don't strip
Authorization/Cookie/Proxy-Authorization for present uses. Locate
pinned_fetch_following_redirects and its callsites (safe_get) and either remove
the cross-host header modification or add an explicit flag/parameter (e.g.,
strip_credentials=True) and default it to False, updating callers accordingly to
preserve current behavior.
🪄 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: 433b7fce-b817-4461-91e5-c67cd8b8b9ad

📥 Commits

Reviewing files that changed from the base of the PR and between 248f5d6 and 85c28cc.

📒 Files selected for processing (9)
  • apps/api/plane/app/views/webhook/base.py
  • apps/api/plane/bgtasks/webhook_task.py
  • apps/api/plane/bgtasks/work_item_link_task.py
  • apps/api/plane/tests/unit/bg_tasks/test_url_security.py
  • apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
  • apps/api/plane/utils/ip_address.py
  • apps/api/plane/utils/url_security.py
  • apps/api/requirements/base.txt
  • apps/api/requirements/test.txt

Comment thread apps/api/plane/utils/ip_address.py
…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.
@sriramveeraghanta sriramveeraghanta changed the title fix: harden webhook/link SSRF (advisory clusters A/B/C) fix: harden webhook/link/OAuth-avatar SSRF (advisory clusters A/B/C/E) May 29, 2026
@sriramveeraghanta
Copy link
Copy Markdown
Member Author

SSRF advisory verification (all similar cases checked)

I pulled every SSRF-class advisory from the repo's security advisories and verified each against the code on this branch. Summary: the webhook/link/favicon reports — including the ones users are re-filing as "still bypassable" — are resolved here, and the one genuinely-unresolved family (OAuth avatar SSRF) is now fixed too.

Advisory Class Status on this branch
GHSA-m3f8-q4wj-9grv (closed) / CVE-2026-30242 / GHSA-75vf-hh93-h7mx webhook → internal IP ✅ blocked (hardened validate_url + pinned send)
GHSA-75fg-f8qg-23wv webhook CGNAT/6to4/multicast missed ✅ now blocked (is_blocked_ip)
GHSA-6485-m23r-fx8q webhook PATCH context-key bypass ✅ fixed (context={"request": request})
GHSA-whh3 / -4mjx / -6p39 / -9292 / -fgcv-6h3f-xcx9 webhook DNS-rebinding TOCTOU ✅ closed (IP pinning)
GHSA-6v37-328w-j2wv / -jw6g-h7h5-rfc6 / -mq87-52pf-hm3h webhook redirect SSRF ✅ closed (allow_redirects=False)
GHSA-8wvv-p676-hcw4 / -fv24-3845-646g / -9292 link-unfurl DNS rebinding ✅ closed (pinned safe_get)
GHSA-9fr2-pprw-pp9j / CVE-2026-39843 favicon redirect SSRF ✅ blocked (per-hop re-validation)
GHSA-3856-6mgg-rx84 favicon DNS rebinding (bypass of CVE-2026-39843) ✅ closed (pinning)
GHSA-cv9p-325g-wmv5 OAuth avatar redirect SSRF → static-asset exfil fixed here (avatar fetch now pinned)
GHSA-hx79-5pj5-qh42 Gitea OAuth SSRF ⚠️ avatar hop fixed; GITEA_HOST first-hop intentionally not IP-blocked (see note)

Note on GHSA-hx79 first-hop: GITEA_HOST is admin-set config, and a self-hosted Gitea on a private network is a legitimate deployment, so IP-blocking the token/userinfo requests would break valid setups. The exploitable, low-privilege vector in that advisory is the avatar (any Gitea user sets a malicious avatar URL), which is now blocked for all providers via download_and_upload_avatarpinned_fetch_following_redirects.

Tests

apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py adds a per-advisory regression map (each test reproduces the advisory scenario and asserts it is blocked). Full unit suite via docker compose -f docker-compose-test.yml: 199 passed.

🤖 Generated with Claude Code

Comment thread apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py Fixed
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

🧹 Nitpick comments (1)
apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py (1)

49-51: ⚡ Quick win

Use socket.AF_INET/AF_INET6 in the _addr addrinfo stub (optional fidelity).
The validators currently only read the IP string from addr[4][0] (not the family field), so hard-coding 6/2 doesn’t affect correctness today—though using socket.AF_INET6/socket.AF_INET would make the mock a more faithful addrinfo tuple.

Suggested fix
+import socket
+
 def _addr(ip):
-    family = 6 if ":" in ip else 2
+    family = socket.AF_INET6 if ":" in ip else socket.AF_INET
     return (family, None, None, None, (ip, 0))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py` around lines 49 -
51, The addrinfo stub function _addr currently hardcodes the address family as
6/2; update it to use socket.AF_INET6 and socket.AF_INET respectively so the
tuple returned by _addr matches real addrinfo semantics. Modify the _addr helper
used in tests (function name _addr) to import/ reference socket and set family =
socket.AF_INET6 if ":" in ip else socket.AF_INET, leaving the rest of the tuple
structure unchanged to keep compatibility with validators that read addr[4][0].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/authentication/adapter/base.py`:
- Around line 156-158: The avatar download guard is ineffective because
pinned_fetch_following_redirects/_request_to_ip call requests without
stream=True and close the Session eagerly, and the upload still uses
response.content; update pinned_fetch_following_redirects and its helper
_request_to_ip to request with stream=True and ensure the
requests.Session/response stay open for the caller to iterate, moving session
close until after the caller finishes reading, and when constructing the upload
use the bounded in-memory buffer (the variable named content built by the
size-limited iter_content loop) instead of BytesIO(response.content) so the
Content-Length/iter_content max-size checks actually prevent large buffering.

---

Nitpick comments:
In `@apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py`:
- Around line 49-51: The addrinfo stub function _addr currently hardcodes the
address family as 6/2; update it to use socket.AF_INET6 and socket.AF_INET
respectively so the tuple returned by _addr matches real addrinfo semantics.
Modify the _addr helper used in tests (function name _addr) to import/ reference
socket and set family = socket.AF_INET6 if ":" in ip else socket.AF_INET,
leaving the rest of the tuple structure unchanged to keep compatibility with
validators that read addr[4][0].
🪄 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: 72d2f58a-6900-401b-a500-6689c682c82d

📥 Commits

Reviewing files that changed from the base of the PR and between 85c28cc and ca2215d.

📒 Files selected for processing (2)
  • apps/api/plane/authentication/adapter/base.py
  • apps/api/plane/tests/unit/bg_tasks/test_ssrf_advisories.py

Comment thread apps/api/plane/authentication/adapter/base.py
- 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.
@sriramveeraghanta sriramveeraghanta merged commit 04622ce into preview May 30, 2026
13 checks passed
@sriramveeraghanta sriramveeraghanta deleted the fix/webhook-ssrf-clusters-abc branch May 30, 2026 18:42
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