Repository navigation
Assurance Case
Source of truth: ASSURANCE-CASE.md, kept in the repository and copied here. Re-copied from master 92b8f9f63 on 26 September 2026, brought up the same day to the document as the tag v6.3.4 (51e2dbda0) has it, and re-copied on 5 October 2026 from the document as the tag v6.3.6 (e3c448aa6) has it (ASSURANCE-CASE.md at v6.3.6): the GitHub workflows are in the git history, the release jobs of
.gitlab-ci.ymlhave made the releases since 6.3.5, and the live update trusts the group's path, which the project moved to on 29 September 2026, with the project's id. 6.3.4 carries an Authenticode signature and no Sigstore bundle, so no server's live update installed it: it was installed by hand (Upgrading Guide), and its download is checked against its SHA-256 sums file, signed with the release tags' SSH key (Security Policy, Verifying a release). Re-copied on 18 October 2026 from the document as the tag v6.3.7 (31d656c0c), the release, tagged and published on 17 October 2026, has it (ASSURANCE-CASE.md at v6.3.7), which changes it in one place, brought to the same commit on 17 October 2026 before it was tagged: the residual risk Bus factor is still effectively 1 says that a second maintainer's approval of every merge request is the rule, that of the nine batches landed by 14 October 2026 one had it and eight landed on the owner's override, and that since 13 October 2026 the second maintainer holds no role in the GitLab project. Re-copied on 19 October 2026 from the document asmasterhas it from batch49, f14412d39 (ASSURANCE-CASE.md at f14412d39), in which 6.3.7, the latest release, is reflected as at the tag; what it changes since the tag, most of it on the project's copy on GitHub since 18 October 2026: the status note says that the accountProgressiverobot, which GitHub suspended on 18 September 2026, stays suspended, and that the project is published on GitHub again, at https://github.com/RainbowWizard-coder/hMailServer, as a second home, with GitLab canonical; every release from 6.0.0-B1 to 6.3.7 is copied there with the same files byte for byte, and one published after the copies of 18 October 2026 is immutable there; no pipeline of the project's runs on GitHub, where GitHub Actions runs one workflow of the project's,.github/workflows/scorecard.yml, beside GitHub's own CodeQL default setup, whose findings the maintainers read; the oldsbom.ymlandsign-release.ymlin the trees of the old tags ran once there on 18 October 2026, on the copies of the releases from 6.2.3 to 6.2.22-pre6, what they wrote was undone, and both have been disabled since, the update path not exposed; the Control Panel's coverage is collected on neither forge; and a correction may come as a pull request on GitHub, which a maintainer lands through a GitLab merge request. The chart of the trust boundaries in section 4, drawn in place of the document's text figure, the sentence on a forward proxy that ends section 6's paragraph on the update path, and the three parts that close section 7 - the traceability table, the chart of which claim each weakness class answers and the table of the residual risks, ranked by what an operator can do about them - are this wiki's own additions, made to this copy on 8 September 2026. Re-copied on 20 October 2026 from the document asmasterhas it from batch50, 0c0b6bae7 (ASSURANCE-CASE.md at 0c0b6bae7), which changes it in one place: its paragraph onSigstoreVerifier, under Supply-chain and integrity weaknesses (CWE-494), says that GitHub's token service signed the releases from 6.2.19 to 6.3.3, as claim C7 and the Security Policy say, where it said every release up to 6.3.3. The traceability table's row for anyone downloading says the same of the Sigstore signatures recorded in Rekor, and that the SPDX and CycloneDX SBOMs are those of the releases from 6.2.3. Re-copied on 21 October 2026 from the document asmasterhas it from batch51, 5e49abf06 (ASSURANCE-CASE.md at 5e49abf06), which changes it in one place: the status note says that two workflows of the project's in the tree are for the copy on GitHub alone,.github/workflows/scorecard.yml, which scores it with OpenSSF Scorecard, and.github/workflows/codeql.yml, which analyses its code with CodeQL on each push tomaster, on each pull request and weekly in place of GitHub's CodeQL default setup, which has done the same since 18 October 2026 with no workflow file and which Scorecard's SAST check does not count, and that neither builds, signs or publishes anything of a release, where it said that Scorecard's was the one workflow there and that GitHub Actions was enabled for it and for the default setup. Re-copied on 24 October 2026 from the document asmasterhas it from batch54, 9336fdd59, onmastersince 24 October 2026 (ASSURANCE-CASE.md at 9336fdd59), which changes it in one place: the status note says that GitHub's immutable-releases setting is on there, that a release published there with it on keeps its files and its tag as published, though its notes can still be edited, and so do the copies of 6.3.6 and 6.3.7, published after it was switched on during the copying of 18 October 2026, and that the copies of the releases before them, published before it, can still be changed, where it said that a release published after the copies of 18 October 2026 cannot be changed and that the copies themselves, published before the setting was switched on, can. The document's links to the repository's other documents point at them as 9336fdd59 has them, or at this wiki's copy where there is one. Re-copied on 26 October 2026 from the document asmasterhas it from batch55, 36bd59faa, onmastersince 26 October 2026 (ASSURANCE-CASE.md at 36bd59faa), which changes it in one place: the residual risk Coverage is below the bar says that thelinux-coveragejob runs the Linux measure on GitLab weekly, under the schedule describedcoveragemade on 24 October 2026 (Sundays at 03:40 UTC), and that its first run, the schedule played by hand that day, pipeline 2983513656, reached 57.58% of the lines, 55.47% of the regions and 46.10% of the branches, GitLab keeping the line figure as the project's coverage, where it said that the job ran on a weekly schedule the owner had yet to create and had not run on GitLab. The row Coverage is below the bar of the table of residual risks, this wiki's own, gives that first run's figures beside the earlier ones. The document's links to the repository's other documents point at them as 36bd59faa has them, or at this wiki's copy where there is one.
This document states what hMailServer claims about its own security, and the argument and evidence for each claim. It is written for someone deciding whether to run this on the public internet, and for anyone auditing that decision.
It is deliberately not a marketing document. Where a claim is limited, the limit is stated. Where a risk is accepted rather than eliminated, it is named in Residual risk.
Status since 18 September 2026. The project's GitHub account,
Progressiverobot, was suspended on 18 September 2026 and stays so, and the
project's home has been
GitLab since 22 September
2026 (in the group hmailserver since 29 September 2026). Since 18 October
2026 the project is published on GitHub again, at
https://github.com/RainbowWizard-coder/hMailServer, as a second home, and
GitLab is canonical: the project's pipelines run there and nowhere else, and a
release is built, signed and published there first.
Claim C7 rests on the release chain that ran on GitHub Actions
(sign-release.yml, sbom.yml, verify-binary-provenance.yml, in the git
history at v6.3.3). It holds for the releases that chain signed, 6.2.19 to
6.3.3, whose Sigstore bundles verify without GitHub. 6.3.4 was built and signed
by hand and carries no Sigstore bundle. The chain's replacement, the release jobs of .gitlab-ci.yml
(ContinuousIntegration.md),
made 6.3.5 on 29 September 2026: every file of it carries a Sigstore bundle
signed by the tag's pipeline, which verifies without GitHub, so the claim holds
for 6.3.5 and for each later release the jobs make. Since 18 October 2026 the
releases are published on GitHub too - every one from 6.0.0-B1 to 6.3.7 that
day, and each later one after GitLab has published it - with the same files
byte for byte, each checked against the SHA-256 that GitLab's package registry
records for it, under the same names but for the per-distribution .deb
files, whose ~ GitHub writes as .. GitHub's immutable-releases setting is on
there: a release published there with it on keeps its files and its tag as
published, though its notes can still be edited, and so do the copies of 6.3.6
and 6.3.7, published after it was switched on during the copying of 18 October
2026; the copies of the releases before them, published before it, can still be
changed.
Likewise the CodeQL, Dependabot and dependency-review checks cited below ran on
GitHub until 18 September 2026 and do not run on GitLab. No pipeline of the
project's runs on GitHub. Two workflows of the project's are in the tree, for
the GitHub copy alone: .github/workflows/scorecard.yml, which scores it with
OpenSSF Scorecard and publishes the result to scorecard.dev and to that
repository's code scanning, the job's two write permissions being for those two
publications alone; and .github/workflows/codeql.yml, which analyses its code
with CodeQL on each push to master, on each pull request and weekly, and whose
one write permission is the upload of its results to code scanning. It takes the
place of GitHub's CodeQL default setup, which has done the same since 18 October
2026 with no workflow file, and which Scorecard's SAST check does not count,
since it sees the default setup only through the check runs of merged pull
requests, of which the copy has none; GitHub takes no workflow's CodeQL analysis
while the default setup is on, so the default setup is switched off when the
workflow reaches master
(ContinuousIntegration.md).
Neither builds, signs or publishes anything of a release. GitHub Actions is
enabled there for those two, for the default setup while it is on, and for the
jobs GitHub runs itself to build the repository's dependency graph; it allows
GitHub's own actions and ossf/scorecard-action alone, each pinned by a full
commit. The maintainers read CodeQL's findings, nothing more.
Old workflows ran there once. The tags from v6.2.3 to v6.3.3, and
build-inputs-1, still carry the chain's sbom.yml in their trees, and those
from v6.2.19, and build-inputs-1, its sign-release.yml, and GitHub ran them
on the publication of
the copies of the releases from 6.2.3 to 6.2.22-pre6 on 18 October 2026
(sign-release.yml failed for v6.2.19): they wrote SBOMs and signature bundles
of their own over 62 of the copied files and added three. Each of the 62 was put
back from the copy checked against GitLab's SHA-256, the three were removed, and
every release was checked again; the runs are on the repository's Actions page.
They ran for no later copy - those of 6.2.23 Alpha 1 to 6.3.3 started neither -
and both have been disabled there since 18 October 2026; a tag after 6.3.3
carries neither. The update path was not exposed: its feed is GitLab's,
and SigstoreVerifier, below, trusts the GitHub signer only under the old
repository's path, Progressiverobot/hmailserver, and only for certificates
issued before 18 September 2026, and a bundle those runs made meets neither.
Since 25 September 2026 GitLab CI has
replacements, not yet blocking - cppcheck, clang-tidy and Semgrep for CodeQL,
Trivy with OSV for the dependency checks - and Security Policy says
what runs.
Contents:
- What the software is
- Security requirements — the claims
- Threat model
- Trust boundaries
- Argument: secure design principles are applied
- Argument: common implementation weaknesses are countered
- Evidence
- Residual risk
hMailServer is a mail server for Windows: an SMTP, IMAP and POP3 implementation, with a management COM API, a Control Panel GUI, and optional REST API, metrics and web-services listeners. It stores mail on the local filesystem and configuration and accounts in a SQL database. It talks to the public internet by design, on ports that are open to anyone.
That last point sets the security posture: the primary interfaces are exposed to unauthenticated strangers, and most of them must accept input before any authentication has happened.
These are the properties the project claims and argues for below.
| # | Claim |
|---|---|
| C1 | An unauthenticated network peer cannot execute code, corrupt memory, or crash the service through the mail protocols. |
| C2 | An unauthenticated peer cannot read, modify or delete mail belonging to an account, nor authenticate as one. |
| C3 | Credentials at rest cannot be practically recovered from a stolen database. |
| C4 | Credentials in transit are not exposed to a network attacker, and are never sent to an unverified peer. |
| C5 | Untrusted message content cannot alter the meaning of a database query, a filesystem path, or a protocol exchange with a third party. |
| C6 | An authenticated ordinary user cannot reach another user's mail, nor gain administrative capability. |
| C7 | A user can verify that the software they downloaded is the software the project published. |
What is explicitly not claimed is listed in Security Policy under Out of scope, and repeated in Residual risk.
| Actor | Position | Capability assumed |
|---|---|---|
| A1 — Internet stranger | Can open TCP connections to any listener | Arbitrary bytes, arbitrary volume, arbitrary timing, protocol non-compliance |
| A2 — Sending MTA | Delivers mail inbound | Everything A1 has, plus fully-controlled message content, headers and MIME structure |
| A3 — Network attacker | On-path between this server and a peer | Observe, modify, drop, replay; present a certificate; downgrade attempts |
| A4 — Authenticated user | Holds valid credentials for one account | Everything A2 has, plus authenticated IMAP/POP3/SMTP submission |
| A5 — Malicious upstream | Controls a DNS answer, a TLSA record, or a remote MTA | Poisoned resolution results, hostile TLS peer behaviour |
| A6 — Local attacker | Has a foothold on the host | Read files the service can read, observe process memory |
Administrators are not modelled as adversaries: an administrator can run event scripts and change configuration by design, so administrative access is equivalent to control of the service. This is stated in Security Policy and is a deliberate boundary, not an oversight.
Mail in transit and at rest; account credentials; TLS private keys; the configuration database; the availability of the service itself.
- Protocol parsers (SMTP, IMAP, POP3) — reachable pre-authentication by A1 and A2. The largest and most dangerous surface.
- MIME and header parsing — reachable by anyone who can send mail.
- TLS layer and certificate handling — reachable by A1, A3 and A5.
- Anti-spam and anti-virus paths, including DNS lookups and handoff to external scanners — driven by attacker-supplied content.
- DNS resolution, including DNSSEC, DANE and MTA-STS — influenced by A5.
- The persistence layer, reached indirectly with attacker-influenced data.
- REST API, metrics and web-services listeners, where enabled.
Each boundary below is a place where data changes trust level and must be checked.
flowchart TB
subgraph actors["Actors"]
A1["A1 Internet stranger"]
A2["A2 Sending MTA"]
A3["A3 Network attacker, on-path"]
A4["A4 Authenticated user"]
A5["A5 Malicious upstream:<br/>DNS, TLSA, remote MTA"]
A6["A6 Local attacker"]
ADMIN["Administrator -<br/>NOT modelled as an adversary"]
end
B1["B1 - Network to process<br/>TCP and TLS termination<br/>Server/Common/TCPIP"]
B2["B2 - Bytes to protocol grammar<br/>SMTP, IMAP, POP3 command parsers<br/>MIME and header parsing"]
B3["B3 - Anonymous to authenticated<br/>credential check against a stored,<br/>key-stretched hash"]
BO["Business objects - per account<br/>Server/Common/BO"]
B4["B4 - Program to database<br/>parameterised statements only"]
B5["B5 - Process to external programs<br/>ClamAV, SpamAssassin, event scripts, DNS<br/>untrusted in BOTH directions, every call bounded"]
B6["B6 - Management plane<br/>the COM API, the single seam"]
A1 --> B1
A2 --> B1
A3 --> B1
A4 --> B1
A5 -.->|"poisoned answers,<br/>hostile TLS behaviour"| B5
A6 -.->|"reads what the service can read,<br/>observes process memory"| BO
ADMIN --> B6
B1 --> B2
B2 --> B3
B3 --> BO
B6 --> BO
BO --> B4
BO --> B5
- B1 — Network to process. Everything arriving is hostile until parsed. TLS is terminated here; peer verification happens before application data moves (see C4).
- B2 — Bytes to protocol grammar. Each command is accepted only in the form its grammar defines; anything else is rejected rather than interpreted generously.
- B3 — Anonymous to authenticated. Crossing it requires a credential check against a stored key-stretched hash. Below this line, an identity exists and every mail access is scoped to it.
- B4 — Program to database. Crossed only through parameterised statements; content never becomes query structure.
- B5 — Process to external programs and the network. Content handed to ClamAV, SpamAssassin, event scripts or DNS is untrusted in both directions, and every such call is time-bounded.
- B6 — Management plane. The COM API is the single seam through which configuration and management happen; it sits inside the administrative trust boundary described above.
Economy of mechanism / single seam. All configuration and management flows
through one interface — the COM API in Server/COM/ — used by the Control
Panel, the regression suite and external scripts alike. There is no second,
lightly-tested management path to audit. This is the layering documented in
Architecture.
Complete mediation. Mail access is mediated per account below B3; the persistence layer is reached only through the business objects, not directly from protocol code.
Fail-safe defaults. The server refuses to store a new secret under a
scheme that is not a password hash: configuring PreferredHashAlgorithm to
None, Blowfish or MD5 is logged as a misconfiguration and silently upgraded to
PBKDF2 rather than honoured. TLS peer verification is on by default, and a
verification failure fails the handshake rather than downgrading the
connection.
Least privilege, explicitly bounded. The administrative boundary is declared rather than assumed: administrators can run scripts and change configuration by design, and reports predicated on already having administrator access are out of scope. Drawing the line publicly is itself a design decision — it tells operators exactly what administrative access is worth to an attacker.
Defence in depth. No single control is relied on. Transport security is layered (STARTTLS and implicit TLS, DANE, MTA-STS, per-route TLS policy); content is filtered by more than one mechanism; and memory-safety risk is attacked from several directions at once (see §6).
Separation of secrets from code and configuration. TLS private keys are files referenced by path; password hashes live in the database; the optional server-wide pepper is an INI setting. No credential is compiled in.
Cryptographic agility. Password hashing (SHA256 / PBKDF2 / Argon2id), the TLS cipher list, and the TLS key-exchange groups are all configurable, so a broken primitive can be replaced by configuration rather than by waiting for a release.
SQL is exclusively parameterised, as a stated project rule in Contributing: "Use parameterised SQL exclusively — never build SQL strings manually." Message and header content therefore never becomes query structure. The rule is enforced at review, and the persistence layer offers parameterised interfaces as the path of least resistance.
This is C++, so this class cannot be argued away — it is attacked from four directions:
-
Compiler-enforced hygiene. The native build treats warnings as errors
(
TreatWarningAsError, MSVC/WX), so a new warning stops the build. -
Platform hardening.
/GSbuffer security checks, DEP (/NXCOMPAT) and ASLR (/DYNAMICBASE) are active on the shipped x64 binaries. - Static analysis. CodeQL analyses the C# tools on every push and pull request, and the C++ server on every push to master, weekly and on demand (not on pull requests - a change to the protocol parsers is analysed by dispatching the workflow against the branch before merging).
-
Fuzzing. A libFuzzer harness suite lives under
fuzz/, with a seed corpus regenerated from the real test messages (make-corpus.ps1; not committed), a MIME token dictionary and a committed regression directory of fixed findings, aimed at exactly the pre-authentication parsers that A1 and A2 can reach.
SMTP line-terminator handling is deliberately restrictive. Bare-LF spellings of the terminator are accepted only when Allow incorrect line endings is enabled, and even then are recognised only at the very end of the buffer with anything behind one discarded — the CVE-2023-51764 SMTP-smuggling rule. This behaviour is pinned by its own regression test, so it cannot be relaxed by accident.
Passwords are stored as per-user salted, key-stretched hashes — PBKDF2 by
default, Argon2id available — with salts from OpenSSL RAND_bytes and an
optional HMAC pepper mixed into Argon2id hashes. A minimum acceptable scheme
can be configured so legacy weak hashes stop being accepted for
authentication. SCRAM-SHA-256 is supported, and TOTP is available as a second
factor.
All security-relevant randomness comes from OpenSSL's CSPRNG (RAND_bytes),
with every call checked for failure: password-hash salts, SCRAM nonces, TLS
session-ticket keys and IVs, app passwords, TOTP secrets, REST API key
material and DNS/DNSSEC transaction IDs. Non-cryptographic generators appear
only in MIME part-boundary strings and OpenTelemetry trace identifiers,
neither of which is a security mechanism. A previous use of rand() for a
DNS-adjacent value was identified and replaced.
Outbound TLS sets verify_peer with verify_fail_if_no_peer_cert and
installs a verification callback that checks the chain and the expected
hostname, with a DANE verifier where a TLSA record applies. Because this is
installed before the handshake completes, credentials cannot be sent to an
unverified peer — which is claim C4.
Availability is treated as a security property, not a performance one. Several unbounded waits were found and fixed as defects in this fork — database-pool acquisition, event scripts, the custom virus scanner, the delivery-side ClamAV read/write timeout, and spam-test DNS lookups, which previously had no application-level timeout at all. Every crossing of boundary B5 is now time-bounded.
Every release asset is signed with Sigstore cosign (keyless) and verified by the workflow before upload, giving claim C7. Dependencies are enumerated in an SBOM, monitored by Dependabot, and gated by dependency review. Every vendored binary is inventoried with a SHA-256 and its provenance, and an unlisted or changed binary fails the build — closing the "a DLL appeared and nobody noticed" gap that a diff review cannot catch.
New in 6.2.28, the server's own update path applies the same verification. UpdateChecker reads the release feed and
SigstoreVerifier checks the installer against its .cosign.bundle in-process -
the file's SHA-256 is the one signed and the one Rekor recorded, the certificate
chains to the embedded Sigstore trust root, and its identity must be one of this
project's two release signers, whole: the sign-release.yml workflow issued by
GitHub's token service, which signed the releases from 6.2.19 to 6.3.3 and is trusted
only for certificates issued before the account's suspension on 18 September
2026, or this project's GitLab CI pipeline issued by GitLab's, for the v* tag
of the release being installed, which signs the releases after, from 6.3.5 -
at the path the project began at, which signed 6.3.5, and at the path of the
group hmailserver it moved to on 29 September 2026, which signs every release
from 6.3.6 and where the certificate must also record the project's id, so that
the servers built before the move take what is signed after it. A GitHub-signed file is installed only as a release up to
6.3.3, and on Windows the installer's own version resource must name the release
it is installed as - so a substituted installer, or a genuine older one offered
as a newer release, is refused before it is run. On Windows the Updates folder
that holds it admits changes from SYSTEM, the Administrators and the service
alone, and the update helper runs the installer, and the rollback image, only
with the SHA-256 the server read when it last checked them, through a handle it
holds until they have finished. The feed it reads is
https://updates.progressiverobot.com, served by GitLab Pages from the project's
releases (hmailserver/docs/UpdateFeed.md). Where the server reaches the
network through a forward proxy ([Settings] HttpProxy, also new in 6.2.28), an
https target is reached by CONNECT and the TLS handshake runs inside the tunnel
under the same certificate verification as a direct connection, so the proxy sees
the name it was asked for and nothing after it; and the release's own signature,
not the transport, is still what is trusted.
| Claim | Where the evidence is |
|---|---|
| C1 |
fuzz/ harnesses; the CodeQL workflow up to 18 September 2026, and the static-analysis and semgrep jobs of .gitlab-ci.yml since; /WX in hMailServer.vcxproj; the regression suite under hmailserver/test/RegressionTests
|
| C2 | Authentication path and per-account scoping in Server/Common/BO/; regression tests for authentication |
| C3 |
Server/Common/Util/Hashing/HashCreator.cpp; IniFileSettings.cpp hash-scheme handling |
| C4 |
Server/Common/TCPIP/TCPConnection.cpp verify mode and callbacks; SslContextInitializer.cpp
|
| C5 | Parameterised-SQL rule in CONTRIBUTING.md; persistence layer; SMTP terminator tests |
| C6 | Account scoping in the BO layer; administrative boundary in SECURITY.md
|
| C7 | Up to 6.3.3, the GitHub workflows sign-release.yml, verify-binary-provenance.yml and sbom.yml (git history, v6.3.3); from 6.3.5, the release jobs of .gitlab-ci.yml (build/ci/release-chain.py, release-sign.sh, release-verify.sh); SigstoreVerifier.cpp. That a published binary corresponds to the published source: the reproducible builds, RELEASE.md step 8 and the linux-repro job (build/ci/linux-repro.sh; ReproducibleBuilds.md) |
Release validation is not a smoke test: the full regression suite runs against
the exact binary being shipped, with live SpamAssassin and ClamAV (real EICAR
detection), DMARC, SPF and DKIM evaluated against a DNS zone the suite serves
itself (Shared/SuiteDns.cs, so a run cannot depend on the public DNS), and TLS
1.2 and 1.3 handshakes end to end. Nothing is mocked except the resolver, and
nothing is skipped.
Read a row as a sentence: this actor reaches this surface, which is defended at this boundary, in support of this claim, and the evidence is here.
| Actor | Surface it reaches | Boundary that defends it | Claim | Principal control | Evidence |
|---|---|---|---|---|---|
| A1 Internet stranger | SMTP, IMAP, POP3 command parsers; TLS handshake; optional listeners | B1, B2 | C1 | Grammar-strict parsing; bounded reads; absolute session ceilings; /guard:cf; /WX
|
fuzz/, CodeQL, the regression suite |
| A2 Sending MTA | Everything A1 reaches, plus MIME structure, headers, attachment names | B2, B5 | C1, C5 | The MIME fuzz harnesses; CrashOracle; bounded scanner calls |
fuzz/, Common/Util/CrashOracle.cpp
|
| A3 Network attacker | The TLS layer, in both directions | B1 | C4 |
verify_peer with verify_fail_if_no_peer_cert; DANE; MTA-STS; TLS 1.2 floor |
TCPConnection.cpp, SslContextInitializer.cpp
|
| A4 Authenticated user | Everything A2 reaches, plus authenticated IMAP, POP3 and submission | B3, B4, B6 | C2, C6 | Per-account scoping in the BO layer; ACLManager as the sole folder-access decider |
Common/BO/, build/check-authz-choke-point.py
|
| A5 Malicious upstream | DNS answers, TLSA records, remote MTA behaviour | B5 | C4, C5 | In-process DNSSEC validation; DANE matching; MTA-STS policy caching; every external call time-bounded |
DnssecResolver.cpp, DaneVerifier.cpp, SMTP/TlsPolicy.cpp
|
| A6 Local attacker | Files the service can read; process memory | outside B1–B5 | C3 | PBKDF2 by default, Argon2id and scrypt available; an optional server-wide pepper kept outside the database |
Hashing/HashCreator.cpp, IniFileSettings.cpp
|
| Anyone downloading | The published artefacts | — | C7 | Signed annotated tags; keyless Sigstore signatures recorded in Rekor from 6.2.19 to 6.3.3; for 6.3.4, built by hand, a SHA-256 sums file signed with the release tags' SSH key; from 6.3.5 a Sigstore bundle on every file, signed by the tag's GitLab pipeline; SPDX and CycloneDX SBOMs from 6.2.3, and from 6.3.4 one beside every compiled file; a reproducible binary hash |
sign-release.yml, sbom.yml, verify-binary-provenance.yml (git history, v6.3.3); the release jobs of .gitlab-ci.yml; SECURITY.md Verifying a release; build/asset-sbom.py
|
Section 6 argues eight weakness classes. This is which claim each one serves and where the control lives.
flowchart LR
subgraph cwes["Weakness classes argued in section 6"]
W89["CWE-89<br/>Injection"]
W119["CWE-119, 787, 125<br/>Memory corruption"]
W444["CWE-444<br/>Protocol smuggling"]
W287["CWE-287, 916<br/>Broken auth and<br/>credential storage"]
W338["CWE-338<br/>Weak randomness"]
W295["CWE-295<br/>Improper certificate<br/>validation"]
W400["CWE-400<br/>Uncontrolled resource<br/>consumption"]
W494["CWE-494<br/>Supply-chain and<br/>integrity"]
end
subgraph claims["Claims"]
C1["C1 no code execution,<br/>corruption or crash"]
C2["C2 no unauthenticated<br/>mail access"]
C3["C3 credentials at rest"]
C4["C4 credentials in transit"]
C5["C5 content cannot alter<br/>a query, a path or an<br/>exchange"]
C6["C6 no cross-user or<br/>privilege escalation"]
C7["C7 verifiable download"]
end
W89 --> C5
W119 --> C1
W444 --> C5
W444 --> C1
W287 --> C2
W287 --> C3
W287 --> C6
W338 --> C3
W338 --> C2
W295 --> C4
W400 --> C1
W494 --> C7
An assurance case that lists risks without saying who can act on them is only half useful.
| Residual risk | Who can reduce it | What that would take |
|---|---|---|
| Memory safety is mitigated, not guaranteed | The project | More fuzz targets — Sieve, ManageSieve and the SMTP command parser are named as untrusted and unwired in Fuzzing |
| Coverage is below the bar | The project | Coverage repeated for each release and raised towards 80%: the native server's lines were 73.48% over the Windows suite on 15 September 2026 and 49.01% (38.87% of branches) on Linux on 26 September 2026, and 57.58% (46.10% of branches) in the linux-coverage job's first run on GitLab, pipeline 2983513656 on 24 October 2026, neither build/coverage-windows.ps1 nor the linux-coverage job is a release step, and the .NET Control Panel's coverage, collected by CI on GitHub until 18 September 2026, is collected on neither forge since |
| Reproducibility shown on one toolchain only | A third party | An independent rebuild on a second machine with independently built OpenSSL, Boost and libpq |
| CFG is a mitigation, not a boundary | Nobody | A defect that overwrites data rather than a pointer is unaffected by /guard:cf, by design |
| Bus factor is 1 | The project, and volunteers | Governance describes how someone becomes a maintainer |
| Administrators are inside the boundary | The operator | Treat administrative credentials as equivalent to host credentials |
| Operator-chosen weakening is possible | The operator | Do not run plaintext listeners or legacy hash schemes. The server warns, but it obeys |
| The installer was not Authenticode-signed before 6.3.1 | The project | Closed 11 September 2026: the installer is signed with Azure Artifact Signing (the service formerly called Trusted Signing) from 6.3.1, with an RFC 3161 countersignature, naming Progressive Robot Ltd. 6.3.0 and earlier stay unsigned: every release up to 6.3.3 whose files GitLab has is re-published there, and since 18 October 2026 copied to GitHub, with its original files and signatures, byte for byte (6.2.0 and 6.0.0-B4 are notes-only). SmartScreen still warns until reputation accumulates, which a new installer every few weeks never does; the Sigstore bundle remains the check that proves provenance, for every asset that carries one (Security Policy, Verifying a release, says which) |
Stated plainly, because an assurance case that claims no residual risk is not credible.
- Memory safety is mitigated, not guaranteed. This is a large C++ codebase with a pre-authentication attack surface. The controls in §6 raise the cost of exploitation; they do not make it impossible.
-
Coverage is below the bar. The native server's line coverage over the
whole Windows regression suite was 73.48% on 15 September 2026 (67,790 of
92,260 lines under
hmailserver/source/Server), by OpenCppCoverage 0.9.9 against the 6.3.3 shipping binary run as a console server (CHANGELOG.md, 6.3.4, Build and tests;test_statement_coverage80in Roadmap.md) - lines only, the tool having no branch mode. Branches are counted on Linux: on 26 September 2026 the Linux build under clang's source-based coverage, driven by the Linux regression suite and the tester programs, reached 49.01% of its lines, 47.23% of its regions and 38.87% of its branches (build/ci/linux-coverage.sh; the suite in two runs whose counters were merged - the whole of it before 29 fixtures were linked into it that day, and those fixtures on their own). Thelinux-coveragejob runs the same measure on GitLab weekly, under the schedule describedcoveragemade on 24 October 2026 (Sundays at 03:40 UTC); its first run, the schedule played by hand that day, pipeline 2983513656 over the whole suite of that day, reached 57.58% of the lines, 55.47% of the regions and 46.10% of the branches; GitLab keeps the line figure as the project's coverage (ContinuousIntegration.md, The coverage schedule). That is a floor rather than the Windows figure's equal: the Linux suite runs only the fixtures its REST shims can drive, the Linux build has no COM layer, and llvm-cov counts every line of a function where OpenCppCoverage counts the lines an optimised build keeps. Every figure is below the 80% the OpenSSF silver level asks of statements, and the branch figure far below the 80% gold asks of branches. Both scripts are in the repository -build/coverage-windows.ps1, andbuild/ci/linux-coverage.sh, which thelinux-coveragejob runs weekly - but neither is a release step, and the Windows script has not run from the tree, so the figures are not repeated for each release. The .NET Control Panel's coverage was collected by CI on GitHub (ci.yml) until 18 September 2026 and is not collected on GitLab yet, nor on GitHub since then. -
Reproducibility has been shown on the project's own machines only.
(hmailserver/docs/ReproducibleBuilds.md
has the procedure and the evidence.) Since 22 August 2026 two clean Release
builds of the same tree produce a byte-identical
hMailServer.exe(/Brepro,/d1trimfile,/pdbaltpath,OPENSSL_NO_FILENAMES; RELEASE.md step 8 repeats the check for every release and the notes carry the hash); on 26 September 2026 two builds of40c7d3e0c, the commit v6.3.4 was first tagged at that day, were bothc42e31bb.... The same day a build of that commit from a second checkout at another path was not identical:/d1trimfiletrimmed only the project file's directory, and sixteen source paths and 31 Boost header paths stayed absolute. With the checkout's root and the libraries' root trimmed, two checkouts at two paths build the same executable (53f9801f...), and step 8 now builds once more in another checkout. 6.3.4, built before the change, reproduces only from its own paths. On Linux thelinux-reprojob buildslinux-build's commit a second time from another path with other file modes, in another build directory, umask and time zone, and requires the binary and the x86-64.deband.rpmto be byte for byte identical before a release (the packages became so the same day, when it found their directories' modes following the umask, the.deb's maintainer scripts the checkout's modes and the.rpmstamping the clock); the AArch64.deband.rpma release carries from 6.3.6 are built once, by the package jobs, and not compared; the image's Ubuntu packages are the archive's of the day it was built, so it pins the build, not the toolchain's history. What has not happened is an independent rebuild by someone outside the project, with an independently built OpenSSL, Boost and libpq, so a third party's ability to confirm a released binary still rests on matching this toolchain exactly. Artifact signatures establish who published a binary; the hash lets anyone with the toolchain check that it corresponds to the source. -
Control Flow Guard is on, and its cost is small but real. Since 6.2.23
Alpha 2 the server is compiled and linked with
/guard:cf(Release and Debug), so an indirect call through a corrupted function pointer or vtable - the natural target of a memory-safety defect in the SMTP, IMAP, POP3 or MIME parsers - terminates the process rather than transferring control. The cost was measured rather than assumed: the full 1,838-test regression gate on the CFG build took 31 minutes on the build machine, against the 55 or so the previous release's gates took on the same machine, so whatever CFG costs on the indirect-call-heavy MIME path is below the run-to-run variance of the suite. The executable grew by about 97 KB of guard tables. CFG is a mitigation, not a boundary: a defect that overwrites data rather than a pointer is unaffected. -
Bus factor is still effectively 1. Since September 2026 the project has
two maintainers, one from Progressive Robot Ltd and one independent of it,
and from the first change after 6.3.4 the rule is that every merge request
is approved by one before the other merges it; but of the nine batches
landed by 14 October 2026 one had that approval and eight landed on the
owner's override, and since 13 October 2026 the second holds no role in the
GitLab project. Release tagging is the owner's alone (the only person with
GitLab's Maintainer role, and the one key
.gitlab/allowed_signersmay hold), security triage rests on him, and so far only one person has a record of changes to the codebase — see Governance. A slow security response remains a realistic failure mode. - Administrators are inside the boundary. Anyone who can administer the server can run code on it. Operators must treat administrative credentials as equivalent to host credentials.
- Operator-chosen weakening is possible. The server permits plaintext listeners and legacy hash schemes for compatibility. It warns, but it obeys.
This assurance case should be revisited whenever the threat model changes — a
new listener, a new external integration, a new authentication mechanism — and
at minimum reviewed against the codebase once a year. Corrections are welcome
through the normal merge-request process described in
Contributing, or as a pull request on GitHub, which a
maintainer lands through GitLab as a batch* branch and a merge request,
keeping its author and sign-off.
hMailServer 6.3.7, the latest release (17 October 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.7 is the latest release (17 October 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page