I could PATCH my own postback_secret via PostgREST — affiliate trigger gap (PR #616 EEE F2)
Tina (32) independent affiliate-marketing studio Ljubljana Trnovo (@tinafoodtech, 44k Slovenian-Croatian niche). Day job Maribor fintech part-time DevSecOps engineer, evenings bug bounty + responsible disclosure. 12 months thMenu affiliate. Tuesday evening reading newly-shipped PR #609 CCC-B "dual-secret rotation" sweep, question formed: **if affiliate can PostgREST direct-PATCH their own postback_secret, rotation pattern completely bypassed.** DevTools opened, own affiliate JWT: `PATCH /rest/v1/affiliate_profiles?id=eq.<own_id> { "postback_secret": "attacker-chosen-known-secret" }` → **204 No Content**. Successful. Supabase realtime postback_secret set to chosen value. PR #524 GG (mid-April 2026) shipped BEFORE UPDATE trigger pattern for RLS WITH CHECK gap — balance_cents, commission_rate, kyc_status etc 10 fields RAISE EXCEPTION ERRCODE 42501 locked. service_role bypass via JWT claims role check. But PR #524 GG privileged-field list manually curated. **PR #609 CCC-B (early May 2026) shipped postback_url + postback_secret + postback_secret_prev + postback_secret_rotated_at but never updated trigger**. PR #524 GG had documented: "if new sensitive column lands and you forget trigger update, default OPEN" — exactly happened. **3 attack vectors**: (1) postback_secret hijack HMAC forge attacker-known-secret fake commission event craft + replay; (2) postback_url exfil attacker server commission events exfil (Stripe customer email, transaction amount, restaurant info); (3) PR #609 CCC-B dual-secret rotation bypass OCC race-guard `AND postback_secret = ?` direct-PATCH bypass. Tina reproduced 3 scenarios own test env + security@thmenu.com PoC writeup. Engineering 1-hour reproduce + severity HIGH. PR #524 GG migration inspected: 10 IF blocks but no postback_*. **PR #616 batch EEE F2** fix sober — CREATE OR REPLACE FUNCTION atomic swap 4 new IF blocks: postback_url + postback_secret + postback_secret_prev + postback_secret_rotated_at all `IF NEW.X IS DISTINCT FROM OLD.X RAISE EXCEPTION read-only USING ERRCODE = "42501"`. service_role bypass unchanged; cron jobs / webhook handlers skip trigger. Migration supabase/migrations/20260524000001_affiliate_coupon_postback_privileged_triggers.sql CREATE OR REPLACE FUNCTION atomic no downtime. Same migration sibling trigger for affiliate_coupons (EEE F1 separate blog). 90-day audit 0 prior exploits, Tina was first (PR #609 CCC-B shipped 5 days earlier, short exposure window). Tina HIGH severity + €750 Wise + Hall of Fame + 1-year Pro tier. Hilal Çanakkale Kepez (@hilal_food, 52k Aegean niche) Çanakkale OSB food export DevSecOps version with same flow. Pattern: **Supabase RLS USING policies not enough — PostgREST PATCH can mutate any field. BEFORE UPDATE trigger pattern mandatory. Keep privileged-field list as BLACKLIST > whitelist — if you forget updating trigger when adding sensitive column, default is LOCKED, not OPEN.** Implementation checklist: (1) BEFORE UPDATE trigger pattern paired with every UPDATE policy; (2) blacklist > whitelist default-deny semantics; (3) migration adding new column → check if it should be in trigger; (4) CREATE OR REPLACE FUNCTION atomic swap; (5) PR template checklist mandatory tick; (6) pentest enumerate columns + direct PATCH affiliate JWT; (7) quarterly trigger source vs schema diff audit.