Dashboard: no host validation for outbound requests; add anti-SSRF guardrail before a telemetry-derived URL gets fetched
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
## Summary
The dashboard has no written rule about host validation for outbound requests, and no SSRF guardrail exists anywhere in the repo. This is **defense-in-depth**, not a report of a live exploitable path — after auditing every outbound call site I could not find one, and I don't think there's production code to change today. The deliverable is the invariant, so the *next* feature doesn't introduce the bug.
The policy we're aligning to:
> If your service handles untrusted input which is used to form a URL, you MUST take protective measures to ensure its target hostname is an expected hostname. In cases where you allow arbitrary hostnames, you MUST NOT pass any sensitive credential, token, or information on that request.
"Untrusted" explicitly includes configuration values, metadata, and file contents that users can influence — not just inbound HTTP request data.
## Audit of the current outbound surface
Searching `src/Aspire.Dashboard` for `HttpClient` / `IHttpClientFactory` / `HttpMessageHandler` / `SocketsHttpHandler` / `GrpcChannel.ForAddress` / `ClientWebSocket` yields exactly four server-side call sites. In a **shipping build, three of the four are pinned to `localhost`**:
| Call site | Target host | Credential attached |
|---|---|---|
| `Model/DebugSessionHelpers.cs` `CreateHttpClient` | **pinned** — `new Uri($"{scheme}://localhost:{port}")`; only the port is config-supplied | `Authorization` bearer + pinned server cert via `ServerCertificateCustomValidationCallback` |
| `Telemetry/DashboardTelemetrySender.cs` | via the debug-session client above — **pinned** | same |
| `Model/Assistant/ChatClientFactory.cs` | **pinned in release.** The `ASPIRE_AI_ENDPOINT` arbitrary-host branch is entirely inside `#if DEBUG` (lines 18-22, 99-129, 196-219); ship builds always fall through to the localhost debug-session URI, so the `/ghcp_info` bearer request stays on loopback | bearer, to loopback |
| `ServiceClient/DashboardClient.cs` | **arbitrary** — resource service URL config → `GrpcChannel.ForAddress` | `x-resource-service-api-key` header |
`DebugSessionHelpers` is the pattern worth copying: pin the host and the server certificate *first*, then attach the token.
`DashboardClient` is the single arbitrary-host + credential path in a shipped dashboard, and I'd argue it should stay that way. The resource service lives wherever the AppHost put it, so the address is architecturally required to be arbitrary; it's one operator-supplied config value read once at startup, and it's gRPC over HTTP/2 with no redirect following. Constraining it would break the product and mitigate nothing.
## What is *not* the problem (checked, so nobody re-treads this)
- **Server-side redirects are clean.** `Model/TargetLocationInterceptor.cs` only ever emits the hardcoded `StructuredLogsPath` constant, and for absolute URLs it compares `uri.GetLeftPart(UriPartial.Authority)` against `appBaseUri` and bails on mismatch. The single `Response.Redirect` in `DashboardWebApplication.cs` passes that constant. No open redirect, and no server-side redirect-following with a credential.
- **OTLP ingestion is inbound, so it is not SSRF by itself.** It is however the largest source of untrusted input in the product — span attributes, log bodies, and resource-model URLs originate from monitored apps, which can be arbitrary third-party containers.
- **Client-side `NavigateTo` targets are safe.** `ManageDataDialog.razor.cs` navigates to `dataRow.Url`, but every `TelemetryDataRow.Url` is built from `DashboardUrls.*Url(resource: resourceName)`, which produces a root-relative path with the resource name escaped through `Uri.EscapeDataString` / `AddQueryString`. No scheme or host is attacker-influenced.
## The actual gap
Nothing states that a URL derived from telemetry or the resource model must be host-validated before a server-side fetch, and must never carry a dashboard credential. The risk is a future feature that dereferences such a URL — a favicon, a health-check probe, a "preview this URL" affordance, or an assistant/MCP tool that follows a link out of telemetry. Any of those is genuine request-driven SSRF with an ambient credential in scope, and nothing in the codebase or review checklist would flag it.
## Proposed
Write the invariant into `.github/instructions/dashboard.instructions.md` so reviewers and agents catch it at review time:
> Never let a server-side request target a URL derived from untrusted input, and never send a dashboard credential to a host that isn't pinned. Telemetry is untrusted. Follow `DebugSessionHelpers.CreateHttpClient`: pin the host and server certificate first, then attach the token. The resource service in `DashboardClient` is the one deliberate exception.
I deliberately am **not** proposing a shared URL-validation helper yet. Nothing would call it today, so it'd be dead code that rots — worth adding at the moment a feature actually needs to fetch an untrusted URL, not before.
Contributor guide
Research direction
Read .github/instructions/dashboard.instructions.md first, then review the cited DebugSessionHelpers.CreateHttpClient and ServiceClient/DashboardClient.cs examples to preserve the deliberate exception. Done means the file states the telemetry-derived URL host-validation and credential rule, including the pinned-host pattern and resource-service exception.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- documentation, security
- Issue type
- Documentation
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100