Skip to content

Commit 2777b89

Browse files
Thales-Chagasclaude
andcommitted
Seguranca: escapa HTML no PDF, protege CSV e ajustes do Advisor
- exportarPDF/htmlTabela: escapa valores (escHtml) antes de escrever no document.write da janela de impressao. Nomes de categoria/centro (criados pelo usuario e pela IA do Telegram) iam sem escapar -> HTML/JS injection no mesmo dominio do app (risco de roubo do token de sessao no localStorage). - exportarExcel: protegerCsv prefixa ' em celulas que comecam com = + @ (CSV formula injection), preservando numeros negativos. - Nova migration seguranca_advisor.sql: fixa search_path de set_updated_at e revoga EXECUTE publico das funcoes de gatilho (rodar no SQL Editor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b75a818 commit 2777b89

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

src/App.jsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,22 @@ async function hashPin(pin, salt) {
414414
EXPORTAÇÕES (PDF via impressão / Excel via CSV)
415415
============================================================ */
416416

417+
// Escapa texto para inserir com segurança dentro de HTML. Usado na exportação
418+
// de PDF (document.write numa janela do MESMO domínio): sem isso, um nome de
419+
// categoria/descrição com "<img onerror=...>" viraria JS rodando no app.
420+
function escHtml(s) {
421+
return String(s ?? "")
422+
.replace(/&/g, "&amp;")
423+
.replace(/</g, "&lt;")
424+
.replace(/>/g, "&gt;")
425+
.replace(/"/g, "&quot;");
426+
}
427+
417428
function exportarPDF(titulo, corpoHtml) {
418429
const w = window.open("", "_blank");
419430
if (!w) return false;
420431
w.document.write(`<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">
421-
<title>${titulo}</title>
432+
<title>${escHtml(titulo)}</title>
422433
<style>
423434
body { font-family: Arial, Helvetica, sans-serif; color: #1e293b; padding: 24px; }
424435
h1 { font-size: 20px; margin: 0 0 2px; } h2 { font-size: 14px; margin: 18px 0 6px; }
@@ -434,11 +445,20 @@ function exportarPDF(titulo, corpoHtml) {
434445
return true;
435446
}
436447

448+
// Evita "CSV formula injection": uma célula começando com = + @ (ou tab/CR)
449+
// pode virar fórmula ao abrir no Excel. Prefixa com ' pra forçar texto.
450+
// Números negativos ("-1.234,56") são preservados (- seguido de dígito).
451+
function protegerCsv(v) {
452+
let s = String(v ?? "");
453+
if (/^[=+@\t\r]/.test(s) || /^-(?!\d)/.test(s)) s = "'" + s;
454+
return s;
455+
}
456+
437457
function exportarExcel(nomeArquivo, linhas) {
438458
const csv =
439459
"" +
440460
linhas
441-
.map((l) => l.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(";"))
461+
.map((l) => l.map((c) => `"${protegerCsv(c).replace(/"/g, '""')}"`).join(";"))
442462
.join("\r\n");
443463
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
444464
const url = URL.createObjectURL(blob);
@@ -1950,9 +1970,9 @@ function PaginaRelatorios({ espaco, empresarial, ano, mesIdx, nomeApp }) {
19501970
const totAnualDesp = anual.reduce((a, m) => a + m.desp, 0);
19511971

19521972
function htmlTabela(cabecalho, linhas, totalLinha) {
1953-
return `<table><thead><tr>${cabecalho.map((c, i) => `<th class="${i > 0 ? "num" : ""}">${c}</th>`).join("")}</tr></thead><tbody>${linhas
1954-
.map((l) => `<tr>${l.map((c, i) => `<td class="${i > 0 ? "num" : ""}">${c}</td>`).join("")}</tr>`)
1955-
.join("")}${totalLinha ? `<tr class="total">${totalLinha.map((c, i) => `<td class="${i > 0 ? "num" : ""}">${c}</td>`).join("")}</tr>` : ""}</tbody></table>`;
1973+
return `<table><thead><tr>${cabecalho.map((c, i) => `<th class="${i > 0 ? "num" : ""}">${escHtml(c)}</th>`).join("")}</tr></thead><tbody>${linhas
1974+
.map((l) => `<tr>${l.map((c, i) => `<td class="${i > 0 ? "num" : ""}">${escHtml(c)}</td>`).join("")}</tr>`)
1975+
.join("")}${totalLinha ? `<tr class="total">${totalLinha.map((c, i) => `<td class="${i > 0 ? "num" : ""}">${escHtml(c)}</td>`).join("")}</tr>` : ""}</tbody></table>`;
19561976
}
19571977

19581978
function exportar(formato) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- ============================================================
2+
-- Thayfinance — Ajustes de segurança apontados pelo Security Advisor
3+
-- Cole este script inteiro no SQL Editor do Supabase e clique em RUN.
4+
-- É IDEMPOTENTE (pode rodar quantas vezes quiser).
5+
-- Resolve os avisos:
6+
-- - "Function Search Path Mutable" (set_updated_at)
7+
-- - "Public/Signed-In can execute SECURITY DEFINER function"
8+
-- (handle_new_user, rls_auto_enable) — funções de GATILHO não precisam
9+
-- de permissão de execução para o usuário final.
10+
-- ============================================================
11+
12+
-- 1) Fixa o search_path da função de updated_at (evita "search_path mutável").
13+
create or replace function public.set_updated_at()
14+
returns trigger
15+
language plpgsql
16+
set search_path = ''
17+
as $$
18+
begin
19+
new.updated_at = now();
20+
return new;
21+
end;
22+
$$;
23+
24+
-- 2) Reforça o search_path fixo na handle_new_user (mantém SECURITY DEFINER
25+
-- porque ela insere em profiles no momento do cadastro).
26+
create or replace function public.handle_new_user()
27+
returns trigger
28+
language plpgsql
29+
security definer
30+
set search_path = public
31+
as $$
32+
begin
33+
insert into public.profiles (id, nome)
34+
values (new.id, coalesce(new.raw_user_meta_data->>'nome', ''));
35+
return new;
36+
end;
37+
$$;
38+
39+
-- 3) Remove o EXECUTE público das funções de GATILHO. Triggers rodam como dono
40+
-- da tabela — o usuário final não precisa (nem deve) poder chamá-las direto.
41+
-- Isso zera os avisos "callable by public / signed-in users".
42+
revoke execute on function public.set_updated_at() from public, anon, authenticated;
43+
revoke execute on function public.handle_new_user() from public, anon, authenticated;
44+
45+
-- rls_auto_enable() pode não existir em todos os ambientes — protege com um DO.
46+
do $$
47+
begin
48+
if exists (
49+
select 1 from pg_proc p
50+
join pg_namespace n on n.oid = p.pronamespace
51+
where n.nspname = 'public' and p.proname = 'rls_auto_enable'
52+
) then
53+
execute 'revoke execute on function public.rls_auto_enable() from public, anon, authenticated';
54+
end if;
55+
end$$;
56+
57+
-- ------------------------------------------------------------
58+
-- Observação: o aviso "Extension in Public (pg_net)" NÃO é tratado aqui de
59+
-- propósito — mover a extensão pg_net de schema pode quebrar integrações do
60+
-- Supabase (pg_cron / webhooks). É seguro deixá-la como está.
61+
-- ============================================================

0 commit comments

Comments
 (0)