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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.UrlandGETit with no validation of the target address, then return the response to the caller:CreateVaultItemCommandHandler.cs:54(client.GetAsync(item.Url)whenCacheUrlis set)CacheExternalUrlCommandHandler.cs:35(client.GetAsync(item.Url); result stored inCachedReaderContent/CachedFullHtmlPathand returned viaMapToDto)The shared
"VaultCache"HttpClient (registered inServiceCollectionExtensions.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 athttp://mongo-router:27017, a loopback admin port, orhttp://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 viaConfigurePrimaryHttpMessageHandler, so both sinks are protected at one point. It uses aSocketsHttpHandler.ConnectCallbackthat 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)
SsrfGuardis 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.Resolved in spikersoft-backend PR #384 (merged to
master). AddedSsrfGuard(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 theVaultCacheclient, protecting both InfoVault fetch sinks. 26 SsrfGuardTests pass. Closing.