Files
Félix Malfait 38b55f8890 Block private-network SSO issuers and replace the safe-mode toggle with an internal host allowlist (#25756)
Fixes #25638. Supersedes #25647, which was closed pending a decision on
self-hosted deployments that run their identity provider on an internal
network.

## What

**SSO requests now go through the SSRF-safe agent.**
`Issuer.discover()`, the JWKS fetch during ID token validation, and the
token and userinfo calls were the only outbound requests not going
through `SecureHttpClientService`, so a workspace admin with the
`SECURITY` permission could point them at internal targets. They are
wired through openid-client's `custom.http_options` hook. openid-client
resolves that hook on whichever object issues the request (the `Issuer`
class for discovery, the issuer instance for JWKS, the client instance
for token and userinfo), so it is set on all three. #25647 only covered
the first, which left the endpoints in an attacker-controlled discovery
document unprotected. `OidcAuthGuard` now calls
`SsoService.discoverOidcIssuer` rather than `Issuer.discover` itself, so
setup and login share one path.

**`OUTBOUND_HTTP_SAFE_MODE_ENABLED` is replaced by
`OUTBOUND_HTTP_ALLOWED_INTERNAL_HOSTS`.** Self-hosters commonly run
their IdP, mail or calendar server on a private address (a sibling
docker-compose container, a LAN host). The only escape hatch was the
global toggle, which also disables protection for webhooks and workflow
actions, and applying it to SSO would have locked SSO-only workspaces
out on upgrade since discovery runs on every login. The new variable is
a comma-separated list of hostnames or IP literals that may be reached
on a private network. Matching is exact and case-insensitive on the
hostname, with no wildcard or suffix patterns; a pasted issuer URL or
`host:port` is reduced to its hostname. `*` allows all, which is what
the old `false` did. Everything else stays protected, and an allowlisted
host can never reach the link-local range (`169.254.0.0/16`,
`fe80::/10`) where cloud metadata services live, so a DNS change cannot
turn an allowlisted name into a path to instance credentials.

**Deprecation.** The old boolean stays declared, marked deprecated, and
`false` is honoured as `*` in
`SecureHttpClientService.getAllowedInternalHosts()` so existing
self-hosted setups keep working after upgrading. While it is `false` the
allowlist is ignored; the server logs a warning once per process saying
so. It can be deleted in a later release; that is the only remaining
reference.

**Setup-time error.** A blocked issuer used to surface as a bare
"Invalid issuer". The underlying reason (e.g. `Request to internal IP
address 172.18.0.5 is not allowed.`) is now included in the setup error
so the admin can diagnose it there. At login the failure only shows as a
redirect.

## Config

| Value | Effect |
| --- | --- |
| empty (default) | private-network addresses are blocked for HTTP
workflow actions, webhooks, SSO and IMAP/SMTP/CalDAV |
| `keycloak,mail.internal,192.168.1.10` | those hosts may reach private
networks (never link-local) |
| `*` | protection off (previous
`OUTBOUND_HTTP_SAFE_MODE_ENABLED=false`) |

## Tests

- Unit: allowlist cases for the agent and hostname utils (private
address allowed, link-local still blocked, wildcard), `isLinkLocalIp`, a
`SecureHttpClientService` spec covering `*`, the deprecated fallback
with its one-time warning and URL/`host:port` entry normalisation, and
an `SsoService` spec that builds the real `SecureHttpClientService` and
asserts the agent refuses `169.254.169.254` for discovery, JWKS, token
and userinfo.
- Integration: the suites that flipped the toggle off to reach the
dovecot, greenmail, radicale and webhook-receiver containers now set the
list to `['*']`. `imap-smtp-outbound` sets it to the actual container
host instead, so the per-host exemption is exercised end to end.
- `npx tsgo --noEmit`, oxlint and oxfmt are clean on the changed files.

## Docs

The self-hosting note in the calendar and emails guide now points at the
allowlist instead of telling people to turn safe mode off, and the
self-host setup guide gains an "Outbound Connections to Internal Hosts"
section. Translated copies under `l/` are left to the i18n pipeline.

## Release note

Self-hosted deployments whose identity provider, mail or calendar server
resolves to a private address should add its hostname or IP to
`OUTBOUND_HTTP_ALLOWED_INTERNAL_HOSTS`. Deployments that set
`OUTBOUND_HTTP_SAFE_MODE_ENABLED=false` keep working unchanged for now,
but that flag overrides the new list, so remove it when migrating.

Session: https://claude.ai/code/session_01WPVhQuV1QgbTS63ivuTjjH
2026-09-11 09:16:26 +02:00
..