From 383894b7aeab1c3982638a6d53236246fa1372c6 Mon Sep 17 00:00:00 2001 From: Eric Mota Tejeda Date: Mon, 10 Aug 2026 01:03:55 -0600 Subject: [PATCH 1/2] chore(apk): apuntar los builds a api.micopay.app (AWS) Render se esta apagando; los dos builds pasan a api.micopay.app. Aviso importante dejado en .env.mainnet: ese dominio corre hoy con STELLAR_NETWORK=TESTNET, asi que el build "mainnet" no mueve fondos reales todavia. Hasta que exista un backend en modo MAINNET, sigue siendo un build de prueba. VITE_ENABLE_DEFI_TRADING=true solo en testnet (finding B2: ese camino no mueve fondos reales aun); queda sin poner en mainnet/production. Co-Authored-By: Claude Opus 5 --- micopay/frontend/.env.mainnet | 7 ++++++- micopay/frontend/.env.testnet | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/micopay/frontend/.env.mainnet b/micopay/frontend/.env.mainnet index ab131dfd..08b203c2 100644 --- a/micopay/frontend/.env.mainnet +++ b/micopay/frontend/.env.mainnet @@ -1,5 +1,10 @@ # Mainnet APK build — overrides .env base values -VITE_API_URL=https://micopay-api.onrender.com +# NOTA: api.micopay.app corre hoy en modo STELLAR_NETWORK=TESTNET (ver +# docs/AWS_MIGRATION_PLAN_2026-07.md §6, taskdef). Este build de mainnet +# apunta al mismo dominio porque Render se está apagando y no tiene caso +# dejarlo ahí — pero hasta que exista un backend real en modo MAINNET, +# las transacciones de este build seguirán siendo de prueba, no reales. +VITE_API_URL=https://api.micopay.app VITE_STELLAR_NETWORK=PUBLIC VITE_HORIZON_URL=https://horizon.stellar.org VITE_NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015 diff --git a/micopay/frontend/.env.testnet b/micopay/frontend/.env.testnet index a5aef7e1..7d82431c 100644 --- a/micopay/frontend/.env.testnet +++ b/micopay/frontend/.env.testnet @@ -1,5 +1,9 @@ # Testnet APK build — overrides .env base values -VITE_API_URL=https://micopay-api.onrender.com +VITE_API_URL=https://api.micopay.app +# CETES/Blend trading UI (see docs/AUDIT_MOBILE_MAINNET.md finding B2 — that +# code path doesn't move real user funds yet). Enabled here because testnet +# builds are internal/demo builds; keep this UNSET in .env.mainnet/.env.production. +VITE_ENABLE_DEFI_TRADING=true VITE_STELLAR_NETWORK=TESTNET VITE_ESCROW_CONTRACT_ID=CB4M5777YFQWKGDUULCX5W6PXEDJSJARDTMH4VV6FXC4W4UPANALO3HZ VITE_MXNE_CONTRACT_ID=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC From ac93a82a79cca41d305e163b5bc0459c586bd9f8 Mon Sep 17 00:00:00 2001 From: Eric Mota Tejeda Date: Mon, 10 Aug 2026 01:03:37 -0600 Subject: [PATCH 2/2] fix(ramp): validar pertenencia de la orden antes de exponerla (IDOR) GET /defi/ramp/order/:orderId y regenerate_tx devolvian el status de la orden de cualquiera con solo tener el orderId. Ver el finding "/defi/ramp/order/:orderId sin check de pertenencia" en AUDIT_MOBILE_MAINNET.md. Nueva tabla ramp_orders (order_id, user_id) que registra al dueno al crear la orden y se valida en ambas rutas (403 si no coincide). Fail-open para ordenes anteriores a la migracion (sin fila de ownership se permite y se loguea warning) para no romper ordenes en curso. Co-Authored-By: Claude Opus 5 --- micopay/backend/src/routes/ramp.ts | 36 ++++++++++++++++++- ...260702090000_ramp_order_ownership.down.sql | 1 + ...20260702090000_ramp_order_ownership.up.sql | 12 +++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 micopay/sql/migrations/20260702090000_ramp_order_ownership.down.sql create mode 100644 micopay/sql/migrations/20260702090000_ramp_order_ownership.up.sql diff --git a/micopay/backend/src/routes/ramp.ts b/micopay/backend/src/routes/ramp.ts index e473e8ee..edb624b3 100644 --- a/micopay/backend/src/routes/ramp.ts +++ b/micopay/backend/src/routes/ramp.ts @@ -41,6 +41,35 @@ function ensureEtherfuseConfigured() { } } +/** + * order_id is a backend-generated UUID handed to Etherfuse, never guessable — + * but nothing previously stopped one authenticated user from polling another + * user's order if they learned its ID. This records ownership at creation and + * enforces it on every subsequent read (see docs/AUDIT_MOBILE_MAINNET.md, + * "IDOR ramp order"). Orders created before this migration have no row here — + * fail open for those (log + allow) rather than break in-flight orders. + */ +async function recordRampOrderOwner(orderId: string, userId: string): Promise { + await db.execute( + 'INSERT INTO ramp_orders (order_id, user_id) VALUES ($1, $2)', + [orderId, userId], + ); +} + +async function assertRampOrderOwnership(orderId: string, userId: string, request: { log: { warn: (obj: unknown, msg: string) => void } }): Promise { + const owner = await db.getOne<{ user_id: string }>( + 'SELECT user_id FROM ramp_orders WHERE order_id = $1', + [orderId], + ); + if (!owner) { + request.log.warn({ order_id: orderId, user_id: userId, category: 'ramp' }, '[ramp] Order has no recorded owner (pre-migration) — allowing'); + return; + } + if (owner.user_id !== userId) { + throw new ForbiddenError('No tienes permiso para ver esta orden'); + } +} + export async function rampRoutes(app: FastifyInstance): Promise { // Get quote: MXN->CETES (onramp) or CETES->MXN (offramp). app.post<{ Body: { type: 'onramp' | 'offramp'; sourceAmount: string } }>( @@ -114,16 +143,19 @@ export async function rampRoutes(app: FastifyInstance): Promise { } const user = await requireOnboardedUser(request.user.id); + const orderId = randomUUID(); try { const result = await createOrder({ - orderId: randomUUID(), + orderId, quoteId, bankAccountId: user.etherfuse_bank_account_id!, publicKey: user.stellar_address, useAnchor, }); + await recordRampOrderOwner(orderId, request.user.id); + return 'offramp' in result ? result.offramp : result.onramp; } catch (err: any) { throw new UpstreamError( @@ -141,6 +173,7 @@ export async function rampRoutes(app: FastifyInstance): Promise { { preHandler: [authMiddleware] }, async (request) => { const { orderId } = request.params; + await assertRampOrderOwnership(orderId, request.user.id, request); try { const order = await getOrder(orderId); return { orderId: order.orderId, status: order.status, type: order.orderType }; @@ -163,6 +196,7 @@ export async function rampRoutes(app: FastifyInstance): Promise { { preHandler: [authMiddleware] }, async (request, reply) => { const { orderId } = request.params; + await assertRampOrderOwnership(orderId, request.user.id, request); try { const { status, body } = await regenerateOrderTx(orderId); return reply.status(status).send(body ?? {}); diff --git a/micopay/sql/migrations/20260702090000_ramp_order_ownership.down.sql b/micopay/sql/migrations/20260702090000_ramp_order_ownership.down.sql new file mode 100644 index 00000000..f01a3222 --- /dev/null +++ b/micopay/sql/migrations/20260702090000_ramp_order_ownership.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS ramp_orders; diff --git a/micopay/sql/migrations/20260702090000_ramp_order_ownership.up.sql b/micopay/sql/migrations/20260702090000_ramp_order_ownership.up.sql new file mode 100644 index 00000000..1ca8391b --- /dev/null +++ b/micopay/sql/migrations/20260702090000_ramp_order_ownership.up.sql @@ -0,0 +1,12 @@ +-- SEC hardening: /defi/ramp/order/:orderId and its regenerate_tx sibling let +-- any authenticated user poll ANY orderId (order IDs are backend-generated +-- UUIDs, never persisted with an owner — see docs/AUDIT_MOBILE_MAINNET.md, +-- "IDOR ramp order"). This table records who created each order so the route +-- can enforce ownership going forward. +CREATE TABLE IF NOT EXISTS ramp_orders ( + order_id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_ramp_orders_user ON ramp_orders (user_id, created_at DESC);