Skip to content

fix(server): set Cache-Control: private on paid responses in gin/echo/fiber adapters - #83

Open
MoneyBund wants to merge 7 commits into
tempoxyz:mainfrom
MoneyBund:fix/adapter-cache-control-private
Open

fix(server): set Cache-Control: private on paid responses in gin/echo/fiber adapters#83
MoneyBund wants to merge 7 commits into
tempoxyz:mainfrom
MoneyBund:fix/adapter-cache-control-private

Conversation

@MoneyBund

Copy link
Copy Markdown

Set Cache-Control: private on paid responses in the Gin, Echo, and Fiber adapters

Type: Security fix · Severity: High · Area: pkg/server/{gin,echo,fiber}

Summary

The net/http middleware marks every successfully-paid response as
Cache-Control: private so that a shared cache can never hand one payer's
Payment-Receipt (and the paid body) to a different client. The three
framework adapters re-implement the success path inline and set the
Payment-Receipt header without the accompanying cache directive. This PR
brings all three adapters back in line with the core middleware and locks the
behavior in with a regression test per adapter.

Why this matters

serveVerified in pkg/server/middleware.go is
explicit about the invariant:

// Mark the paid response as private so shared caches never serve a
// Payment-Receipt to a different client. This mirrors the MPP spec
// (mpp.dev/protocol) and the rust reference implementation.
w.Header().Set("Cache-Control", "private")
w.Header().Set("Payment-Receipt", receipt.ToPaymentReceipt())

The Gin, Echo, and Fiber ChargeMiddleware handlers each set only
Payment-Receipt. When such a server sits behind a CDN or reverse proxy, a
200 OK on a GET route with a Payment-Receipt header and no cache
directives is eligible for heuristic caching (RFC 9111 §4.2.2). A shared
cache may then replay the paid body and another user's receipt to an unrelated,
unpaid client — precisely the leak the core middleware was written to prevent.

Three of the four officially supported stacks were affected; only the raw
net/http path (and ComposeMiddleware, which routes through serveVerified)
was protected.

The fix

Add Cache-Control: private alongside the Payment-Receipt header in each
adapter's verified branch, using each framework's native header API:

Adapter Call
Gin c.Writer.Header().Set("Cache-Control", "private")
Echo c.Response().Header().Set("Cache-Control", "private")
Fiber c.Set("Cache-Control", "private")

@brendanjryan

Copy link
Copy Markdown
Contributor

LGTM - can you just add a changelog?

@MoneyBund

Copy link
Copy Markdown
Author

LGTM - can you just add a changelog?

Done

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants