Skip to content

Potential payment verification issue in paid resource gate #5

Description

@chenshj73

Potential payment verification issue in paid resource gate

Hi, I noticed a possible payment-flow issue while reviewing the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.

Reviewed HEAD: 85f3eb6b61b9

What I observed

In the visible payment path, I observed:

ucp-proxy is a real UCP checkout/payment proxy. The MCP schema comments require idempotency-key for complete/cancel, but neither REST nor MCP handlers validate or persist an idempotency key, and CompleteCheckout directly calls WooCommerce /checkout. Replaying complete_checkout can therefore re-submit the payment/order path instead of being deduplicated.

Relevant code locations:

  • internal/handler/checkout.go:95-141
  • internal/handler/mcp.go:84-100
  • internal/woocommerce/client.go:426-533
  • README.md:72-93

Relevant code excerpts

internal/handler/checkout.go:95-119

  95 // handleCompleteCheckout submits payment and finalizes the checkout.
  96 // POST /checkout-sessions/{id}/complete
  97 func (h *Handler) handleCompleteCheckout(w http.ResponseWriter, r *http.Request) {
  98 	ctx := r.Context()
  99 	checkoutID := r.PathValue("id")
 100 
 101 	if checkoutID == "" {
 102 		h.writeCheckoutError(w, "", model.NewValidationError("id", "checkout ID required"))
 103 		return
 104 	}
 105 
 106 	var req model.CheckoutSubmitRequest
 107 	if err := decodeJSON(r, &req); err != nil {
 108 		h.writeCheckoutError(w, checkoutID, err)
 109 		return
 110 	}
 111 
 112 	// Validate payment submission: at least one instrument with selected=true
 113 	if len(req.Payment.Instruments) == 0 {
 114 		h.writeCheckoutError(w, checkoutID, model.NewValidationError("payment.instruments", "at least one instrument required"))
 115 		return
 116 	}
 117 	selectedInstrument := req.Payment.SelectedInstrument()
 118 	if selectedInstrument == nil {
 119 		h.writeCheckoutError(w, checkoutID, model.NewValidationError("payment.instruments", "one instrument must have selected=true"))

internal/handler/mcp.go:84-100

  84 // CompleteCheckoutInput is the input schema for complete_checkout tool.
  85 // Per spec: idempotency-key is required in meta for complete_checkout.
  86 type CompleteCheckoutInput struct {
  87 	Meta     MCPMeta                 `json:"meta" jsonschema:"request metadata (idempotency-key required),required"`
  88 	ID       string                  `json:"id" jsonschema:"checkout ID,required"`
  89 	Checkout CompleteCheckoutPayload `json:"checkout" jsonschema:"checkout data,required"`
  90 }
  91 
  92 // CompleteCheckoutPayload contains the checkout completion data.
  93 type CompleteCheckoutPayload struct {
  94 	Payment *model.Payment `json:"payment,omitempty" jsonschema:"payment instruments"`
  95 }
  96 
  97 // CancelCheckoutInput is the input schema for cancel_checkout tool.
  98 // Per spec: idempotency-key is required in meta for cancel_checkout.
  99 type CancelCheckoutInput struct {
 100 	Meta MCPMeta `json:"meta" jsonschema:"request metadata (idempotency-key required),required"`

internal/woocommerce/client.go:426-450

 426 // CompleteCheckout submits payment and finalizes the checkout.
 427 func (c *Client) CompleteCheckout(ctx context.Context, checkoutID string, req *model.CheckoutSubmitRequest) (*model.Checkout, error) {
 428 	cartToken := ExtractCartToken(checkoutID)
 429 
 430 	// Check for products requiring escalation
 431 	if c.hasEscalationConfig() {
 432 		cart, _, err := c.getCartViaMutation(ctx, cartToken, nil)
 433 		if err != nil {
 434 			return nil, fmt.Errorf("checking cart for escalation: %w", err)
 435 		}
 436 		if matches := c.findEscalationMatches(cart); len(matches) > 0 {
 437 			return c.buildEscalationResponseForProducts(ctx, checkoutID, cart, cartToken, matches)
 438 		}
 439 	}
 440 
 441 	// Find selected payment instrument
 442 	instrument := req.Payment.SelectedInstrument()
 443 	if instrument == nil {
 444 		return nil, model.NewValidationError("payment", "no payment instrument selected")
 445 	}
 446 
 447 	wcReq := &WooCheckoutRequest{}
 448 
 449 	// Route based on credential type (not handler_id - proxy is PSP-agnostic)
 450 	// Credential type is the contract between tokenization and processing.

README.md:72-93

  72 - **Stateless**: session tokens encoded in checkout IDs. No server-side session storage.
  73 - **Per-tenant**: one proxy instance per merchant. Scales to zero when idle.
  74 - **Multi-transport**: same checkout logic exposed via REST and MCP (Model Context Protocol).
  75 - **Pluggable adapters**: each platform adapter translates UCP → native API calls.
  76 - **Capability negotiation**: agents provide their profile URL, proxy intersects capabilities.
  77 
  78 ## API Overview
  79 
  80 | Endpoint                                | Description                                           |
  81 | --------------------------------------- | ----------------------------------------------------- |
  82 | `GET /.well-known/ucp`                  | Discovery profile (capabilities, transport endpoints) |
  83 | `POST /checkout-sessions`               | Create checkout from line items                       |
  84 | `GET /checkout-sessions/{id}`           | Get current checkout state                            |
  85 | `PUT /checkout-sessions/{id}`           | Update checkout (full state replacement)              |
  86 | `POST /checkout-sessions/{id}/complete` | Submit payment and finalize order                     |
  87 | `DELETE /checkout-sessions/{id}`        | Cancel checkout                                       |
  88 | `POST /mcp`                             | MCP transport (JSON-RPC over HTTP)                    |
  89 
  90 **Capability Negotiation:** All checkout operations require agent profile for capability intersection:
  91 - **REST**: `UCP-Agent: profile="https://agent.example/profile"` header
  92 - **MCP**: `meta.ucp-agent.profile` field in request params
  93 

Why this may matter

This may let a request pass the payment gate without the payment state intended by the server.

Suggested check

Consider making the paid-resource path depend on server-trusted payment requirements and a completed payment state. In particular, re-check recipient/payTo, amount, asset, network, nonce/idempotency, resource binding, and settlement result at the exact point where the protected API/tool/content is released.

Conservative caveat

I only reviewed the code visible in this repository at the HEAD above. If deployment-specific middleware or an upstream service enforces the missing binding/settlement invariant, this may already be mitigated there.

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