FMG-3: fix SMTP authentication bypass, remote DoS, and silent delivery failures #9
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
fmartingr/smtp2shoutrrr!9
Loading…
Reference in a new issue
No description provided.
Delete branch "butterrobot/fmg-3-smtp-backend-fixes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three defects in the SMTP backend, all predating the FMG-2 dependency upgrade.
Authentication bypass
The credential check joined its two comparisons with
&&, so it only rejected a client that got both halves wrong — a correct username with any password authenticated, as did a correct password with any username. Both comparisons now have to pass, and both are evaluated before either is acted on so the timing does not distinguish a wrong username from a wrong password. They compare SHA-256 digests throughsubtle.ConstantTimeCompare, which keeps the length of the configured credential out of the timing as well.Fixing the comparison alone does not close the hole. go-smtp advertises
AUTHbut leaves enforcement to the backend, so a client could skipAUTHentirely and still have its message forwarded. The session now tracks authentication and refusesMAIL,RCPTandDATAwithout it.Nor does the gate mean anything while the credentials it checks can be guessed.
SetDefaultsinventedUsername = "username"andPassword = "password"when the config omitted them, with only aslog.Warn— so aconfig.tomlnaming nothing but a port and its recipients relayed to anyone who tried the obvious pair, for exactly the deployments that had configured the least.Validatenow rejects unset credentials the way it already rejects an unusable target, and the invented defaults are gone.Behavior change: a client that used to send without issuing
AUTHnow has to be configured with the credentials fromconfig.toml, and a deployment that never set them will not start until it does. Both documented in the README.Remote denial of service
A malformed
Content-Typereachedlog.Fatalf, which callsos.Exit(1). One message with an attacker-controlled header terminated the daemon and took every other in-flight message with it.Body()now returns the parse error like the rest of the function does.Silent notification loss
Datalogged forwarding errors and returned250, so a client whose notification reached no target at all treated the message as delivered and never retried. The reply now reflects what happened:250— at least one target accepted it. A partial failure is logged but still accepted, since a retry redelivers to every target and would duplicate the notification on the ones that already have it. This keeps the existingTestPartialTargetFailurecontract.451— no target accepted it. Worth retrying.550 5.6.0— the message cannot be parsed. No retry can fix the same bytes.550 5.3.5— the recipient has no notification targets configured. A configuration fault; no retry can add targets.LoadConfigblocks such a config, so this is only reachable from one built in code.Session transaction state
Session.Resetwas empty while go-smtp calls it after every message, so recipients accumulated across a connection and the second message on it was also forwarded to the first one's targets.Fixing that introduced a data race, caught in review:
Resetruns on the connection goroutine, but go-smtp runsDataon its own goroutine for aBDATtransfer and does not blockRSETduring one. A mutex now guards the recipients and the authentication flag acrossRcpt,DataandReset, andDatasnapshots the recipients up front instead of reading the slice while forwarding.Validation
make format,make ci-lint(0 issues),make test(-race, 94.2% coverage),make checkandmake buildall pass locally.Every regression test was confirmed to fail against the code it guards:
origin/main— the malformedContent-Typeone by killing the test binary outright, which is the reported DoS;TestResetDuringChunkedTransferIsSynchronizedagainstda8e8e4, where-racereportsSession.ResetracingSession.Dataon all three runs attempted;TestLoadConfigRejectsUnsetCredentialsagainstda8e8e4'sconfig.go.Known gaps, not addressed here
Body()returns("", nil)for amultipart/*with an unusable boundary and for any media type that is neithermultipart/nortext/, so an empty notification is pushed to every target and answered250.forwardEmailbreaks after the first matching[[Recipients]]entry, so a message addressed to two matching entries notifies only the first and still answers250. Pre-existing.[CatchAll], is accepted with250and dropped. Rejecting atRCPTwould be more honest.smtp.ErrAuthRequiredis go-smtp's502 5.7.0where RFC 4954 §6 specifies530 5.7.0. Left on the library's own constant.Closes FMG-3