cloudflare / cloudflare/mpp-proxy

Potential paid-cookie scope issue: auth_token is not bound to route or payment context

Open
#24 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
65
Forks
11
PR merge metrics
No merged PRs in 30d

Description

Hi, I noticed a possible paid-cookie scope issue in the proxy.

The JWT payload in `src/jwt.ts` only records that a payment happened and when it expires:

```ts
6: export interface JWTPayload {
7: paid: boolean; // indicates payment was verified
8: iat: number; // issued at (seconds since epoch)
9: exp: number; // expires at (seconds since epoch)
10: }
```

When a JWT is generated, it contains `paid: true` but no route, amount, recipient, resource, network, or payment method scope:

```ts
89: // JWT Payload
90: const payload: JWTPayload = {
91: paid: true,
92: iat: now,
93: exp: now + expiresInSeconds,
94: };
115: return `${dataToSign}.${encodedSignature}`;
```

In `src/auth.ts`, a valid cookie skips payment middleware entirely:

```ts
21: export function requirePaymentOrCookie(paymentMw: MiddlewareHandler) {
23: // Check for valid cookie
24: const token = getCookie(c, "auth_token");
40: const payload = await verifyJWT(token, jwtSecret);
42: // If token is valid, skip payment and go directly to handler
43: if (payload) {
44: c.set("auth", payload);
45: await next(); // Call the handler
46: return;
47: }
50: // No valid cookie - apply payment middleware
51: return await paymentMw(c, next);
```

Each protected route can have its own amount/description:

```ts
81: export function createProtectedRoute(config: ProtectedRouteConfig) {
100: const paymentMw = payment(mppx.charge, {
101: amount: config.amount,
102: description: config.description,
103: });
106: return await requirePaymentOrCookie(paymentMw)(c, next);
```

In `src/index.ts`, the token is generated after the protected middleware accepts a new payment and then set as a path-wide cookie:

```ts
166: const result = await protectedMiddleware(c, async () => {
168: const hasExistingAuth = c.get("auth");
170: if (!hasExistingAuth) {
171: // This is a new payment - generate JWT cookie
174: jwtToken = await generateJWT(c.env.JWT_SECRET, 3600);
175: }
...
219: if (jwtToken) {
220: setCookie(c, "auth_token", jwtToken, {
224: maxAge: 3600,
225: path: "/",
226: });
```

The resulting authorization path appears to be:

```text
first paid request -> auth_token(paid=true) -> later protected route -> skip payment middleware
```

I did not see the cookie scoped to the route pattern, amount, recipient, resource, or network that produced it.

This may matter when a deployment has more than one protected route or price. A cookie minted for one protected path could be accepted for another protected path until expiry.

A safer design would include route/payment context claims in the JWT and compare them against the current protected route before skipping payment middleware. I am reporting this as a potential issue rather than a confirmed exploit, since a single-route deployment with one price would reduce the risk.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.