Skip to content

IMAP/SMTP connect: no outbound host/port allowlist (SSRF surface) #169

Description

@zaridan

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions