Skip to content

Harden payment endpoint authorization and state transitions #27

Open

Description

@matteius

Summary

Replace #4 with a trackable fix for payment authorization and payment state changes. The reported /forms/payments/<submission_id>/cancel/ route is still accessible without proving ownership. The same payment flow also needs protection at initiation, confirmation, provider return, and webhook handling.

Current gaps

  • initiate_payment looks up a pending submission by numeric ID and creates a payment attempt without checking the caller's right to that submission. It does this on GET and can expose the payment client configuration.
  • confirm_payment accepts a payment record ID and calls the provider without checking the caller's right to its submission.
  • payment_return finds a pending attempt by submission ID and processes a provider return without authenticating the returning checkout or binding it to the expected attempt.
  • payment_cancel accepts GET or POST, requires no ownership check, and sets any submission to draft, including one that has already progressed beyond pending_payment.
  • Stripe webhook handling accepts unverified event payloads when STRIPE_WEBHOOK_SECRET is unset. The webhook must remain callable by the provider, but events that change payment state need authenticated provenance.

These paths are in payments/views.py and stripe_provider.py. Public forms intentionally allow anonymous submissions, so adding login_required everywhere would break supported checkout flows.

Acceptance criteria

  • Define and enforce one payment access policy: an authenticated submitter can act only on their own submission; an anonymous public-form submitter can continue checkout only with a session-bound or otherwise unguessable, short-lived checkout credential. A numeric submission/payment ID alone grants no access. The policy applies before rendering payment details, exposing client secrets, calling providers, or changing records. Unauthorized callers get a non-disclosing response.
  • Make initiation safe against arbitrary GETs and repeat requests. Repeated navigation or retries must not create uncontrolled payment attempts or duplicate provider charges; use stable idempotency for an attempt and document how a legitimate retry starts a new one.
  • Restrict confirmation, return, and cancellation to the correct payment attempt and valid lifecycle states. Bind redirect returns to the originating checkout and verify the provider's result. Cancellation must use an authorized, CSRF-protected user action (or a verified provider callback), and must never revert a completed, submitted, or approved submission to draft.
  • Require verification of Stripe webhook signatures before applying an event; fail closed if the verification secret is absent or the signature is invalid. Apply the same authenticated-event requirement to other providers through the provider interface. Handle replayed or out-of-order events without rolling state backward or creating duplicate workflow tasks.
  • Add regression tests for anonymous callers, a different signed-in user, the rightful signed-in user, a rightful anonymous public-form checkout, guessed IDs, GET versus POST, repeated requests, invalid/missing webhook signatures, and cancellation after completion. Update payment setup docs for the required webhook secret and checkout behavior.

Original report: #4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-candidateMaintainer-approved issue for a DigitalOcean agent PR

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions