Add security hardening SQL patch
This commit is contained in:
parent
e29a51e7ea
commit
219670583b
|
|
@ -0,0 +1,122 @@
|
|||
-- Security hardening migration for OTP, public delivery invitations, and Edge Function rate limits.
|
||||
-- Run this file once in Supabase SQL Editor before deploying the updated Edge Functions.
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
create table if not exists public.rate_limits (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
scope text not null,
|
||||
rate_key text not null,
|
||||
window_start timestamptz not null,
|
||||
count integer not null default 1,
|
||||
blocked_until timestamptz,
|
||||
created_at timestamptz not null default timezone('utc', now()),
|
||||
updated_at timestamptz not null default timezone('utc', now()),
|
||||
unique (scope, rate_key, window_start)
|
||||
);
|
||||
|
||||
alter table public.delivery_invitations add column if not exists expires_at timestamptz;
|
||||
alter table public.delivery_invitations add column if not exists revoked_at timestamptz;
|
||||
alter table public.delivery_invitations add column if not exists access_count integer not null default 0;
|
||||
alter table public.delivery_invitations add column if not exists last_accessed_at timestamptz;
|
||||
|
||||
create index if not exists idx_delivery_invitations_expires_at
|
||||
on public.delivery_invitations (expires_at);
|
||||
|
||||
create index if not exists idx_rate_limits_scope_key_window
|
||||
on public.rate_limits (scope, rate_key, window_start desc);
|
||||
|
||||
create index if not exists idx_rate_limits_blocked_until
|
||||
on public.rate_limits (blocked_until);
|
||||
|
||||
create or replace function public.check_rate_limit(
|
||||
p_scope text,
|
||||
p_key text,
|
||||
p_max_count integer,
|
||||
p_window_seconds integer,
|
||||
p_block_seconds integer default 0
|
||||
)
|
||||
returns table (
|
||||
allowed boolean,
|
||||
current_count integer,
|
||||
limit_count integer,
|
||||
blocked_until timestamptz,
|
||||
window_start timestamptz
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $function$
|
||||
declare
|
||||
v_now timestamptz := timezone('utc', now());
|
||||
v_window_start timestamptz;
|
||||
v_count integer;
|
||||
v_blocked_until timestamptz;
|
||||
begin
|
||||
if p_max_count <= 0 then
|
||||
raise exception 'max_count must be positive';
|
||||
end if;
|
||||
|
||||
if p_window_seconds <= 0 then
|
||||
raise exception 'window_seconds must be positive';
|
||||
end if;
|
||||
|
||||
v_window_start := to_timestamp(
|
||||
floor(extract(epoch from v_now) / p_window_seconds) * p_window_seconds
|
||||
);
|
||||
|
||||
select rl.blocked_until
|
||||
into v_blocked_until
|
||||
from public.rate_limits rl
|
||||
where rl.scope = p_scope
|
||||
and rl.rate_key = p_key
|
||||
and rl.blocked_until is not null
|
||||
and rl.blocked_until > v_now
|
||||
order by rl.blocked_until desc
|
||||
limit 1;
|
||||
|
||||
if v_blocked_until is not null then
|
||||
return query
|
||||
select false, 0, p_max_count, v_blocked_until, v_window_start;
|
||||
return;
|
||||
end if;
|
||||
|
||||
insert into public.rate_limits (scope, rate_key, window_start, count, blocked_until)
|
||||
values (p_scope, p_key, v_window_start, 1, null)
|
||||
on conflict (scope, rate_key, window_start)
|
||||
do update set
|
||||
count = public.rate_limits.count + 1,
|
||||
blocked_until = case
|
||||
when public.rate_limits.count + 1 > p_max_count and p_block_seconds > 0 then greatest(
|
||||
coalesce(public.rate_limits.blocked_until, v_now),
|
||||
v_now + make_interval(secs => p_block_seconds)
|
||||
)
|
||||
else public.rate_limits.blocked_until
|
||||
end,
|
||||
updated_at = v_now
|
||||
returning count, blocked_until
|
||||
into v_count, v_blocked_until;
|
||||
|
||||
return query
|
||||
select
|
||||
v_count <= p_max_count and (v_blocked_until is null or v_blocked_until <= v_now),
|
||||
v_count,
|
||||
p_max_count,
|
||||
v_blocked_until,
|
||||
v_window_start;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
alter table public.rate_limits enable row level security;
|
||||
|
||||
drop policy if exists "rate limits admin only" on public.rate_limits;
|
||||
create policy "rate limits admin only" on public.rate_limits
|
||||
for all
|
||||
using (public.current_role_name() = 'admin')
|
||||
with check (public.current_role_name() = 'admin');
|
||||
|
||||
drop policy if exists "users self or admin" on public.users;
|
||||
create policy "users self or admin" on public.users
|
||||
for select
|
||||
using (public.current_role_name() = 'admin' or id = auth.uid());
|
||||
|
||||
Loading…
Reference in New Issue