Plutus v1.0.1
[1.0.1] — 2026-07-05
Security-hardening release. A four-part follow-up to the internal pre-1.0
review, from a fresh 2026-07-05 audit (money/ledger, auth/OIDC, HTTP surface,
deploy posture). No API/contract changes; the /v1 surface is unchanged. Every
finding was traced to source and covered by tests. Highlights: closed an OIDC
login-CSRF, made all money side effects atomic with the markers guarding them
(no webhook credit-loss / idempotency double-debit under crash+retry), added
security headers + CSV-injection defense, and made the server fail closed rather
than expose an unauthenticated dashboard on a public interface.
Security
- Fail closed instead of exposing an unauthenticated dashboard. The server now
refuses to bind a non-loopback host (e.g.--host 0.0.0.0) when authentication
is disabled — previously the shipped Docker/compose default (serve --demo --host 0.0.0.0) and the off-by-default / fail-off-on-misconfig auth could
publish an open billing console on the network. Override for a trusted network
with--allow-insecure/PLUTUS_ALLOW_INSECURE=1; the disposable demo image
sets it explicitly. Newdocs/deploy-hardening.mddocuments the safe config. - Config backups no longer escape the ignore rules.
.gitignore/.dockerignore
matched only*.plutus-bak-*but the real backup name isconfig.yaml.bak-<ts>,
which could carry file-sourced secrets into a commit or image; nowconfig.yaml*
and*.bak-*are ignored. - SMTP
alerts.require_tls(opt-in) refuses to send alerts over an
unencrypted connection (protects alert bodies, not just credentials, from a
STARTTLS downgrade). PyYAML is now a hard requirement at server start
(fail fast) rather than silently degrading config parsing.
Changed
- Dependency floors gained upper bounds (
stripe<14,reportlab<5,PyYAML<7,
pytest<10) for reproducible builds; the Docker image now runs as a non-root
user. - Ledger atomicity: every money side effect now commits in the same transaction
as the marker guarding it. Two crash-window bugs are closed:
(1) Webhook silent credit loss / double-reverse —mark_stripe_eventcommitted
the event claim in a separate transaction from the creditadd_ledger, so a
crash between them plus Stripe's retry left the event marked "duplicate" with the
credit never applied (customer paid, no credit); concurrent refund/dispute events
for one charge could also double-reverse.handle_webhook_eventnow wraps the
claim and all side effects in onedb.immediate(BEGIN IMMEDIATE) transaction
withcommit=Falsethreaded throughmark_stripe_event/add_ledger/
set_org_tier; any error rolls the whole thing back for a clean retry.
(2)/v1/usageidempotency double-debit — the batch committed, but the
idempotency-key status was flipped in a later separate commit, so a crash in
between left committed events behind a NULL-status claim that the 120s reclaim
deleted, letting a retry re-record the batch. The response is now stored inside
the samedb.immediateblock as the debits. (2026-07-05 security review) - Prepaid hard-stop decided in integer micro-dollars. The
block_over_balance
check compared float USD (balance - cost_usd < 0); it now uses
get_balance_micros/usd_to_microsso sub-micro float error can't let a debit
slip past zero or wrongly block one. (2026-07-05 security review) - OIDC login-CSRF: bind the OAuth flow to the browser that started it. The
authorizationstatewas held only in a process-global pool, so an attacker
could complete their own Google sign-in, capture theircode+state, and feed
a victim's browser a/auth/callback?code=…&state=…link — planting an
attacker-owned session in the victim's browser (the victim would then operate
inside, and leak API keys to, the attacker's tenant)./auth/loginnow sets a
short-livedHttpOnly; SameSite=Laxplutus_oauth_statecookie and
handle_callbackrequires the callbackstateto match it (constant-time),
in addition to the existing_pendingnonce check. (2026-07-05 security review) - Security headers on every response.
_sendnow emitsX-Frame-Options: DENY
(+ CSPframe-ancestors 'none') so the dashboard can't be framed/clickjacked,
X-Content-Type-Options: nosniff,Referrer-Policy: same-origin, and a CSP that
locksdefault-src/object-src/base-uri/form-actionto self. Script/style
keep'unsafe-inline'(the dashboard is inline-only); a nonce-basedscript-src
is a follow-up. (2026-07-05 security review) - CSV export formula-injection neutralized. Tenant-controlled
provider/model/workspace/task_typecells beginning with= + - @(or a
leading tab/CR) are now quote-prefixed inexport.csv, so a crafted value like
=HYPERLINK(...)can't execute when a teammate opens the export. (2026-07-05) - Webhook error/log hardening.
/webhook/stripeno longer echoes the raw
exception text to the caller on a bad signature (returns a generic message, logs
detail server-side), and the success path logs only the event id/type instead of
the applied result (org_id/amount/balance). (2026-07-05 security review)
Changed
views.simple_page's body parameter renamed tobody_htmlwith a docstring
making explicit that it is inserted as raw HTML and callers must pre-escape
untrusted data (removes a latent XSS foot-gun; no live path today). (2026-07-05)