Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions .eslintrc.tailwind.js

This file was deleted.

2 changes: 1 addition & 1 deletion scripts/ds-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const DS_CONFIG = {
],
FORBIDDEN_PATTERNS: [
{ pattern: /(?:bg|text|border)-(?:\[(?:#(?:[0-9a-fA-F]{3,6})|rgb|hsl|rgba|hsla)\])/, label: 'Arbitrary Color' },
{ pattern: /(?:bg|text|border)-(?:white|black(?:(?!\-[0-9]))|(?:red|blue|green|yellow|slate|gray|zinc|neutral|stone|orange|amber|lime|emerald|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+)\b/, label: 'Literal Color' },
{ pattern: /(?:bg|text|border)-(?:white|black(?:(?!-[0-9]))|(?:red|blue|green|yellow|slate|gray|zinc|neutral|stone|orange|amber|lime|emerald|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+)\b/, label: 'Literal Color' },
{ pattern: /font-(?:inter|sans|mono|serif)\b/, label: 'Literal Font' },
{ pattern: /(?<!linear-gradient\(|radial-gradient\(|conic-gradient\()#([0-9a-fA-F]{3,6})\b/, label: 'Raw Hex' },
],
Expand Down
3 changes: 2 additions & 1 deletion src/components/contacts/ContactsRichView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,15 @@ export const ContactsRichView: React.FC<ContactsRichViewProps> = () => {
setIsAddDialogOpen(true);
toast.info("Atalho: Novo Registro", { duration: 1000 });
break;
case 'f':
case 'f': {
e.preventDefault();
const searchInput = document.querySelector('input[placeholder*="Buscar"]') as HTMLInputElement;
if (searchInput) {
searchInput.focus();
toast.info("Atalho: Focar Busca", { duration: 1000 });
}
break;
}
case 'g':
e.preventDefault();
state.setViewMode('grid');
Expand Down
2 changes: 1 addition & 1 deletion src/components/security/RateLimitRealtimeAlerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function RateLimitRealtimeAlerts() {
const audio = new Audio('/notification.mp3');
audio.volume = 0.5;
audio.play().catch(() => {});
} catch (e) {}
} catch { /* intentionally empty */ }
};

const handleDismiss = async (alertId: string) => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/components/PasswordStrengthMeter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const requirements: PasswordRequirement[] = [
{ label: 'Letra maiúscula (A-Z)', test: (p) => /[A-Z]/.test(p), weight: 1 },
{ label: 'Letra minúscula (a-z)', test: (p) => /[a-z]/.test(p), weight: 1 },
{ label: 'Número (0-9)', test: (p) => /\d/.test(p), weight: 1 },
{ label: 'Caractere especial (!@#$%)', test: (p) => /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(p), weight: 1 },
{ label: 'Caractere especial (!@#$%)', test: (p) => /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(p), weight: 1 },
];

// Simple hash function for k-anonymity check
Expand Down
2 changes: 1 addition & 1 deletion src/features/connections/hooks/useConnectionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function useConnectionsManager() {
} else {
sessionStorage.setItem(QR_STORAGE_KEY, JSON.stringify(qrCodeDialog));
}
} catch {}
} catch { /* intentionally empty */ }
}, [qrCodeDialog]);

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/inbox/components/chat/MarkdownPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function formatWhatsAppText(text: string): string {
// Code blocks (```...```) — must come before inline
.replace(/```([\s\S]*?)```/g, '<code class="bg-muted/50 px-1.5 py-0.5 rounded text-xs ">$1</code>')
// Bold (*...*) — not greedy, avoid matching **
.replace(/\*([^\*]+)\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<strong>$1</strong>')
// Italic (_..._)
.replace(/_((?!_)[^_]+)_/g, '<em>$1</em>')
// Strikethrough (~...~)
Expand Down
2 changes: 1 addition & 1 deletion src/features/inbox/components/linkPreviewUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// URL regex pattern
export const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
export const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi;

const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'];
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov'];
Expand Down
3 changes: 1 addition & 2 deletions src/features/inbox/hooks/useScheduledMediaUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export function useScheduledMediaUpload() {
const uploadScheduledMedia = useCallback(
async (attachment: File): Promise<UploadResult> => {
const fileName = `scheduled_${Date.now()}_${attachment.name}`;
let mediaUrl: string | undefined;
let messageType = 'text';

const { error: uploadError } = await supabase.storage
Expand All @@ -49,7 +48,7 @@ export function useScheduledMediaUpload() {
.from('whatsapp-media')
.createSignedUrl(fileName, 604800);

mediaUrl = signedData?.signedUrl;
const mediaUrl = signedData?.signedUrl;

messageType = attachment.type.startsWith('audio')
? 'audio'
Expand Down
2 changes: 1 addition & 1 deletion src/features/inbox/hooks/useSipClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function useSipClient() {

const findContactByPhone = useCallback(async (phone: string): Promise<string | null> => {
try {
const n = phone.replace(/[\s\-\(\)]/g, '');
const n = phone.replace(/[\s\-()]/g, '');
const { data, error } = await supabase.from('contacts').select('id').or(`phone.eq.${n},phone.eq.+${n},phone.ilike.%${n.slice(-8)}%`).limit(1).maybeSingle();
Comment on lines +44 to 45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verifica onde a string .or(...) está sendo montada com interpolação direta
rg -nP --type=ts -C3 '\.or\(`phone\.eq\.\$\{n\},phone\.eq\.\+\$\{n\},phone\.ilike\.\%\$\{n\.slice\(-8\)\}\%`\)'
rg -nP --type=ts -C3 '\bfindContactByPhone\s*\('

Repository: adm01-debug/zapp-web

Length of output: 1480


🏁 Script executed:

# Lê o arquivo completo para entender o contexto
cat -n src/features/inbox/hooks/useSipClient.ts | head -80

Repository: adm01-debug/zapp-web

Length of output: 4749


🏁 Script executed:

# Procura por outras instâncias de phone normalization ou queries diretas ao Supabase
rg -n 'phone\s*=' src/features/inbox/hooks/useSipClient.ts -A 2

Repository: adm01-debug/zapp-web

Length of output: 46


🏁 Script executed:

# Verifica se há migrations ou schema que defina o tipo do campo phone
fd -e sql -e ts | xargs rg -l 'contacts.*phone' | head -5

Repository: adm01-debug/zapp-web

Length of output: 315


🏁 Script executed:

# Procura por outros usos de .or() com interpolação de strings
rg -n '\.or\(`' src/features/inbox/hooks/ -A 1

Repository: adm01-debug/zapp-web

Length of output: 1226


🏁 Script executed:

# Procura por migrations que definem o campo phone
fd -e sql | xargs grep -l 'phone' 2>/dev/null | head -10

Repository: adm01-debug/zapp-web

Length of output: 824


🏁 Script executed:

# Verifica policies de RLS para contacts
fd -e sql | xargs grep -A 3 'policy.*contacts' 2>/dev/null | head -20

Repository: adm01-debug/zapp-web

Length of output: 2142


🏁 Script executed:

# Verifica se há sanitização em useNewConversation que também tem o mesmo padrão
sed -n '40,60p' src/features/inbox/hooks/useNewConversation.ts

Repository: adm01-debug/zapp-web

Length of output: 765


🏁 Script executed:

# Procura pela definição mais recente do campo phone nas migrations
rg -A 5 'CREATE TABLE.*contacts|ALTER TABLE.*contacts' supabase/migrations/ | head -40

Repository: adm01-debug/zapp-web

Length of output: 4262


🏁 Script executed:

# Verifica o tipo do campo phone
rg 'phone\s+(text|varchar|character|numeric|bigint)' supabase/migrations/

Repository: adm01-debug/zapp-web

Length of output: 1293


🏁 Script executed:

# Verifica se há função ou trigger que normaliza phone
rg -i 'normalize.*phone|phone.*format' supabase/

Repository: adm01-debug/zapp-web

Length of output: 5370


Sanitização insuficiente permite injeção de filtro PostgREST

O regex atual /[\s\-()]/g deixa passar caracteres especiais (vírgula, aspas, ponto, etc.) que quebram a sintaxe do filtro .or(). Uma entrada como 123",phone.eq.456,phone.ilike.% passa intacta, corrompendo a query.

Use /\D/g para manter apenas dígitos (números de telefone só precisam disso):

-      const n = phone.replace(/[\s\-()]/g, '');
+      const n = phone.replace(/\D/g, '');
+      if (!n) return null;
       const { data, error } = await supabase.from('contacts').select('id').or(`phone.eq.${n},phone.eq.+${n},phone.ilike.%${n.slice(-8)}%`).limit(1).maybeSingle();

⚠️ Mesmo problema existe em useNewConversation.ts:51: searchQuery não tem sanitização e passa direto para .or(\name.ilike.%${searchQuery}%,phone.ilike.%${searchQuery}%`)`. Adicione validação lá também.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const n = phone.replace(/[\s\-()]/g, '');
const { data, error } = await supabase.from('contacts').select('id').or(`phone.eq.${n},phone.eq.+${n},phone.ilike.%${n.slice(-8)}%`).limit(1).maybeSingle();
const n = phone.replace(/\D/g, '');
if (!n) return null;
const { data, error } = await supabase.from('contacts').select('id').or(`phone.eq.${n},phone.eq.+${n},phone.ilike.%${n.slice(-8)}%`).limit(1).maybeSingle();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/inbox/hooks/useSipClient.ts` around lines 44 - 45, The phone
sanitization in useSipClient.ts currently uses phone.replace(/[\s\-()]/g, '')
which still allows special characters into the PostgREST .or() filter and can
enable injection; change the cleaning to remove all non-digits (usePhone ->
create n from phone by stripping using a non-digit regex) so the value passed to
supabase.from('contacts').select(...).or(...) is only digits, and fail/return
early if result is empty; also fix the analogous issue in useNewConversation.ts
where searchQuery is interpolated into
.or(`name.ilike.%${searchQuery}%,phone.ilike.%${searchQuery}%`) by
validating/sanitizing searchQuery (either allow a safe subset of characters or
escape/normalize percent/quote characters, or reject unsafe input) before
building the query.

return data?.id || null;
} catch { return null; }
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useActionFeedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function useActionFeedback() {
return new Promise((resolve) => {
const { description, undoDuration = 5000, onUndo, onConfirm } = options;
let undone = false;
// eslint-disable-next-line prefer-const -- closure: timeoutId is referenced inside toast `action.onClick` (declared above) and only assigned later by setTimeout
let timeoutId: NodeJS.Timeout;
const toastResult = showFeedback('info', {
description, duration: undoDuration,
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useCSAT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ export function useCSAT(period: 'today' | 'week' | 'month' = 'month') {
switch (period) {
case 'today':
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
case 'week':
case 'week': {
const weekAgo = new Date(now);
weekAgo.setDate(weekAgo.getDate() - 7);
return weekAgo.toISOString();
case 'month':
}
case 'month': {
const monthAgo = new Date(now);
monthAgo.setMonth(monthAgo.getMonth() - 1);
return monthAgo.toISOString();
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useThemeAudit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const useThemeAudit = () => {
try {
const parsed = JSON.parse(saved);
presetId = parsed.preset || DEFAULT_PRESET_ID;
} catch (_e) {}
} catch { /* intentionally empty */ }
}

const preset = PRESETS.find(p => p.id === presetId);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/csvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function getCsvFilename(prefix: string, suffix?: string): string {
const date = new Date().toISOString().slice(0, 10);
const parts = [prefix, suffix, date].filter(Boolean).join('-');
// Strip unsafe filename chars
return parts.replace(/[^a-zA-Z0-9_\-\.]/g, '_') + '.csv';
return parts.replace(/[^a-zA-Z0-9_.-]/g, '_') + '.csv';
}

// ── Compat wrappers ──────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion src/lib/evolutionDirectClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function getDirectConfig(): DirectEvoConfig | null {
if (!raw) return null;
const cfg = JSON.parse(raw) as DirectEvoConfig;
if (cfg.evolution_api_url && cfg.evolution_api_key) return cfg;
} catch {}
} catch { /* intentionally empty */ }
return null;
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function truncate(str: string, maxLength: number): string {
export function sanitizeDisplayName(name: string, maxLength = 100): string {
// Remove zero-width characters, control chars, and direction overrides
const cleaned = name
// eslint-disable-next-line no-control-regex -- intentionally matches control chars (U+0000-001F, U+007F-009F) to strip them
.replace(/[\u200B-\u200F\u202A-\u202E\uFEFF\u0000-\u001F\u007F-\u009F]/g, '')
.trim();

Expand All @@ -112,7 +113,7 @@ export function sanitizeDisplayName(name: string, maxLength = 100): string {
export function sanitizeJid(jid: string): string | null {
// Individual: 5511999999999@s.whatsapp.net
// Group: 120363XXXX@g.us
const jidPattern = /^[\d\-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/;
const jidPattern = /^[\d-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Regex de JID permite entradas inválidas sem dígitos.

Com ^[\d-]+...$, valores como ---@s.whatsapp.net passam na validação, embora o formato esperado no próprio docstring exija dígitos (ou digits-timestamp para grupos). Isso pode deixar JID inválido seguir no fluxo.

Diff sugerido
-  const jidPattern = /^[\d-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/;
+  const jidPattern = /^(?:\d+@s\.whatsapp\.net|\d+-\d+@g\.us|\d+@(?:lid|newsletter))$/;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const jidPattern = /^[\d-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/;
const jidPattern = /^(?:\d+@s\.whatsapp\.net|\d+-\d+@g\.us|\d+@(?:lid|newsletter))$/;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/security.ts` at line 116, A constante jidPattern aceita strings sem
dígitos porque permite sequências de hífens; atualize jidPattern (a constante
jidPattern) para exigir explicitamente uma sequência inicial de um ou mais
dígitos, opcionalmente seguida de um hífen e outra sequência de um ou mais
dígitos (ou seja, "digits" ou "digits-hyphen-digits"), antes do @ e do sufixo
(s.whatsapp.net | g.us | lid | newsletter), para impedir entradas como
"---@s.whatsapp.net".

const trimmed = jid.trim();

if (jidPattern.test(trimmed)) {
Expand Down
3 changes: 1 addition & 2 deletions src/pages/admin/AdminStressTestPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export default function AdminStressTestPage() {
type: StressTaskType; idx: number; phone: string; instance: string;
}) => {
const sample = await sampleFor(type, idx);
let messageId: string | undefined;
let result: any;
let accessibility: StressResult['accessibility'] | undefined;

Expand Down Expand Up @@ -162,7 +161,7 @@ export default function AdminStressTestPage() {
} as any);
break;
}
messageId = result?.key?.id ?? result?.messageId ?? result?.id;
const messageId = result?.key?.id ?? result?.messageId ?? result?.id;
return { messageId, detail: sample.detail, accessibility };
}, [evo]);

Expand Down
Loading