If you believe you've found a security issue in arium, please report
it privately. Public issues on the tracker should be reserved for
non-security bug reports.
- Preferred: open a GitHub Security Advisory at https://github.com/tonybierman/arium/security/advisories/new — this keeps the report private until a fix lands.
- Fallback: email
tonybierman@gmail.comwith[arium security]in the subject.
This is a personal project with no SLA; expect best-effort response times. Please include in your report:
- a clear description of the issue and its impact,
- a minimal reproduction (commit SHA + repro steps or PoC),
- whether you intend to disclose publicly and on what timeline.
Researchers will be credited in the changelog and the resolved advisory unless they request otherwise.
The crate is pre-1.0 and ships from main. Only the latest commit on
main receives fixes. There is no release-branch backport policy yet.
arium is a reusable authentication and authorization library. It is intended to defend against:
- credential stuffing and brute-force sign-in (per-IP rate limiting),
- offline password cracking after database exfiltration (Argon2id hashing),
- session theft over the wire (cookie flags + TLS expected at the reverse proxy),
- OAuth callback CSRF (
stateverification stored in the session and checked on callback — seecrates/arium/src/oauth.rs), - TOTP recovery-code reuse (single-use enforced at the DB layer:
mfa_recovery_codes.used_at), - SQL injection (queries built with SQLx parameter binding — no string interpolation),
- secret leakage into the repo (gitleaks + trufflehog scans, see below).
It is not intended to defend against:
- a compromised host or database operator,
- a malicious dependency injected before the advisory database flags it,
- application-level authorization bugs in code that consumes this crate (it provides building blocks; correct use is the consumer's responsibility),
- side-channel attacks on Argon2 outside the documented parameter envelope.
- Passwords hashed with Argon2id via the
argon2crate. - Sessions backed by
axum_session/axum_session_auth. - TOTP enrolment generates recovery codes whose Argon2 hashes are
stored in
mfa_recovery_codes; consumption setsused_atso a code can never be replayed. - OAuth flows store the CSRF
stateper provider in the session before redirecting and compare on callback. - Per-IP rate limiting on the entire router via
tower_governor. - Append-only
audit_eventstable records authentication and admin state changes. - Bootstrap admin is gated by the
BOOTSTRAP_ADMIN_EMAILenv var — the first signup matching that email is auto-promoted, after which the env var no longer grants privileges to anyone else. - Secure response headers by default.
installstamps a behaviour-safe static set on every response (including short-circuited ones like a rate-limit429):X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: SAMEORIGIN,Cross-Origin-Opener-Policy: same-origin,X-Permitted-Cross-Domain-Policies: none, and a restrictivePermissions-Policy. No configuration required. The environment-specific headers (HSTS, CSP) and theSecurecookie flag are opt-in — see Deploying behind HTTPS below. - Credential forms submit over POST. The login, forgot-password, and
reset-password forms carry
method="post", so even in a degraded path (JS disabled, pre-hydration, or a native scanner submit) typed credentials never land in the URL / access logs /Referer. - Constant-time login. Sign-in runs an Argon2 verify on every attempt — including unknown emails — so response timing can't be used to enumerate which addresses have accounts.
- API tokens are shown in cleartext only once at creation; only a prefix
and a SHA-256 hash are stored. The
Authorization: Bearermiddleware (applied byinstallunder thetokensfeature) matches the hash against non-revokedapi_keysrows — a malformed or revoked token is silently ignored, never trusted. - Per-resource authorization (
arium-authz). Resource-scoped checks are fresh, per-request, and default-deny:require_resource(and theAuthzCtxguard) hits the app's membership storage on every call — no caching — and treats "no relationship" and a below-threshold role identically as a deny, keeping a storage lookup failure distinct from a deliberate deny so the former can never be silently read as "no access." Denials write aresource.access.deniedaudit row.
Two GitHub Actions workflows run security tooling:
Per push / PR — .github/workflows/ci.yml
Gating (a failure blocks the merge):
cargo clippy— two passes.--all-targetswith the default warning set, plus--libwith security-leaning lints layered on (unwrap_used,expect_used,panic,indexing_slicing,arithmetic_side_effects). The strict set is scoped to the lib target only —unwrap()is idiomatic in tests.cargo deny check bans licenses sources— deterministic policy enforcement againstCargo.lock. Advisories are deliberately excluded here (see theauditjob below) so an upstream-controlled moving target doesn't block PRs.cargo audit— RustSec advisory database againstCargo.lock. One curated ignore lives in.cargo/audit.toml, each entry justified inline with a reachability argument and a pointer to the standup card tracking re-evaluation.cargo machete— unused dependency detection.gitleaks detect --source . --redact— secret scan of the full repo + history. Invoked via the official binary, not the marketplace action (the action's license check is flaky for personal accounts). Allowlist lives in.gitleaks.toml.cargo fmt --check— formatting.cargo testand acargo checkmatrix across realistic feature combinations.
Nightly — .github/workflows/nightly.yml
cargo auditagain, so newly-published advisories surface within 24 hours even when no PR is open.cargo outdated— surfaces new compatible releases.cargo geiger— countsunsafereachable in the dep tree.trufflehog— full-history secret scan with--only-verified.
deny.toml— license allow-list with per-entry rationale; explicitallow-gitper source (no wildcard org trust);yanked = "deny";allow-wildcard-paths = truefor internal workspace members only..cargo/audit.toml— RustSec ignores, each entry annotated with the reachability argument that justifies it and a pointer to the standup card tracking re-evaluation..gitleaks.toml— extends the upstream default ruleset with an allowlist for one recognisable test-fixture hex literal used to exercise email-template width checks..gitignore— excludes dev SQLite databases, the dev-fallbackemails/directory, and.envfiles.
- Password hashing: Argon2id (
argon2crate, default parameters). Verification uses thepassword-hashecosystem; do not reach into raw hash bytes. - OAuth:
oauth25.x overreqwest+rustls-tls. - TOTP:
totp-rs5.x. - Random secrets: seeded from the OS via
argon2::password_hash:: rand_core::OsRngandgetrandom— never a thread-local PRNG.
Three production hardening knobs are off by default — each one breaks
plain-HTTP localhost development, so you opt in when deploying behind TLS.
Set them on the AuthConfig builder before install:
let cfg = AuthConfig::builder(pool, mailer)
// ...providers, audit, rate-limit, etc...
.cookie_secure(true) // session cookie sent over HTTPS only
.hsts(arium::RECOMMENDED_HSTS) // Strict-Transport-Security
.content_security_policy("default-src 'self'; ...") // tune for your build
.build()?;cookie_secure(true)addsSecureto the session cookie. Leave it off locally — aSecurecookie is never sent over HTTP, so enabling it on a plain-HTTP dev build silently logs everyone out. The cookie staysSameSite=Lax(notStrict) on purpose: the OAuth provider's callback is a cross-site top-level redirect, and onlyLaxlets the session cookie — which carries the CSRFstateand PKCE verifier — ride it.Strictbreaks OAuth.hsts(...)enablesStrict-Transport-Security.arium::RECOMMENDED_HSTSis a sensible two-yearincludeSubDomains; preloadvalue. Only set it behind HTTPS: once a browser sees HSTS it refuses plain-HTTP for the domain until the directive expires, which can lock you out of alocalhostdev build.content_security_policy(...)enables CSP. A Dioxus app hydrates from wasm plus an inline bootstrap script, so the policy must permit them — a wrong CSP silently breaks hydration. See the rustdoc onAuthConfigBuilder::content_security_policyfor a working starter policy to tighten with nonces/hashes once you've confirmed your build still hydrates.
RECOMMENDED_HSTS is exported from the arium crate; reference it as
arium::RECOMMENDED_HSTS (or pass the equivalent string literal) — the
arium-dioxus / arium-leptos adapters re-export AuthConfig and
AuthConfigBuilder but not the constant.
- TLS termination is expected at a reverse proxy in front of the app; the example does not terminate TLS itself.
- The development-mode email backend writes
.emlfiles to disk for inspection — never enable it in production (theMAIL_*env vars drive the production SMTP backend instead). RUSTSEC-2023-0071(rsa 0.9.10, Marvin Attack) — reaches the build graph only throughsqlx-macros-core's compile-time backend support and is not reachable from a deployed SQLite-only or Postgres-only build. Triaged, ignored in.cargo/audit.tomlwith the reachability argument inline. No upstream fix exists yet; recheck monthly via the nightlycargo auditand drop the ignore the moment a fix ships.