POST /api/v1/inbound/imap/connect and /imap/check take an operator-supplied imapHost/imapPort/smtpHost/smtpPort and make the server open a connection to them. There is no validation of the target at all.
PR #165 added authorization — both routes now require an admin acting Agent, where previously the service Bearer alone sufficed. That bounds who can name a target. It does not bound which targets are nameable.
The remaining surface
An admin can point either endpoint at any host and port the deployment can reach — including link-local metadata endpoints, private RFC1918 ranges, and localhost — and read connection outcomes back through the per-leg LegResult, distinguishing open from closed from filtered.
The threat model is narrower than the pre-fix one (admin, not any token holder), but "admin" is not the same as "may probe the deployment's internal network," and connect/check is reachable from the operator UI.
Precedent in this codebase
src/webhooks/ssrf.ts already solves the equivalent problem for webhook endpoint URLs. Outbound mail connections should get comparable treatment rather than being the one un-guarded outbound dial.
Acceptance
- A shared outbound-target policy covers both webhook URLs and mail host/port, or the mail path gets its own with an explicit, documented rationale for differing.
- Private, loopback, and link-local targets are refused by default, with an operator-configurable escape hatch for genuinely self-hosted mail servers on a private network — which is a real and supported deployment, so this must not be a flat ban.
- Refusal is a clean 4xx naming the reason, never a timeout the operator has to guess at.
- Tests cover loopback, RFC1918, link-local, and a DNS name that resolves to a private address.
Note the last case: allowlisting by hostname alone is insufficient, since a public name can resolve to a private address. This needs resolution-time checking, which is the same trap ssrf.ts already documents.
Found by CodeRabbit on PR #165, partially addressed there (authorization only), 2026-07-31.
POST /api/v1/inbound/imap/connectand/imap/checktake an operator-suppliedimapHost/imapPort/smtpHost/smtpPortand make the server open a connection to them. There is no validation of the target at all.PR #165 added authorization — both routes now require an admin acting Agent, where previously the service Bearer alone sufficed. That bounds who can name a target. It does not bound which targets are nameable.
The remaining surface
An admin can point either endpoint at any host and port the deployment can reach — including link-local metadata endpoints, private RFC1918 ranges, and localhost — and read connection outcomes back through the per-leg
LegResult, distinguishing open from closed from filtered.The threat model is narrower than the pre-fix one (admin, not any token holder), but "admin" is not the same as "may probe the deployment's internal network," and connect/check is reachable from the operator UI.
Precedent in this codebase
src/webhooks/ssrf.tsalready solves the equivalent problem for webhook endpoint URLs. Outbound mail connections should get comparable treatment rather than being the one un-guarded outbound dial.Acceptance
Note the last case: allowlisting by hostname alone is insufficient, since a public name can resolve to a private address. This needs resolution-time checking, which is the same trap
ssrf.tsalready documents.Found by CodeRabbit on PR #165, partially addressed there (authorization only), 2026-07-31.