Use private vulnerability reporting on this repository. It is enabled, and it is the only channel.
No email address is published here on purpose. A security address that is not monitored is worse than none, because it silently absorbs reports; the GitHub advisory workflow notifies the maintainer, keeps the report private until it is fixed, and produces a citable advisory at the end.
Please report privately, rather than in a public issue, anything that could let a third party read credentials or mail contents, forge a message, or downgrade the transport. Ordinary defects — a wrong error message, a missing input — belong in a normal issue.
Expect an acknowledgement and, where the report is valid, an advisory rather than
a bare commit. KNOWN-DEFECTS.adoc explains why this project publishes its own
defects instead of waiting to be asked about them.
| Version | Supported |
|---|---|
v0.3.0 | Yes — current release |
v0.2.0 | No — superseded; implicit TLS only, no STARTTLS |
v0.1.0 | No — superseded; secure fails open |
main | No — not a release; during a release window it names assets that are not published yet |
Pin a tag or a commit SHA; never @main.
src/tls/Client.zig is a vendored copy of the Zig standard library's TLS
client, carried in-tree because the action builds a static binary against a
pinned toolchain.
Three facts about it, stated plainly because they are the most security-relevant properties of this action:
- It is 91 KB — about 69% of all Zig source in this repository. The majority of the code you run is TLS code this project did not write.
- It has no tests of its own. All 18 test blocks live in
src/smtp.zig(15) andsrc/message.zig(3).Client.zighas zero. - It is outside the proof boundary. The Idris2 specification covers
dot-stuffing (a theorem over all inputs) and the protocol state machine (a
Reflproof over the eight rows that exist). TLS is explicitly not covered at all — see "Scope of the proof, stated exactly" inKNOWN-DEFECTS.adoc.
The consequence: this action's formal claims are about the SMTP layer, not about the cryptography underneath it. A TLS defect here would be inherited from the upstream implementation, and a fix means re-vendoring rather than patching. Vendoring also means an upstream fix does not reach you automatically.
- Certificate verification is always on. There is no
ignore_certinput and there will not be one.dawidd6/action-send-mailhas one; parity would be a regression, and this is a deliberate incompatibility. - Transport selection is fail-closed. An unrecognised
securevalue is rejected outright rather than falling back to plaintext.secure: false(STARTTLS) upgrades in place per RFC 3207 and fails if the server does not advertiseSTARTTLS, rather than silently sending in the clear. Plaintext requires typingplaintext. - The post-upgrade EHLO is the one that counts. RFC 3207 §4.2 requires a
second
EHLOafter a STARTTLS upgrade, and the capability list it returns replaces the cleartext one. The cleartext list is unauthenticated — an active attacker can edit it — so it is never used to decide anything. - The password never reaches
argv. It is passed through the environment, so it cannot leak into a process listing on a shared runner. - The binary is pinned by SHA-256 in
action.yml, andrelease.ymlrefuses to publish unless a rebuild from source reproduces those hashes. The action ref you pin therefore determines the exact bytes that run. - CR/LF in headers is rejected, never sanitised. Header injection fails the step instead of being silently repaired into something the caller did not write.
These are not vulnerabilities; they are boundaries you should know before you depend on the action.
- Linux only. The released binaries are static
linux-muslbuilds. The action gates onuname -sand fails with a clear message on Windows and macOS runners. - Password mechanisms only: PLAIN and LOGIN. The mechanism is chosen from the
parsed EHLO capability list (issue #9) rather than assumed, and on the STARTTLS
path from the post-upgrade list, so an active attacker cannot steer mechanism
selection by rewriting the cleartext greeting. A server advertising neither
mechanism is refused before any credential byte is written — the failure names
what was on offer instead of putting a password on the wire to be rejected.
XOAUTH2is deliberately not implemented: it requires a token from an OAuth flow, which a repository secret cannot supply. - The AUTH LOGIN exchange is outside the proven state machine. The table holds
one
authrow expecting235; the two intermediate334challenges are driven in Zig and covered by tests, not by a theorem. The reply that decides success or failure is still checked against the table. SeeKNOWN-DEFECTS.adoc, "Scope of the proof". - No per-operation network deadlines. A whole-run watchdog bounds the entire
run instead. It is a deadline, not an idle timer: a server that dribbles bytes
slowly will run to the deadline rather than being cut off at the first stall.
See
BUSTFILE.adoc, BUST-2026-001 and BUST-2026-002, for why the per-operation form is unavailable in Zig 0.16.0. - Header-value validation is corpus-tested, not proved. An injection vector
outside the golden-vector corpus is not excluded by a theorem. A universal
theorem for
headerValueOkis tracked in issue #5.
This policy covers the action, the Zig binary and the release workflow in this repository. It does not cover your SMTP provider, your workflow secrets, or the contents of the mail you choose to send.