Skip to content

SSRF: webhook subscription issues an outbound request to a caller-controlled URL with no destination validation #999

Description

@luuhung1217

Summary

When a webhook subscription is created, Webhooks.API issues a server-side OPTIONS request to a URL taken from the request body, with no validation of the destination (no allow-list, and no deny-list for loopback / link-local / RFC1918 / cloud-metadata addresses). An authenticated caller can therefore make the service send requests to internal hosts it can reach but the caller cannot — a server-side request forgery (blind: the response body is not returned to the caller).

The only precondition before the outbound call, CheckSameOrigin(request.Url, request.GrantUrl), is not a destination-security control — see "On CheckSameOrigin" below.

Affected code

src/Webhooks.API/Services/GrantUrlTesterService.cs

public async Task<bool> TestGrantUrl(string urlHook, string url, string token)
{
    if (!CheckSameOrigin(urlHook, url))          // urlHook = request.Url, url = request.GrantUrl
        return false;

    var client = factory.CreateClient();
    var msg = new HttpRequestMessage(HttpMethod.Options, url);   // <-- outbound to a caller-chosen host
    msg.Headers.Add("X-eshop-whtoken", token);
    var response = await client.SendAsync(msg);                  // no host/IP allow- or deny-list
    ...
}

The stored DestUrl (= request.Url) is also requested later at event-delivery time (WebhooksSender), extending the same issue beyond subscription.

Steps to reproduce

Verified by pointing <internal-host> at a local listener: the listener received a server-originated
OPTIONS / request (carrying the X-eshop-whtoken header), confirming the service performs an outbound
request to a caller-controlled, loopback destination. Pointing it at another internal port (e.g. a database
port) still triggers the outbound request (blind; non-2xx response → 400 to the caller, but the request
is sent).

Note: reaching cloud-metadata specifically (169.254.169.254) is inferred from the absence of any IP
filtering in the code
, not directly demonstrated in my environment (no metadata endpoint present).

On CheckSameOrigin (why this isn't a "bypass")

CheckSameOrigin compares the origin of request.Url (event-delivery target) with request.GrantUrl
(ownership-grant target). Its purpose is delivery-consent binding — ensuring the endpoint that proves
consent shares an origin with the endpoint that will receive events — not SSRF protection.

It cannot serve as an SSRF control: both URLs come from the same request, so the caller simply sets them
equal and points both at any internal host. Setting Url == GrantUrl is satisfying a functional
precondition, not defeating a security mechanism. The actual gap is the complete absence of destination
validation
on the outbound request.

Impact

Authenticated, blind SSRF from Webhooks.API to arbitrary internal endpoints (loopback, RFC1918, link-local,
including cloud-metadata and service-mesh addresses). Exploitable via side effects / timing, and for
credential theft against metadata services that respond to the request.

Suggested fix

Validate the destination of the outbound request, independently of CheckSameOrigin:

  • After DNS resolution, reject private / loopback / link-local / metadata ranges
    (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, …),
    re-checking post-resolution to defeat DNS rebinding.
  • Restrict the scheme to https (and http only if explicitly required).
  • Prefer an explicit allow-list of permitted destinations where the deployment allows it.
  • Apply the same validation at delivery time (WebhooksSender), not only at subscription time.

CheckSameOrigin can remain for its delivery-consent purpose, but it is orthogonal to the fix above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions