Skip to content

Go MPP middleware can emit false receipts, misses body binding, and has non-atomic replay protection #2

Description

@chenshj73

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions