dotnet / dotnet/eShop

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

Open
#999 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
10.9k
Forks
3.8k
Avg merge
1d 11h
Merged PRs (30d)
4

Description

## 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`

```csharp
public async Task 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

```bash

```

Verified by pointing `` 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.