Only the latest published version receives fixes.
| Version | Supported |
|---|---|
| 1.x | ✅ |
Please do not open a public issue for a security problem.
Report it privately through GitHub's private vulnerability reporting, or by email to dolid.sasha@gmail.com.
Include, as far as you can:
- what an attacker can do, and what they need in order to do it;
- the affected version, and the runtime (Node.js, Bun or Deno) and its version;
- the transport or provider involved (SMTP server, Amazon SES, another HTTP provider);
- a minimal reproduction.
You can expect an acknowledgement within a few days, and an assessment of severity and a fix timeline once the report is confirmed.
The transport layer, the delivery events and their webhooks, the queue with its drivers, templates and the audit of states are built; suppression and the crypto-shredding of the audit are next. The principles all of it is designed around:
- Message content is untrusted data. Addresses, display names, subjects, headers, file names
and bodies can come from users. A CR or LF in any header field is rejected rather than encoded
through, so no input can add a header or a recipient, and the body is dot-stuffed so that no
line can end the SMTP
DATAearly. - The SMTP channel is secure by default. TLS certificates are verified. AUTH is never sent over a plain connection: when TLS is required and the server does not offer STARTTLS, or the upgrade fails, the send fails instead of falling back to plain text. Bcc recipients never appear in the headers of the message.
- Provider requests stay with their provider. Redirects are not followed, so credentials in a header never reach another host; a provider URL must be HTTPS (plain HTTP only to a loopback host); and an answer is read up to 4 MiB.
- Attachments do not read the machine by default. No file or URL is read for an attachment
but through
fromPathandfromUrlof./attachments, which the application calls.fromUrlchecks every URL, redirects included, withallowUrl; it refuses a redirect from https to http, sends its headers to the first origin only and bounds time and bytes.fromPathwith arootcompares real paths, so a symbolic link cannot lead out of it. - The default check of
fromUrlrefuses local addresses, written or resolved, but not DNS rebinding. It refuseslocalhost,*.localhostand an address in a loopback, private, link-local (the cloud metadata address among them), carrier-grade NAT, multicast, reserved, documentation or benchmarking range, and in the IPv6 forms that carry an IPv4 address (IPv4-compatible::/96, NAT6464:ff9b::/96and64:ff9b:1::/48, 6to42002::/16): an IP literal as written (isPublicUrl), and every address a host name resolves to. A name that answers the fetch with another address than it gave the check (DNS rebinding) passes: an application that fetches URLs its users give passes afetchthat checks the address it connects to. AnallowUrlof the application replaces the check, resolution included. - The sendmail command gets arguments, not a shell line. The recipients follow
--, none may start with-, and Bcc recipients go on the command line, never into the message. - Credentials and personal data stay out of logs. A transport shows
[REDACTED]in place of its SMTP password, OAuth2 tokens, provider key and DKIM private key when it is inspected, logged or serialized../oteland./loggerrecord addresses only when told to (recordSensitiveData,recordMessage); otherwise the addresses that error text names, such as the recipient of an SMTP rejection, are redacted. A receipt returned to the application keeps its full error text: logging it is the application's to scrub. - Webhooks are verified before they are read. A delivery event is read only from a request whose signature verifies, or, where the provider signs nothing, whose Basic credentials or token match, compared in constant time; within a window of time where a time is signed, and once by its id. An SNS message (SES, Scaleway) is taken only from a listed topic, signed with the certificate of an SNS host or of the trust chain the application pins.
- A job of the queue is untrusted when it is read. Whoever can write to its store (Redis,
MongoDB) could have written it, so the worker checks its shape and its hash before it acts on
it, and
checkJobcan refuse a sender. The hash, a SHA-256, tells a job that changed but not one that was forged, since a writer can recompute it: withsigningKeys, each job carries an HMAC-SHA256 under the application's key, and the worker sends no job that one of its keys did not sign. A job names no file or URL: an attachment is its bytes or a reference that the application'sresolveAttachmentreads. A job's data is stored as it is, a token of a reset link among it, unless acodecof the application's seals it (an envelope encryption: the store then holds the version, the hash and the signature in clear and the rest encrypted); once a job is done with, every driver drops its message (retainData, the failed jobs' kept for a retry by hand), so a reset link does not wait in the store and its backups. The id of a job is a hash of its key, which goes into the audit and the logs: a key names what the message tells of, never an address or a secret, andidSecretderives the id with an HMAC so that it cannot be traced back. The store is the application's to restrict (Redis ACLs and TLS, MongoDB roles). - Templates are named, never pathed. A template, include, partial or layout is read only
through the loader of the renderer, by a name with no
..or leading/; the file loader compares real paths, so a symbolic link cannot lead out of its directory. No engine reads a file itself, MJML'smj-includeis off unless asked for, output is escaped by default (Liquid included), and the CSS inliner fetches no remote stylesheet. Templates are code: EJS, Eta, Pug and Nunjucks run JavaScript, so templates that tenants write belong to Liquid, Handlebars or Mustache. - The audit holds no recipient. An event names its message by
correlationId, with the hashes of its job and message; the text of an error in it has its addresses redacted. - The audit log can forget a person (planned). Recipients are stored as a keyed hash and an encrypted block whose key can be destroyed, so a request to erase someone's data does not require editing an append-only log.
- Drivers are the application's. A queue or database driver passed in (BullMQ, MongoDB) is
trusted the same as any other module the application
requires; its connection security is the application's to configure.