Merge pull request #351 from ArnasDon/fix/ghsa-34q7-fv77-625j-role-checks

fix(security): enforce agent role on service-role write routes (GHSA-34q7-fv77-625j)
This commit is contained in:
Arnas Donauskas 2026-07-08 09:39:41 +03:00 committed by GitHub
commit 03e851bea5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 86 additions and 2 deletions

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/automations/admin-client'
export async function POST(
@ -7,6 +8,16 @@ export async function POST(
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
// Duplicating creates a new automation row — a write. Enforce `agent`
// (the service-role client below bypasses the agent-gated
// automations_insert RLS).
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const supabase = await createClient()
const {
data: { user },

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/automations/admin-client'
import {
loadStepsTree,
@ -47,6 +48,16 @@ export async function PATCH(
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
// Editing an automation is a write — the RLS automations_update policy
// requires `agent`, but this route mutates via the service-role client
// which bypasses RLS, so enforce the role here.
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const user = await requireUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@ -125,6 +136,15 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
// Deleting an automation is a write — enforce `agent` (the service-role
// client below bypasses the agent-gated automations_delete RLS).
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const user = await requireUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'
import { getCurrentAccount, toErrorResponse } from '@/lib/auth/account'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { runAutomationsForTrigger } from '@/lib/automations/engine'
import type { AutomationTriggerType } from '@/types'
@ -9,9 +9,11 @@ import type { AutomationTriggerType } from '@/types'
* account_id and dispatch over the account's automations.
*/
export async function POST(request: Request) {
// Firing automations sends outbound WhatsApp — a write action. Require
// at least `agent`; a viewer must not be able to trigger sends.
let accountId: string
try {
const ctx = await getCurrentAccount()
const ctx = await requireRole('agent')
accountId = ctx.accountId
} catch (err) {
return toErrorResponse(err)

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/automations/admin-client'
import { getTemplate } from '@/lib/automations/templates'
import { insertSteps, type BuilderStepInput } from '@/lib/automations/steps-tree'
@ -24,6 +25,15 @@ export async function GET() {
}
export async function POST(request: Request) {
// Creating an automation is a write — the RLS automations_insert policy
// requires `agent`, but this route inserts via the service-role client
// which bypasses RLS, so the role must be enforced here.
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const supabase = await createClient()
const {
data: { user },

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/flows/admin-client'
import { validateFlowForActivation } from '@/lib/flows/validate'
@ -23,6 +24,16 @@ export async function POST(
) {
const { id } = await context.params
// Changing status (activate / draft / archive) is a write — the RLS
// flows_update policy requires `agent`, but the service-role client
// below bypasses RLS, so enforce the role here (a viewer passes the
// membership-only ownership check).
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const supabase = await createClient()
const {
data: { user },

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/flows/admin-client'
/**
@ -91,6 +92,16 @@ export async function PUT(
context: { params: Promise<{ id: string }> },
) {
const { id } = await context.params
// Writes require at least `agent` — the RLS flows_update policy demands
// it, but this route mutates via the service-role client which bypasses
// RLS, so the role must be enforced here (a viewer passes ownership).
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const guard = await requireOwnership(id)
if (!guard.ok) return NextResponse.json(guard.body, { status: guard.status })
@ -177,6 +188,15 @@ export async function DELETE(
context: { params: Promise<{ id: string }> },
) {
const { id } = await context.params
// Writes require at least `agent` — see the PUT handler note. The
// service-role client below bypasses the agent-gated flows_delete RLS.
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const guard = await requireOwnership(id)
if (!guard.ok) return NextResponse.json(guard.body, { status: guard.status })

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { requireRole, toErrorResponse } from '@/lib/auth/account'
import { supabaseAdmin } from '@/lib/flows/admin-client'
import { getFlowTemplate } from '@/lib/flows/templates'
@ -45,6 +46,15 @@ export async function GET() {
}
export async function POST(request: Request) {
// Creating a flow is a write — the RLS flows_insert policy requires
// `agent`, but this route inserts via the service-role client which
// bypasses RLS, so the role must be enforced here.
try {
await requireRole('agent')
} catch (err) {
return toErrorResponse(err)
}
const guard = await requireUser()
if (!guard.ok) {
return NextResponse.json(guard.body, { status: guard.status })