Add security hardening SQL patch

This commit is contained in:
Codex 2026-04-29 15:21:31 +03:00
parent e29a51e7ea
commit 219670583b
1 changed files with 122 additions and 0 deletions

View File

@ -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());