[Security] SSRF in InfoVault URL caching — user-supplied vault URL fetched server-side with no internal-address guard #685

Closed
opened 2026-07-17 22:23:12 +00:00 by spikerj · 1 comment
Owner

Vulnerability (SSRF)

The InfoVault feature lets an authenticated user save an external URL to their vault and have the server fetch and cache its content. Both fetch sinks pull the user-controlled item.Url and GET it with no validation of the target address, then return the response to the caller:

  • CreateVaultItemCommandHandler.cs:54 (client.GetAsync(item.Url) when CacheUrl is set)
  • CacheExternalUrlCommandHandler.cs:35 (client.GetAsync(item.Url); result stored in CachedReaderContent / CachedFullHtmlPath and returned via MapToDto)

The shared "VaultCache" HttpClient (registered in ServiceCollectionExtensions.cs:630) had no SSRF protection, and there is no FluentValidation validator constraining the URL. The API runs on the internal overlay networks (Mongo, Redis, RabbitMQ, Keycloak, OpenBao), so any authenticated user could point a vault item at http://mongo-router:27017, a loopback admin port, or http://169.254.169.254/… (cloud metadata) and read the response back out of their vault cache — internal-service reconnaissance and data exfiltration.

Fix (this PR)

Added SsrfGuard (SpikerSoft.Api/Infrastructure/Security/SsrfGuard.cs) and wired it as the primary handler of the "VaultCache" client via ConfigurePrimaryHttpMessageHandler, so both sinks are protected at one point. It uses a SocketsHttpHandler.ConnectCallback that resolves the host and validates the ACTUAL connect IP — after DNS, before the socket connects — refusing loopback / RFC1918 private / link-local (incl. 169.254.169.254) / IPv6 unique-local / CGNAT / unspecified / multicast targets (and IPv4-mapped-IPv6 forms). Validating at connect time also covers HTTP redirects (each redirect reconnects through the callback) and DNS-rebinding (no check/use gap). Legitimate public URLs are unaffected.

Tests

SsrfGuardTests — 26 cases: 18 non-public targets blocked (loopback, all RFC1918 ranges, cloud-metadata, IPv6 unique-local, CGNAT, multicast, and ::ffff: IPv4-mapped evasions) + 8 public targets allowed (incl. boundary cases just outside 172.16/12 and 100.64/10). All pass.

Follow-up (not in this PR)

SsrfGuard is reusable; any future HttpClient that fetches user-supplied URLs should adopt the same handler. A deeper defense would also add network-egress restrictions on the API to the untrusted-fetch path.

## Vulnerability (SSRF) The InfoVault feature lets an authenticated user save an external URL to their vault and have the server fetch and cache its content. Both fetch sinks pull the user-controlled `item.Url` and `GET` it with **no validation** of the target address, then return the response to the caller: - `CreateVaultItemCommandHandler.cs:54` (`client.GetAsync(item.Url)` when `CacheUrl` is set) - `CacheExternalUrlCommandHandler.cs:35` (`client.GetAsync(item.Url)`; result stored in `CachedReaderContent` / `CachedFullHtmlPath` and returned via `MapToDto`) The shared `"VaultCache"` HttpClient (registered in `ServiceCollectionExtensions.cs:630`) had no SSRF protection, and there is no FluentValidation validator constraining the URL. The API runs on the internal overlay networks (Mongo, Redis, RabbitMQ, Keycloak, OpenBao), so **any authenticated user** could point a vault item at `http://mongo-router:27017`, a loopback admin port, or `http://169.254.169.254/…` (cloud metadata) and read the response back out of their vault cache — internal-service reconnaissance and data exfiltration. ## Fix (this PR) Added `SsrfGuard` (`SpikerSoft.Api/Infrastructure/Security/SsrfGuard.cs`) and wired it as the primary handler of the `"VaultCache"` client via `ConfigurePrimaryHttpMessageHandler`, so **both** sinks are protected at one point. It uses a `SocketsHttpHandler.ConnectCallback` that resolves the host and validates the ACTUAL connect IP — after DNS, before the socket connects — refusing loopback / RFC1918 private / link-local (incl. 169.254.169.254) / IPv6 unique-local / CGNAT / unspecified / multicast targets (and IPv4-mapped-IPv6 forms). Validating at connect time also covers HTTP redirects (each redirect reconnects through the callback) and DNS-rebinding (no check/use gap). Legitimate public URLs are unaffected. ## Tests `SsrfGuardTests` — 26 cases: 18 non-public targets blocked (loopback, all RFC1918 ranges, cloud-metadata, IPv6 unique-local, CGNAT, multicast, and `::ffff:` IPv4-mapped evasions) + 8 public targets allowed (incl. boundary cases just outside 172.16/12 and 100.64/10). All pass. ## Follow-up (not in this PR) `SsrfGuard` is reusable; any future HttpClient that fetches user-supplied URLs should adopt the same handler. A deeper defense would also add network-egress restrictions on the API to the untrusted-fetch path.
Author
Owner

Resolved in spikersoft-backend PR #384 (merged to master). Added SsrfGuard (SocketsHttpHandler.ConnectCallback validating the actual connect IP — blocking loopback/RFC1918/link-local/metadata/IPv6-ULA/CGNAT/multicast + IPv4-mapped-v6, covering redirects & DNS-rebinding) as the primary handler of the VaultCache client, protecting both InfoVault fetch sinks. 26 SsrfGuardTests pass. Closing.

Resolved in spikersoft-backend PR #384 (merged to `master`). Added `SsrfGuard` (SocketsHttpHandler.ConnectCallback validating the actual connect IP — blocking loopback/RFC1918/link-local/metadata/IPv6-ULA/CGNAT/multicast + IPv4-mapped-v6, covering redirects & DNS-rebinding) as the primary handler of the `VaultCache` client, protecting both InfoVault fetch sinks. 26 SsrfGuardTests pass. Closing.
Sign in to join this conversation.