Skip to content

fix(webhook): Block SSRF via customer webhook endpoints - #7459

Merged
regdocs merged 3 commits into
developfrom
fix/webhook-ssrf
Sep 11, 2026
Merged

fix(webhook): Block SSRF via customer webhook endpoints#7459
regdocs merged 3 commits into
developfrom
fix/webhook-ssrf

Conversation

@regdocs

@regdocs regdocs commented Sep 11, 2026

Copy link
Copy Markdown
Member

Problem

The Press Webhook feature (Settings → Developer) lets any team configure an endpoint URL that Press then sends HTTP requests to — on Validate Webhook and on every event delivery. The request is made by the Press host, which is the control plane and holds fleet-wide credentials, so an endpoint that points inward turns Press into a proxy for internal resources.

validate_endpoint_url_format only rejected the private ranges when the URL contained a literal IP. Three gaps let a request reach internal addresses anyway:

  1. A hostname that resolves to a private IP passed the check (the resolve was never done). Encoded forms like http://2130706433/ also passed and resolve to 127.0.0.1.
  2. requests.post followed redirects in both the validate and the delivery path, so a public endpoint could answer 302 → http://169.254.169.254/….
  3. validate_endpoint returned the response body to the user, and every delivery stored it in Press Webhook Attempt.

Chained: set the endpoint to a public host that 302-redirects to the cloud metadata service, click Validate, and the host's metadata (IAM credentials on some providers, user-data, SSH keys) comes back in the dashboard.

Fix

New press/utils/ssrf.py with ssrf.post(url, **kwargs):

  • Resolves the host and refuses if any resolved address is non-global (blocks the metadata IP, private ranges, IPv4-mapped IPv6, and mixed-record DNS rebinding).
  • Pins the connection to the resolved IP, so the name can't be re-pointed to an internal address between the check and the connect (DNS rebinding). TLS SNI and the certificate check stay bound to the original hostname, verified against real HTTPS hosts, so legitimate endpoints are unaffected.
  • Forces allow_redirects=False.

Both call sites — PressWebhook.validate_endpoint and PressWebhookLog._send_webhook_call — now go through it and surface SSRFError as a normal failure.

Follow-ups (infrastructure, not in this PR)

These close the exposure that already exists and can't be done in a press PR — flagging for whoever owns the control-plane hosts:

  • Rotate any secrets baked into the Press hosts' user-data; assume they may already have been read.
  • Enforce IMDSv2 and minimise (or remove) the control-plane host's IAM role.
  • Block 169.254.169.254 egress on the Press hosts.

Tests

  • press/utils/test_ssrf.py — resolver refuses the metadata IP, private ranges, mixed-record rebinding, IPv4-mapped loopback, and unresolvable hosts; post refuses before opening any socket.
  • test_press_webhook.pyvalidate_endpoint refuses a host resolving to the metadata IP and leaks no body.

🤖 Generated with Claude Code

regdocs and others added 2 commits September 11, 2026 18:37
Webhook endpoints are customer-controlled, and Press — the control plane
that holds fleet-wide credentials — is what sends the request. A plain
requests.post lets the endpoint point at the cloud metadata service, a
private range, or localhost and reflect the response back.

ssrf.post resolves the host, refuses any non-global address, pins the
connection to the resolved IP (so the name can't rebind between the check
and the connect), and never follows redirects. TLS SNI and the certificate
check stay bound to the original hostname, so legitimate HTTPS endpoints
are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The endpoint validation only rejected literal private IPs, so a hostname
resolving to a private address (or an encoded IP) passed, and requests.post
followed redirects — letting "Validate Webhook" reach http://169.254.169.254
via a 302 and return the host's cloud metadata to the user. The background
delivery path leaked the same way into Press Webhook Attempt.

Route both call sites — validate_endpoint and _send_webhook_call — through
ssrf.post so the request is refused unless it resolves to a public address.

Also fix a pre-existing wrong annotation (save: True) and allowlist the
test fixture secret, both surfaced by the hooks on the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@regdocs regdocs self-assigned this Sep 11, 2026
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

Safe to merge.

Reviews (2) · Last reviewed commit: "fix(webhook): Keep Basic Auth credential..."

Comment thread press/utils/ssrf.py Outdated
@codecov-commenter

codecov-commenter commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.56198% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.09%. Comparing base (07485b0) to head (7352817).
⚠️ Report is 24 commits behind head on develop.

Files with missing lines Patch % Lines
press/utils/ssrf.py 89.09% 6 Missing ⚠️
...ess/doctype/press_webhook_log/press_webhook_log.py 40.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #7459      +/-   ##
===========================================
+ Coverage    61.94%   62.09%   +0.14%     
===========================================
  Files         1067     1069       +2     
  Lines       102449   102758     +309     
  Branches      1777     1778       +1     
===========================================
+ Hits         63460    63805     +345     
+ Misses       38944    38908      -36     
  Partials        45       45              
Flag Coverage Δ
dashboard 85.94% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The pinning adapter built the Host header from parts.netloc, which includes
any user:password@ userinfo. Endpoints with Basic Auth in the URL were sent
a malformed "Host: user:password@example.com" and failed validation and
delivery. requests already moves the credentials into the Authorization
header, so build the Host (and the pinned IP authority) from the host and
port only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@regdocs regdocs added the backport-master For mergify backport to master label Sep 11, 2026
@mergify

mergify Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@regdocs
regdocs merged commit bb299c6 into develop Sep 11, 2026
14 checks passed
@regdocs
regdocs deleted the fix/webhook-ssrf branch September 11, 2026 13:37
regdocs added a commit that referenced this pull request Sep 11, 2026
fix(webhook): Block SSRF via customer webhook endpoints (backport #7459)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-master For mergify backport to master

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants