🔒 AUDITORIA DE SEGURANÇA - RV-ADV

1.2.3 Sanitização de Uploads | 1.2.4 SQL Injection | 1.2.5 XSS Prevention

Data da Auditoria: 2026-04-18
Executor: OpenShell Security Audit
Escopo: Frontend (src/), Edge Functions (supabase/functions/)


📊 RESUMO EXECUTIVO

CategoriaItens AuditadosFindingsSeveridade Máx
Sanitização de Uploads42ALTA
SQL Injection51MÉDIA
XSS Prevention62MÉDIA

1.2.3 SANITIZAÇÃO DE UPLOADS

📍 Localização: src/lib/validation/security-schemas.js

✅ FINDING 1 - Configuração Adequada de ALLOWED_MIME_TYPES

Severidade: INFO
Status: ✅ SEGURO

// Linhas 9-16
export const ALLOWED_MIME_TYPES = [
  "application/pdf",
  "image/jpeg",
  "image/png",
  "image/webp",
  "application/msword",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
];

Avaliação: Lista de MIME types bem definida, excluindo tipos perigosos (.exe, .sh, .html, .js).


⚠️ FINDING 2 - Validação de MIME Type Client-Side Apenas

Severidade: ALTA
CWE: CWE-434 (Unrestricted Upload of File with Dangerous Type)
Localização:

  • src/components/documents/DocumentUpload.jsx:112
  • src/components/documents/ClientDocumentsSection.jsx:518-538

Evidência:

// DocumentUpload.jsx:112 - Aceita apenas por extensão client-side
<input
  type="file"
  accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.docm,.dotx"
  onChange={handleFileChange}
/>

// ClientDocumentsSection.jsx:518-538 - Validação só de tamanho e formato
const validFormats = [
  "application/pdf",
  "image/jpeg",
  "image/png",
  // ...
];

Problema:

  1. O atributo accept HTML pode ser burlado
  2. O file.type pode ser spoofed (ex: enviar .exe como application/pdf)
  3. NÃO HÁ validação server-side do MIME type real do arquivo

Recomendação:

  • Implementar validação server-side na Edge Function ocr-classify-document ou ai-proxy
  • Verificar magic numbers (file signatures) ao invés de confiar em file.type
  • Usar biblioteca como file-type ou magic-bytes no backend

⚠️ FINDING 3 - Ausência de Validação de Nome de Arquivo

Severidade: ALTA
CWE: CWE-22 (Path Traversal)
Localização:

  • src/services/aiService.js:13-16

Evidência:

// aiService.js:13-16
async function uploadFileToStorage(file, bucket = 'client-documents', folder = 'documents') {
  const fileExt = file.name.split('.').pop();  // Vulnerável a path traversal
  const fileName = `${Date.now()}_${Math.random().toString(36).substring(2)}.${fileExt}`;
  const filePath = `${folder}/${fileName}`;  // Não sanitiza o folder

Problema:

  • Nome do arquivo original não é sanitizado antes de extrair extensão
  • Possível path traversal com nomes como ../../../etc/passwd
  • Parâmetro folder não é validado

Recomendação:

// Correção sugerida
function sanitizeFilename(filename) {
  // Remove path traversal
  return filename
    .replace(/\.{2,}[\/\\]/g, '')
    .replace(/[\/\\]/g, '-')
    .replace(/[^a-zA-Z0-9.\-_]/g, '');
}

const fileExt = sanitizeFilename(file.name).split('.').pop();

🔍 FINDING 4 - Limite de Tamanho Inconsistente

Severidade: MÉDIA
Localização: Código vs Schema

Evidência - Schema:

// security-schemas.js:6-7
export const MAX_PAYLOAD_SIZE = 10 * 1024 * 1024;  // 10MB
export const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;   // 50MB

Evidência - Código:

// ClientDocumentsSection.jsx:518
if (file.size > 10 * 1024 * 1024) {  // Usando 10MB, não 50MB
  toast.error("Arquivo muito grande. Máximo 10MB.");
  return;
}

Problema: O schema define MAX_UPLOAD_SIZE=50MB, mas o código usa 10MB. Embora seja mais restritivo (seguro), a inconsistência pode causar confusão.


1.2.4 SQL INJECTION

📍 Localização: Serviços Supabase

⚠️ FINDING 5 - Uso de .ilike com Template String

Severidade: MÉDIA
CWE: CWE-89 (SQL Injection)
Localização: src/services/clientService.js:131

Evidência:

// clientService.js:125-133
async searchByName(searchTerm, limit = 20) {
  const { data, error } = await supabase
    .from(this.table)
    .select("*")
    .or(`full_name.ilike.%${searchTerm}%,cpf_cnpj.ilike.%${searchTerm}%`)  // ⚠️
    .eq('status', 'ativo')
    .limit(limit);

Problema: Embora o Supabase escape automaticamente parâmetros, template strings com .ilike e .or podem ser vulneráveis a wildcard abuse e em casos específicos de caracteres especiais.

Evidência de Teste:

// Teste: searchTerm = "test%; DROP TABLE clients; --"
// A query resultante seria:
// SELECT * FROM clients WHERE (full_name.ilike.%test%; DROP TABLE clients; --% OR ...)

No entanto, o Supabase/PostgREST normalmente rejeita queries malformadas. O risco real é:

  1. Wildcard abuse: "%%" pode causar DoS (retornar todos registros)
  2. Performance: "%" sem prefixo força full table scan

Recomendação:

// Correção sugerida
async searchByName(searchTerm, limit = 20) {
  // Sanitizar: remover caracteres especiais, limitar wildcards
  const sanitized = searchTerm
    .replace(/[%_]/g, '')  // Remove wildcards SQL
    .trim()
    .slice(0, 100);        // Limita tamanho
  
  if (sanitized.length === 0) return [];
  
  const { data, error } = await supabase
    .from(this.table)
    .select("id, full_name, cpf_cnpj")  // Limita colunas
    .ilike('full_name', `%${sanitized}%`)
    .eq('status', 'ativo')
    .limit(Math.min(limit, 100));

✅ FINDING 6 - RPC Seguro (buscar_jurisprudencia)

Severidade: INFO
Localização: src/services/jurisprudenciaService.js:60

Evidência:

// Linhas 60-64
const { data, error } = await supabase.rpc('buscar_jurisprudencia', {
  query_embedding: embedData.embedding,  // Array de números
  match_count: resolvedMatchCount,          // Número limitado
  similarity_threshold: 0.4,              // Constante
});

Avaliação: ✅ RPC parametrizado corretamente com tipos seguros. Não há concatenação de SQL.


✅ FINDING 7 - Queries BaseService (Prepared Statements)

Severidade: INFO
Localização: src/services/baseService.js

Avaliação: Todas as operações CRUD usam o cliente Supabase nativo que automaticamente parametriza queries:

  • .eq(), .filter(), .select() - Prepared statements implícitos
  • Sem concatenação de SQL
  • Schema Zod para validação de entrada

1.2.5 XSS PREVENTION

📍 Localização: Componentes React

⚠️ FINDING 8 - Uso de dangerouslySetInnerHTML

Severidade: MÉDIA
CWE: CWE-79 (Cross-site Scripting)
Localização:

  • src/components/ui/chart.jsx:61-80
  • src/modules/periciapro/components/ui/chart.jsx:61-80

Evidência:

// chart.jsx:61-80
return (
  <style
    dangerouslySetInnerHTML={{
      __html: Object.entries(THEMES)
        .map(([theme, prefix]) => `
          ${prefix} [data-chart=${id}] {
            ${colorConfig
              .map(([key, itemConfig]) => {
                const color = itemConfig.theme?.[theme] || itemConfig.color;
                return color ? ` --color-${key}: ${color};` : null;
              })
              .join("\n")}
          }
        `)
        .join("\n"),
    }}
  />
);

Análise:

  1. Color é controlado - vem de configuração estatizada (THEMES)
  2. Key vem de colorConfig - se este for derivado de dados de usuário, há risco

Risco: Se colorConfig receber dados do usuário sem sanitização:

// Cenário hipotético de ataque:
// colorConfig = [["key<script>alert(1)</script>", { color: "#fff" }]]
// Resultado: <style> --color-key<script>alert(1)</script>: #fff; </style>

Recomendação:

// Sanitizar keys e values antes de interpolar
const sanitizeCss = (str) => str.replace(/[^a-zA-Z0-9_-]/g, '');

return color ? ` --color-${sanitizeCss(key)}: ${sanitizeCss(color)};` : null;

✅ FINDING 9 - Ausência de DOMPurify/Markdown Sanitization

Severidade: INFO
Localização: Uso de react-markdown

Evidência:

// package.json
"react-markdown": "^9.0.1"

Avaliação:

  • react-markdown v9+ sanitiza HTML por padrão (não renderiza raw HTML)
  • Não há necessidade de DOMPurify explícito
  • Nenhum uso de skipHtml ou escapeHtml desativado encontrado

Severidade: INFO
Localização:

  • src/components/documents/ClientDocumentsSection.jsx:759
  • src/components/documents/DocumentViewer.jsx:39

Evidência:

// ClientDocumentsSection.jsx:759
window.open(doc.file_url, "_blank")

// DocumentViewer.jsx:39
window.open(document.file_url, "_blank");

Análise:

  • file_url vem do Supabase Storage (bucket 'client-documents')
  • As URLs são geradas pelo Supabase (domínio controlado)
  • Não há risco de javascript: scheme injection
  • Status: ✅ SEGURO

✅ FINDING 11 - React Auto-Escape Ativo

Severidade: INFO
Avaliação: Todo o projeto usa React JSX que automaticamente escapa conteúdo dinâmico:

  • {userInput} → Escapado automaticamente
  • <div>{description}</div> → description escapado
  • Não há uso de innerHTML direto (exceto o caso do chart)

📋 MATRIZ DE RISCOS AGREGADOS

#FindingSeveridadeStatusCWEPrioridade Correção
1Upload sem validação server-side de MIMEALTA🔴 ABERTOCWE-434IMEDIATA
2Path traversal em nome de arquivoALTA🔴 ABERTOCWE-22IMEDIATA
3searchByName com template stringsMÉDIA🟡 ABERTOCWE-891 semana
4dangerouslySetInnerHTML em chartMÉDIA🟡 ABERTOCWE-791 semana
5Inconsistência de MAX_UPLOAD_SIZEBAIXA🟢 INFORMATIVO-Quando conveniente
6ALLOWED_MIME_TYPES adequadoINFO✅ OK--
7RPC buscar_jurisprudencia seguroINFO✅ OK--
8Prepared statements em BaseServiceINFO✅ OK--
9react-markdown adequadoINFO✅ OK--
10window.open seguro em documentosINFO✅ OK--
11React auto-escape ativoINFO✅ OK--

🛠️ RECOMENDAÇÕES PRIORITÁRIAS

Prioridade 1 (Imediata - 24h)

  1. Validar MIME type no Edge Function:
// supabase/functions/ocr-classify-document/index.ts
import { fileTypeFromBlob } from 'file-type';

export default async (req: Request) => {
  const blob = await req.blob();
  const fileType = await fileTypeFromBlob(blob);
  
  const ALLOWED_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
  if (!ALLOWED_TYPES.includes(fileType?.mime)) {
    return new Response(JSON.stringify({ error: 'Invalid file type' }), { status: 400 });
  }
  // ...
}
  1. Sanitizar nomes de arquivo:
// src/services/aiService.js
function sanitizePath(input) {
  return input
    .replace(/\.{2,}[\/\\]/g, '')
    .replace(/[\/\\]/g, '-')
    .slice(0, 100);
}

Prioridade 2 (1 semana)

  1. Refatorar searchByName:
// Usar .ilike() de forma segura
const sanitized = searchTerm.replace(/[%_]/g, '').trim();
await supabase
  .from('clients')
  .select('id, full_name, cpf_cnpj')
  .ilike('full_name', `%${sanitized}%`)
  .limit(20);
  1. Sanitizar chart styles:
const sanitizeCss = (str) => str?.replace(/[^a-zA-Z0-9_-]/g, '') || '';

📁 ARQUIVOS REFERENCIADOS

src/lib/validation/security-schemas.js
src/components/documents/DocumentUpload.jsx
src/components/documents/ClientDocumentsSection.jsx
src/components/documents/DocumentViewer.jsx
src/services/aiService.js
src/services/clientService.js
src/services/baseService.js
src/services/jurisprudenciaService.js
src/components/ui/chart.jsx
supabase/functions/ocr-classify-document/index.ts

Relatório gerado automaticamente por OpenShell Security Audit Tool
Baseado em guidelines OWASP, CWE, Supabase Security Best Practices

Built with LogoFlowershow