From 751e78e064dc41e343e648ce721d77df87d70172 Mon Sep 17 00:00:00 2001 From: Erhnysr Date: Sat, 25 Jul 2026 23:26:22 +0300 Subject: [PATCH] fix: require shared secret for withdraw endpoint (#29) --- .env.example | 6 ++++++ README.md | 12 ++++++++++++ app/api/gateway/withdraw/route.ts | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/.env.example b/.env.example index d1a5c6f..0520825 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,12 @@ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key SELLER_ADDRESS=0xYourWalletAddress SELLER_PRIVATE_KEY=0xYourSellerPrivateKey +# Shared secret that protects POST /api/gateway/withdraw. Callers must send it +# as the `x-withdraw-secret` header. STRONGLY RECOMMENDED in production: if this +# is left unset, the withdraw endpoint is unauthenticated and anyone can drain +# the seller's Gateway balance to an arbitrary destinationAddress. +WITHDRAW_SECRET= + # Buyer wallet (for the payment agent) BUYER_ADDRESS=0xYourBuyerWalletAddress BUYER_PRIVATE_KEY=0xYourBuyerPrivateKey diff --git a/README.md b/README.md index b8b8cb9..b708971 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,15 @@ This sample application: - Assumes testnet usage only - Handles secrets via environment variables - Is not intended for production use without modification + +### Protecting the withdraw endpoint + +`POST /api/gateway/withdraw` moves the seller's Gateway balance using +`SELLER_PRIVATE_KEY` and accepts an arbitrary `destinationAddress`. Left open, +anyone who can reach it can drain funds to their own wallet. + +Set `WITHDRAW_SECRET` in production. When it is set, every request must include +a matching `x-withdraw-secret` header or it is rejected with `401 Unauthorized`. +For a hosted seller dashboard, call this endpoint from a trusted server-to-server +context rather than directly from the browser, so the secret is never shipped in +the client bundle. diff --git a/app/api/gateway/withdraw/route.ts b/app/api/gateway/withdraw/route.ts index 5a22e9f..ef649a3 100644 --- a/app/api/gateway/withdraw/route.ts +++ b/app/api/gateway/withdraw/route.ts @@ -48,6 +48,17 @@ export async function POST(req: NextRequest) { ); } + // Optional shared-secret guard. When WITHDRAW_SECRET is set, callers must + // present it via the x-withdraw-secret header. This endpoint moves the + // seller's Gateway balance, so it must not be left open in production. + const withdrawSecret = process.env.WITHDRAW_SECRET; + if (withdrawSecret) { + const provided = req.headers.get("x-withdraw-secret"); + if (provided !== withdrawSecret) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } + const body = await req.json(); const { amount, destinationChain, destinationAddress } = body as { amount: string;