Subj : Submission port (587/465) is not an MSA: authenticated submission requ To : GitLab note in main/sbbs From : Rob Swindell Date : Mon Aug 10 2026 01:45:08 https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10136 ## Sender validation: why it is harder than it looks, and a way through Expanding item 4 of the suggested direction. Deuce's original ask had two levels, worth keeping distinct: * **Requirement:** "email received on the submission port must be from a valid BBS domain and allows authenticated users to send only those" * **Ideal:** "ideally it should verify the from is a valid email address of the authenticated user" ### The difficulty: the recipient side is deliberately permissive Synchronet accepts a wide set of forms as a local recipient address: | Form | Notes | |---|---| | Alias | via `matchuser()` -> `matchusername()` | | Real name, or `real.name` | only when `MM_REALNAME` is set | | `sysop`, `postmaster`, `sys_id` | when `ALLOW_SYSOP_ALIASES` is set | | `scfg.sys_op` (sysop's real name) | resolves to user 1 | | `name` plus `#tag` | subaddressing, via `smtp_splittag()` / `checktag()` | | User number | only when `alias.cfg` produced it (`p == alias_buf && IS_DIGIT(*p)`) | | `alias.cfg` entry | exact match or `*suffix` wildcard; the value may be a local name, a user number, or an arbitrary **external** address | | `sub:` | posts to a sub-board | | `qwkid!user@host`, Fido `f1.n2.z3.fidonet` | routing forms | | Mail-processor `to` list | `mailproc_list[].to` | | `default_user` | catch-all: any otherwise-unknown recipient lands here | `matchusername()` is looser still: it walks both strings skipping every non-alphanumeric character, so `John Smith`, `John.Smith`, `John_Smith`, `JohnSmith` and `J.o.h.n.S.m.i.t.h` are all the same recipient. Reusing any of that for sender validation would be a mistake. ### Generate forward, do not look up in reverse The recipient question is "who does this deliver to?", where generosity is a feature. The sender question is "does this address belong to the authenticated user?", where every extra accepted form is another string a user can put in `From:` and have the server bless -- and, now, DKIM-sign. So build the small closed set of addresses that **are** the authenticated user, and compare against it: 1. `usermailaddr()` of their alias, at `sys_inetaddr` and each domain in the domain list. Mandatory: this is exactly what the server itself puts in `From:` for that user. 2. Their real name in dotted form at those domains, when `MM_REALNAME` is set (mirrors the recipient rule). 3. The `alias` plus `#tag` variants, so subaddressing survives -- same mailbox, same person. 4. Optionally their `user.netmail`, at the sysop's discretion. Compare the local part with `matchusername()` semantics, so dots and underscores remain irrelevant, and check the domain against `sys_inetaddr` plus the domain list. Cheap, closed, no reverse search. ### What must NOT feed sender validation The set above deliberately never consults `alias.cfg`, `default_user`, `sub:`, the mail-processor lists, or the QWK/Fido routing forms. All of those answer the delivery question. Two would be actively dangerous if reused: * **`default_user` makes every address valid.** On a catch-all system, any sender would validate. * **`alias.cfg` values may be external addresses.** Accepting those as senders is exactly the forgery being prevented. The one case that genuinely needs `alias.cfg`: a sysop who wants `sales@example.net` to route to user 5 *and* wants user 5 to send as it. Supporting that means consulting `alias.cfg` only for entries whose value resolves to the authenticated user's own number or alias -- never entries with external values, never `*` wildcards. That belongs behind an explicit strictness level, not in the default. ### Strictness levels and per-port defaults | Level | Meaning | |---|---| | off | No verification (today's behavior) | | domain | `From:` domain must be one of ours | | user | `From:` must be an address of the authenticated user (optionally plus self-resolving `alias.cfg` entries) | The only real argument for **off** is backward compatibility: some existing user legitimately sends through their BBS with an off-domain `From:`, which is the classic `ALLOW_RELAY` use case. That argument has no force on the submission port, which is opt-in and has no legacy behavior depending on it. Suggested defaults: * **Port 25:** `ALLOW_RELAY` as today, verification **off** by default. Nothing breaks. * **Ports 587/465:** verification always on, **user** by default, with **domain** available for sysops who need shared addresses. That makes the answer to Deuce's original question "enable the submission port" rather than "set these three options". It also resolves the disagreement rather than picking a side: sending from an arbitrary `From:` is long-standing port-25 behavior that cannot be yanked, and constraining submission to our own domains is what port 587 is *for*. Both hold once the policy is scoped per-port. ### Two spec details that bite a naive implementation * **`MAIL FROM:<>` must still be accepted.** RFC 6409 section 3.2: "a null return path, that is, MAIL FROM:<>, is permitted and MUST NOT, in itself, be cause for rejecting a message." A straight compare-to-user-address check rejects it. * **Adding `Sender:` is not a substitute for rejection.** RFC 6409 section 8.1 lets the MSA stamp `Sender:` when `From:` is not the submitter, and that is the right behavior for a shared address *within our domains* -- `From: sales@example.net` submitted by user 5. It must **never** be used as a fallback that permits an off-domain `From:`: the message would still carry an arbitrary sender and still be DKIM-signed with the system's key, which is the outcome being prevented. Off-domain `From:` gets rejected, not annotated. Also note RFC 5322 permits multiple mailboxes in `From:`. Either require every one of them to pass, or reject the message. -- *Authored by Claude (Claude Code), on behalf of @rswindell* --- SBBSecho 3.37-Linux * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705) .