Hi, updating this issue with the concrete finding and expanded code references.
Finding
Go MPP middleware can emit false receipts, misses body binding, and has non-atomic replay protection.
Reviewed Code
Reviewed commit: dee9b3aa6f9840b59371153eed733880e4715692.
Key Excerpts
server/server.go:408
406 | return
407 | }
408 | result.SetReceiptHeader(w)
409 | next.ServeHTTP(w, r)
410 | })
server/server.go:431
429 | return
430 | }
431 | result.SetReceiptHeader(w)
432 | fn(w, r, *result.Receipt)
433 | }
middleware/gin/gin.go:52
50 | return
51 | }
52 | result.SetReceiptHeader(c.Writer)
53 | c.Set(receiptKey, *result.Receipt)
54 | c.Next()
middleware/fiber/fiber.go:52
50 | return write402(c, result)
51 | }
52 | setReceipt(c, result)
53 | return c.Next()
54 | }
middleware/fiber/fiber.go:135-144
135 | // setReceipt stores the Receipt in context locals and sets the Payment-Receipt header.
136 | func setReceipt(c *fiber.Ctx, result *server.HandlerResult) {
137 | rw := &bufferedWriter{headers: make(http.Header)}
138 | result.SetReceiptHeader(rw)
139 | if h := rw.headers.Get(mppx.PaymentReceiptHeader); h != "" {
140 | c.Response().Header.Set(mppx.PaymentReceiptHeader, h)
141 | }
142 | if result.Receipt != nil {
143 | c.Locals(receiptKey, *result.Receipt)
144 | }
challenge.go:25-26
25 | // Digest is an optional SHA-256 digest of the HTTP request body ("sha-256=<base64>").
26 | Digest string `json:"digest,omitempty"`
Why This Matters
For the receipt issue, a protected handler can reject or fail after payment verification, while the final response still contains a success Payment-Receipt. Clients, logs, or accounting systems may record the resource as paid and delivered even though downstream application logic failed.
For the body-binding issue, a paid credential for one POST body can be replayed with a different body if the payment terms are otherwise the same. Paid routes where the request body controls the purchased action, prompt, job, or data can execute work different from what was originally priced.
For replay protection, two concurrent requests using the same transaction hash or serialized transaction can both pass Get before either request calls Put, resulting in duplicate resource delivery for one payment.
Suggested Fix
- Buffer or wrap downstream responses so receipt insertion happens after the final successful status is known.
- Compute and include a digest for request bodies in middleware-generated challenges, and verify the same digest on paid retries.
- Extend the store interface with
PutIfAbsent(ctx, key, value) or equivalent atomic semantics; implement it with a mutex in MemoryStore and with unique insert / Redis SET NX / database constraints for production stores.
- Add tests for paid handlers returning errors, POST body mutation, and concurrent replay of the same transaction hash.
Thanks for taking a look. If you prefer to move detailed follow-up into a GitHub Security Advisory, please invite @chenshj73; otherwise this public issue should be enough to identify and fix the bug.
Hi, updating this issue with the concrete finding and expanded code references.
Finding
Go MPP middleware can emit false receipts, misses body binding, and has non-atomic replay protection.
Reviewed Code
Reviewed commit:
dee9b3aa6f9840b59371153eed733880e4715692.Key Excerpts
server/server.go:408server/server.go:431middleware/gin/gin.go:52middleware/fiber/fiber.go:52middleware/fiber/fiber.go:135-144challenge.go:25-26Why This Matters
For the receipt issue, a protected handler can reject or fail after payment verification, while the final response still contains a success
Payment-Receipt. Clients, logs, or accounting systems may record the resource as paid and delivered even though downstream application logic failed.For the body-binding issue, a paid credential for one POST body can be replayed with a different body if the payment terms are otherwise the same. Paid routes where the request body controls the purchased action, prompt, job, or data can execute work different from what was originally priced.
For replay protection, two concurrent requests using the same transaction hash or serialized transaction can both pass
Getbefore either request callsPut, resulting in duplicate resource delivery for one payment.Suggested Fix
PutIfAbsent(ctx, key, value)or equivalent atomic semantics; implement it with a mutex inMemoryStoreand with unique insert / RedisSET NX/ database constraints for production stores.Thanks for taking a look. If you prefer to move detailed follow-up into a GitHub Security Advisory, please invite
@chenshj73; otherwise this public issue should be enough to identify and fix the bug.