Skip to content

sec: scheduledPurchaseTemplate embeds token in dashboard query-string link (not in direct API call) #406

Description

@cristim

Summary

The scheduledPurchaseTemplate (plain-text, for plan-based upcoming purchase notifications) in internal/email/templates.go:51-55 embeds the approval token in dashboard-facing query-string links:

[Review & Edit] {{.DashboardURL}}?action=edit&token={{urlquery .ApprovalToken}}
[Pause Plan]    {{.DashboardURL}}?action=pause&token={{urlquery .ApprovalToken}}
[Cancel This Purchase] {{.DashboardURL}}?action=cancel&token={{urlquery .ApprovalToken}}

These links point at the dashboard SPA (not the /api/purchases/approve/ endpoint directly). The SPA reads the token from location.search and uses it for subsequent API calls. This means:

  1. The token appears in browser history against the dashboard URL.
  2. If the dashboard page loads any analytics, font, or tracking resource (now or in the future), the Referer header carries the full URL including the token.
  3. Server-side logging of the dashboard's CloudFront distribution will record the token in access logs.

The purchaseApprovalRequestTemplate (for direct/ad-hoc purchases) correctly directs the link to /purchases/approve/<id>?token=... (the API path), which is the same leakage category — but the scheduled purchase template additionally exposes the token in the dashboard SPA load, not just the API call.

Affected file

  • internal/email/templates.go:51-55 — scheduled purchase notification template

Suggested fix

For the scheduled purchase template, the action links should either:

  1. Direct the user to a dashboard route that does not pass the token in the URL (e.g. /purchases/<id> which loads the execution and shows action buttons authenticated by session), or
  2. Follow the same POST-redirect pattern recommended in the companion issue about token leakage via GET query strings.

The token in the scheduled notification should ideally be used only to authenticate the cancel API call server-side after the user has logged in, not as a persistent URL parameter on the dashboard page itself.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions