mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-27 12:25:40 +00:00
security(p2p): fix withdrawal BOLA, financial RLS exposure, admin dispute escrow
Critical/high audit remediation on the custodial P2P ledger. Auth model is wallet-based; identity (citizen/visa) is cryptographically bound to a wallet (People Chain tiki.citizenNft / active p2p_visa), which enables a correct fix. - Withdrawal BOLA (CRITICAL): process-withdraw now requires a wallet-SIGNED challenge; server verifies signature (raw + <Bytes> forms), asserts the signer OWNS the identity, consumes a single-use nonce (replay), and derives user_id server-side — the client-supplied user_id is ignored. process-withdrawal (batch) now requires the service-role key (was: any bearer, incl. public anon key). request_withdraw is REVOKEd from anon/authenticated + service-role guarded. - Financial RLS (HIGH): drop the blanket USING(true) SELECT on user_internal_balances, p2p_balance_transactions, p2p_deposit_withdraw_requests; lock p2p_user_payment_methods (IBAN PII) + p2p_fiat_disputes UPDATE to service_role; legitimate reads move behind scoped SECURITY DEFINER RPCs. - Deposit integrity: verify-deposit now binds the on-chain sender to identity ownership before crediting. - Admin dispute (CRITICAL fund-logic): DisputeResolutionPanel relabeled trades without moving escrow. New admin-signed resolve-dispute function + admin_resolve_dispute RPC moves escrow (release/refund/split) atomically with correct accounting (avoids the double-count in the legacy resolve_p2p_dispute). Client isAdmin documented as cosmetic. DEPLOY RUNBOOK (gated; owner runs): 1) apply migrations 20260225/20260725* in order; 2) deploy edge functions process-withdraw, process-withdrawal, verify-deposit, resolve-dispute; 3) set edge secrets PEOPLE_RPC_ENDPOINT (+ optional ADMIN_WALLETS); 4) ship frontend. Migrations + functions + frontend must go together or the app breaks. KNOWN RESIDUAL (Round 2 — as severe as the withdrawal BOLA): release_/lock_/refund_ escrow_internal still have PUBLIC EXECUTE and the client calls release_escrow_internal directly with the anon key from confirmPaymentReceived -> an anon caller can drain any victim's LOCKED balance. Fix = a wallet-signed confirm-payment edge function (same pattern as withdrawals) before revoking PUBLIC execute. Not yet fixed.
This commit is contained in:
@@ -8,6 +8,7 @@ import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { ApiPromise, WsProvider } from 'npm:@pezkuwi/api@16.5.36'
|
||||
import { blake2b } from 'npm:@noble/hashes@1.7.1/blake2b'
|
||||
import { base58 } from 'npm:@scure/base@1.2.4'
|
||||
import { assertIdentityOwnedByWallet } from '../_shared/identity-auth.ts'
|
||||
|
||||
// Allowed origins for CORS
|
||||
const ALLOWED_ORIGINS = [
|
||||
@@ -61,6 +62,17 @@ async function identityToUUID(identityId: string): Promise<string> {
|
||||
// PEZ asset ID
|
||||
const PEZ_ASSET_ID = 1
|
||||
|
||||
// People Chain endpoint — required to verify citizen NFT ownership server-side
|
||||
const PEOPLE_RPC_ENDPOINT = Deno.env.get('PEOPLE_RPC_ENDPOINT') || 'wss://people-rpc.pezkuwichain.io'
|
||||
|
||||
let peopleApiInstance: ApiPromise | null = null
|
||||
async function getPeopleApi(): Promise<ApiPromise> {
|
||||
if (peopleApiInstance && peopleApiInstance.isConnected) return peopleApiInstance
|
||||
const provider = new WsProvider(PEOPLE_RPC_ENDPOINT)
|
||||
peopleApiInstance = await ApiPromise.create({ provider })
|
||||
return peopleApiInstance
|
||||
}
|
||||
|
||||
interface DepositRequest {
|
||||
txHash: string
|
||||
token: 'HEZ' | 'PEZ'
|
||||
@@ -501,6 +513,30 @@ serve(async (req) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Bind the crediting identity to the depositing wallet. The on-chain sender
|
||||
// has just been proven to equal `walletAddress`; requiring that wallet to
|
||||
// OWN `identityId` prevents crediting a balance the depositor does not own
|
||||
// (citizen NFT / active visa binding).
|
||||
const depositOwnership = await assertIdentityOwnedByWallet(serviceClient, identityId, walletAddress, getPeopleApi)
|
||||
if (!depositOwnership.ok) {
|
||||
await serviceClient
|
||||
.from('p2p_deposit_withdraw_requests')
|
||||
.update({
|
||||
status: 'failed',
|
||||
error_message: `Identity ownership check failed: ${depositOwnership.error}`,
|
||||
processed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', depositRequest.id)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: depositOwnership.error || 'Depositing wallet does not own this identity'
|
||||
}),
|
||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Process deposit
|
||||
const { data: processResult, error: processError } = await serviceClient
|
||||
.rpc('process_deposit', {
|
||||
|
||||
Reference in New Issue
Block a user