.agents/skills/security-review/references/outbound-url-validation.md
Any Langfuse code path that issues an outbound HTTP request to a URL derived from user input (mutation form, public API field, integration config, image proxy) can be coerced into Server-Side Request Forgery. The high-value internal targets in Langfuse's deployment topology include:
169.254.169.254, IMDSv2 endpoints)127.0.0.1, localhost, Docker API on
2375/2376)Even when the surface requires integrations:CRUD or admin scope, the
attacker model assumes the credentialed user is malicious or compromised;
SSRF lets them pivot from app-level admin to network-level access that the
deployment topology otherwise denies.
All under packages/shared/src/server/outbound-url/:
parseOutboundUrl(urlString)
— safe parse. Rejects embedded credentials, invalid encoding, and bad URL
syntax. Use this instead of new URL(...) for any user-supplied URL.validateOutboundUrlHost({ url, whitelist, logContext, shouldSkipDnsCheckForLiteralIps })
— checks hostname blocklist, IP literal blocklist, and forward DNS
resolution against blocked CIDRs (defends DNS rebinding by resolving every
A/AAAA plus the local getaddrinfo view).addSecureOutboundConnectionValidation(options, ...)
— attaches connect-time IP validation to a fetch request so the TCP peer
is re-validated after DNS resolution, not just at save time.fetchWithSecureRedirects(...)
— manual redirect handling. Validates each Location hop with the
caller-supplied validator and strips sensitive headers (Authorization,
Cookie, signing headers) on cross-origin redirects.Surface-specific wrappers (reuse these rather than rolling your own):
validateLlmConnectionBaseURLvalidateWebhookURLvalidateBlobStorageEndpoint
and the companion
blobStorageEndpointConnectionValidationOptions
for connect-time enforcement through StorageServiceFactory.Every outbound-URL surface MUST apply all three of:
addSecureOutboundConnectionValidation (or the SDK's equivalent hook)
through the request.fetch() defaults to redirect: 'follow' and will silently chase a
redirect into the loopback range. Use fetchWithSecureRedirects with the
matching validator instead.web/src/features/llm-api-key/server/router.ts (update mutation calls
validateLlmConnectionBaseURL before persisting).web/src/pages/api/public/llm-connections/index.ts.packages/shared/src/server/webhooks/validation.ts is wired into both the
automation form and the worker-side webhook sender.web/src/features/blobstorage-integration/blobstorage-integration-router.ts
(validate mutation calls validateBlobStorageEndpoint); connection-time
enforcement flows through StorageServiceFactory.getInstance({ connectionValidation: blobStorageEndpointConnectionValidationOptions() }).packages/shared/src/server/... that delegates to
validateOutboundUrlHost so blocklist behavior, DNS rebinding handling,
and credential checks stay centralized.LANGFUSE_WEBHOOK_WHITELISTED_HOST/IPS/IP_SEGMENTS,
LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST/IPS/IP_SEGMENTS, and
LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST/IPS/IP_SEGMENTS). Do
not share another surface's allowlist; each surface keeps its own..env*.example and the package env.mjs/ts.fetchWithSecureRedirects with the same
wrapper as the validator.127.0.0.1, 169.254.169.254, an RFC1918 literal, an RFC1918 hostname
(DNS rebinding), http:// on Cloud, and a URL containing
user:pass@host.fetch(<user-supplied-url>) (or axios, got, etc.) without an upstream
call to a validate*URL helper, or without
addSecureOutboundConnectionValidation on the request options.host / endpoint / baseURL / webhookUrl
field without invoking the matching validator before the write.StorageServiceFactory.getInstance({ endpoint }) (or any SDK client init
that takes a user-controlled URL) without connectionValidation plumbed
through.new URL(userInput) instead of
parseOutboundUrl(userInput). The latter rejects embedded credentials and
bad encoding, both of which are recurring SSRF/credential-leak vectors.fetch(url) (default redirect: 'follow') on a
user-controlled URL. Switch to fetchWithSecureRedirects.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION set) forces strict mode.
Allowlist env vars are ignored on Cloud, and HTTPS is enforced for surfaces
that require it (LLM base URL, blob storage endpoint).LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST/IPS/IP_SEGMENTSLANGFUSE_WEBHOOK_WHITELISTED_HOST/IPS/IP_SEGMENTSLANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST/IPS/IP_SEGMENTSTODO(next major) in
blobStorageEndpointValidation.ts to flip the default; until then, do not
rely on blob storage validation for new surfaces — wire your own wrapper
that defaults to strict.A change that adds a new outbound URL surface MUST include server-side tests that assert each of the following fails validation:
http://127.0.0.1, http://[::1])http://169.254.169.254)http://10.0.0.1)http://user:pass@host)http:// on Cloud (where HTTPS is required)Pattern reference:
worker/src/__tests__/llm-base-url-validation.test.ts and the test files
adjacent to the wrappers above.