Skip to content

Plutus v1.0.1

Choose a tag to compare

@tcconnally tcconnally released this 05 Jul 18:46
· 109 commits to main since this release
4bbc145

[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. New docs/deploy-hardening.md documents the safe config.
  • Config backups no longer escape the ignore rules. .gitignore/.dockerignore
    matched only *.plutus-bak-* but the real backup name is config.yaml.bak-<ts>,
    which could carry file-sourced secrets into a commit or image; now config.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-reversemark_stripe_event committed
    the event claim in a separate transaction from the credit add_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_event now wraps the
    claim and all side effects in one db.immediate (BEGIN IMMEDIATE) transaction
    with commit=False threaded through mark_stripe_event/add_ledger/
    set_org_tier; any error rolls the whole thing back for a clean retry.
    (2) /v1/usage idempotency 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 same db.immediate block 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_micros so 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
    authorization state was held only in a process-global pool, so an attacker
    could complete their own Google sign-in, capture their code+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/login now sets a
    short-lived HttpOnly; SameSite=Lax plutus_oauth_state cookie and
    handle_callback requires the callback state to match it (constant-time),
    in addition to the existing _pending nonce check. (2026-07-05 security review)
  • Security headers on every response. _send now emits X-Frame-Options: DENY
    (+ CSP frame-ancestors 'none') so the dashboard can't be framed/clickjacked,
    X-Content-Type-Options: nosniff, Referrer-Policy: same-origin, and a CSP that
    locks default-src/object-src/base-uri/form-action to self. Script/style
    keep 'unsafe-inline' (the dashboard is inline-only); a nonce-based script-src
    is a follow-up. (2026-07-05 security review)
  • CSV export formula-injection neutralized. Tenant-controlled
    provider/model/workspace/task_type cells beginning with = + - @ (or a
    leading tab/CR) are now quote-prefixed in export.csv, so a crafted value like
    =HYPERLINK(...) can't execute when a teammate opens the export. (2026-07-05)
  • Webhook error/log hardening. /webhook/stripe no 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 to body_html with 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)