mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-26 10:55:41 +00:00
test(p2p): DB-backed proof of the trade fund-routing freeze trigger (BYPASS #1)
Runs the exact freeze trigger from migration 20260725040000 against a real Postgres engine (pglite) and proves: anon UPDATE of buyer_id/crypto_amount (the escrow-redirect attack) is BLOCKED; status/proof-only stays allowed; service_role is exempt; the attacker redirect never persists. 5/5 pass. Upgrades the custody-critical trigger from code-reviewed to proven-by-execution. RLS/REVOKE role-enforcement still needs a multi-role staging Postgres (pglite is single-user).
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// DB-backed verification of migration 20260725040000 (BYPASS #1 custody fix):
|
||||
// the freeze trigger must make trade fund-routing columns immutable for non-
|
||||
// service-role writers. Runs the EXACT trigger from the migration on a real
|
||||
// Postgres engine (pglite/WASM, no system install). Run:
|
||||
// deno run -A web/supabase/migrations/__tests__/freeze_trade_financials.test.ts
|
||||
// NOTE: proves the trigger (primary custody guard, current_setting-based). The
|
||||
// RLS/REVOKE role-enforcement in the sibling migrations is single-user-untestable
|
||||
// here and must be verified on a multi-role staging Postgres (see DEPLOY_RUNBOOK).
|
||||
import { PGlite } from "npm:@electric-sql/pglite";
|
||||
const db = new PGlite();
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (c:boolean,m:string)=>{ c?(pass++,console.log(" ✅ "+m)):(fail++,console.log(" ❌ "+m)); };
|
||||
async function raises(sql:string){ try{ await db.exec(sql); return false; }catch(_e){ return true; } }
|
||||
|
||||
// minimal schema + EXACT trigger from migration 20260725040000
|
||||
await db.exec(`
|
||||
CREATE TABLE p2p_fiat_trades (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
offer_id uuid, seller_id uuid, buyer_id uuid,
|
||||
crypto_amount numeric, fiat_amount numeric, price_per_unit numeric,
|
||||
escrow_locked_amount numeric, status text, buyer_payment_reference text
|
||||
);
|
||||
CREATE OR REPLACE FUNCTION enforce_trade_financial_immutability()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF current_setting('role', true) = 'service_role'
|
||||
OR current_setting('request.jwt.claim.role', true) = 'service_role' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF NEW.offer_id IS DISTINCT FROM OLD.offer_id
|
||||
OR NEW.seller_id IS DISTINCT FROM OLD.seller_id
|
||||
OR NEW.buyer_id IS DISTINCT FROM OLD.buyer_id
|
||||
OR NEW.crypto_amount IS DISTINCT FROM OLD.crypto_amount
|
||||
OR NEW.fiat_amount IS DISTINCT FROM OLD.fiat_amount
|
||||
OR NEW.price_per_unit IS DISTINCT FROM OLD.price_per_unit
|
||||
OR NEW.escrow_locked_amount IS DISTINCT FROM OLD.escrow_locked_amount THEN
|
||||
RAISE EXCEPTION 'Trade fund-routing columns are immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER trg_freeze_trade_financials BEFORE UPDATE ON p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_trade_financial_immutability();
|
||||
INSERT INTO p2p_fiat_trades (id, seller_id, buyer_id, crypto_amount, status)
|
||||
VALUES ('11111111-1111-1111-1111-111111111111','22222222-2222-2222-2222-222222222222','33333333-3333-3333-3333-333333333333', 100, 'payment_sent');
|
||||
`);
|
||||
console.log("BYPASS #1 (escrow-release redirection via trade-row) — freeze trigger:");
|
||||
// A) anon UPDATE buyer_id -> MUST raise
|
||||
await db.exec(`SELECT set_config('request.jwt.claim.role','anon',false);`);
|
||||
ok(await raises(`UPDATE p2p_fiat_trades SET buyer_id='44444444-4444-4444-4444-444444444444' WHERE id='11111111-1111-1111-1111-111111111111'`),
|
||||
"anon UPDATE buyer_id (attacker redirect) -> BLOCKED");
|
||||
// B) anon UPDATE crypto_amount -> MUST raise
|
||||
ok(await raises(`UPDATE p2p_fiat_trades SET crypto_amount=999999 WHERE id='11111111-1111-1111-1111-111111111111'`),
|
||||
"anon UPDATE crypto_amount (amount tamper) -> BLOCKED");
|
||||
// C) anon UPDATE status only -> MUST succeed (non-custodial fields open)
|
||||
ok(!(await raises(`UPDATE p2p_fiat_trades SET status='paid', buyer_payment_reference='x' WHERE id='11111111-1111-1111-1111-111111111111'`)),
|
||||
"anon UPDATE status/proof only -> ALLOWED (correct)");
|
||||
// D) service_role UPDATE buyer_id -> MUST succeed (exempt)
|
||||
await db.exec(`SELECT set_config('request.jwt.claim.role','service_role',false);`);
|
||||
ok(!(await raises(`UPDATE p2p_fiat_trades SET buyer_id='55555555-5555-5555-5555-555555555555' WHERE id='11111111-1111-1111-1111-111111111111'`)),
|
||||
"service_role UPDATE buyer_id -> ALLOWED (reconciliation)");
|
||||
// verify buyer_id was NOT changed by the blocked anon attempt
|
||||
const r:any = await db.query(`SELECT buyer_id FROM p2p_fiat_trades WHERE id='11111111-1111-1111-1111-111111111111'`);
|
||||
ok(r.rows[0].buyer_id==='55555555-5555-5555-5555-555555555555',
|
||||
"final buyer_id = service_role value (attacker redirect never persisted)");
|
||||
console.log(`\nRESULT: ${pass} passed, ${fail} failed`);
|
||||
Deno.exit(fail>0?1:0);
|
||||
Reference in New Issue
Block a user