fix(payments): verify member_id belongs to caller's space (IDOR)

logCashPayment and linkPaymentToMember accepted a uuid member_id without
checking it belongs to the caller's space (the equipment/class paths were
already hardened for exactly this). Look up the member scoped to the caller's
space and reject a foreign id before writing.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Moheeb Zara
2026-08-04 19:10:42 -07:00
co-authored by Claude Opus 4.8
parent 1aa0cd818a
commit 3243e4ce98
+24
View File
@@ -36,6 +36,19 @@ export async function logCashPayment(formData: {
const transactionDate = v.data.transaction_date ?? new Date().toISOString() const transactionDate = v.data.transaction_date ?? new Date().toISOString()
// A supplied member_id must belong to the caller's space before we stamp it
// onto the payment, mirroring reserveEquipment. Without this a treasurer
// could link a cash payment to a member id from another space.
if (v.data.member_id) {
const { data: tgt } = await supabase
.from('space_members')
.select('id')
.eq('id', v.data.member_id)
.eq('space_id', member.space_id)
.maybeSingle()
if (!tgt) return { error: 'That member was not found in this space.' }
}
const { data, error } = await supabase const { data, error } = await supabase
.from('payments') .from('payments')
.insert({ .insert({
@@ -92,6 +105,17 @@ export async function linkPaymentToMember(paymentId: string, memberId: string) {
if (!auth.ok) return { error: auth.error } if (!auth.ok) return { error: auth.error }
const { member: self } = auth const { member: self } = auth
// The target member must belong to the caller's space before we link the
// payment to it, mirroring reserveEquipment. Without this a treasurer could
// link a payment to a member id from another space.
const { data: tgt } = await supabase
.from('space_members')
.select('id')
.eq('id', v.data.memberId)
.eq('space_id', self.space_id)
.maybeSingle()
if (!tgt) return { error: 'That member was not found in this space.' }
const { data: payment } = await supabase const { data: payment } = await supabase
.from('payments') .from('payments')
.select('amount, transaction_date') .select('amount, transaction_date')