security: audit remediation batch 2 — error redaction, auth gates, timing-safe comparisons, fail-closed access control#175
Closed
adm01-debug wants to merge 54 commits into
Closed
Conversation
…h hardening - fix(rls): isolate cotacoes SELECT per user_id + supervisor check (CRÍTICO-003) - fix(rls): replace stale JWT role check with is_supervisor_or_above() in UPDATE policy - fix(rls): restrict role_permissions write policies to 'authenticated' role only - perf(db): add missing indexes on webhook_audit_log and empresas - feat(ci): add RLS dangerous-policy audit step (check-rls-dangerous-policies.mjs) - fix(ci): allowlist docs/adr and CONTRIBUTING.md in check-no-db-push guard - fix(csp): remove unsafe-inline from script-src in public/_headers - fix(cors): replace broken nonce placeholder in edge function CSP header - fix(cors): narrow ALLOWED_ORIGIN_PATTERNS to project-specific lovable.app pattern - feat(webhook): add WEBHOOK_DISPATCHER_SECRET shared-secret auth (fail-closed) - fix(security): harden sanitizeHtml() — SVG, CSS injection, all event handler styles - fix(access): explicit agente role case in checkAccess() to prevent bypass - fix(autosave): scope localStorage key by userId to prevent cross-user data bleed - fix(build): use sourcemap:'hidden' to enable Sentry uploads without serving maps https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Comment on lines
12
to
+25
| return html | ||
| // Remove entire dangerous block-level tags including their content | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") | ||
| .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "") | ||
| .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "") | ||
| .replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "") | ||
| .replace(/on\w+="[^"]*"/gi, "") // remove event handlers like onclick | ||
| .replace(/on\w+='[^']*'/gi, "") | ||
| .replace(/href="javascript:[^"]*"/gi, "") | ||
| .replace(/src="javascript:[^"]*"/gi, ""); | ||
| .replace(/<embed\b[^>]*\/?>/gi, "") | ||
| .replace(/<applet\b[^<]*(?:(?!<\/applet>)<[^<]*)*<\/applet>/gi, "") | ||
| // Strip SVG entirely (can contain onload and foreign objects) | ||
| .replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, "") | ||
| // Remove all event handler attributes (on* in any quote style or unquoted) | ||
| .replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, "") |
Comment on lines
12
to
+17
| return html | ||
| // Remove entire dangerous block-level tags including their content | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") | ||
| .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "") |
…ize.ts SSOT All 16 remaining edge functions that still imported from _shared/auth.ts (authenticateRequest / authErrorResponse / requireDev) are now using the _shared/authorize.ts SSOT (authorize()). This eliminates 16 divergent auth implementations and closes gaps where error paths could leak auth exceptions as 500s instead of proper 401/403 responses. Changes per function: - Import: authenticateRequest/authErrorResponse → authorize - Call: await authenticateRequest(req) → await authorize(req); if (!auth.ok) return auth.response - Property: auth.userId → auth.user.id, auth.localServiceClient → auth.supabaseAdmin - connections-hub-audit: requireDev + nested try/catch → auth.role !== 'dev' check with 403 - rbac-edge-functions_test: update pattern to match new authorize API https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Upgrades jsdom from ^20.0.3 to ^29.1.1, resolving: - GHSA-vpq2-c234-7xj6: @tootallnate/once Incorrect Control Flow Scoping (low) - http-proxy-agent 4.0.1-5.0.0 depends on vulnerable @tootallnate/once (low) - jsdom 16.6.0-22.1.0 depends on vulnerable http-proxy-agent (low) Remaining: 2 moderate (esbuild via vite dev-server — dev-only, no prod impact). esbuild fix requires vite@8 (major breaking change, tracked separately). https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
The project had both sonner and react-hot-toast installed. Only one file used react-hot-toast (FilterPanelHeader). Migrated to sonner and uninstalled the duplicate library, reducing bundle size and dependency surface. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- Remove 6 deleted edge functions from edge-authz-manifest.ts (bi-share-dossier, collections-public-react, comparisons-public-react, generate-mockup-nanobanana, kit-public-view, quote-public-view) - Add cors-audit (dev-only) to edge-authz-manifest.ts - Remove same 6 from LEGACY_ALLOWLIST in check-edge-structured-logging.mjs and update SNAPSHOT_SIZE from 84 to 78 - Rebuild cors-snapshot.json (77 functions, all via _shared/cors) Fixes CI gate failures: edge-authorization and cors-snapshot-freshness. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Replace lovable.app preview URLs with promogifts.com.br in: - canonical link - og:url and twitter:url - JSON-LD structured data url Also correct connections-hub-audit manifest category from 'service' to 'dev' (code enforces auth.role === 'dev' inline). https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Implement useIdleTimeout hook that automatically signs out authenticated users after 30 minutes of inactivity. Shows a warning toast 2 minutes before logout. Activity events: mousemove, keydown, click, touchstart, scroll. Wired into AppBootstrapContainer so it runs for all authenticated routes. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…AIXO-002) - Migration 20260513040000: insert dev entries in role_permissions by copying all admin permissions. Eliminates the need for the legacy admin→dev mapping in the frontend. - useRBAC.tsx: toDbRole now returns 'dev' directly for the dev role instead of 'admin'. Type extended from 'admin|manager|vendedor' to include 'dev'. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Audit document:
- docs/AUDIT_INDEPENDENT_2026-05-13.md — exhaustive fresh-eyes audit
covering RLS, edge functions, cron jobs, storage policies, RBAC,
CI/CD, and AI quota system
Fixes applied (highest severity first):
C-001 (CRÍTICO): Remove dead quote-public-view from config.toml
- Function folder does not exist; orphaned entry caused confusion
- supabase/config.toml: remove [functions.quote-public-view] block
C-002 (CRÍTICO): Fix cron job pointing to wrong Supabase project
- Migrations 20260424154125 and 20260429163414 hardcoded project ref
nmojwpihnslkssljowjh (dev/staging) instead of doufsxqlfjyuvxuezpln
- Migration 20260513050000 reschedules connections-auto-test cron at
the correct project URL with CRON_SECRET auth header
A-001 (ALTO): Add auth gate to connections-auto-test
- Any caller could trigger expensive connection tests (no auth check)
- Now requires x-cron-secret (CRON_SECRET env) or service-role JWT
- Paired with migration 20260513050000 that updates cron to send secret
A-002 (ALTO): Fix semantic-search manifest classification
- Was "public" but calls authorize() and returns 401 for anon users
- Corrected to "authenticated" in edge-authz-manifest.ts
M-002 (MÉDIO): Fix quarantine storage policy using email matching
- Old: auth.jwt() ->> 'email' LIKE '%admin%' (bypassable)
- New: is_admin_or_above(auth.uid()) — role-based, not email-based
- Migration 20260513060000 replaces all three quarantine policies
https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
CRITICAL: Two DB helper functions existed only in the live database, never captured in migrations. Fresh deploys would fail on 98+ policies. is_admin_or_above() bug: - Old impl: has_role(dev) OR has_role(admin) — missing 'supervisor' - Effect: supervisors silently denied access to 98 RLS-protected resources - Fix: delegate to is_supervisor_or_above() (covers dev|supervisor|admin|manager) is_coord_or_above() bug: - Old impl: has_role(dev) OR has_role(admin) OR has_role(coordenador) - 'coordenador' is not a valid app_role enum value — always false - Fix: delegate to is_supervisor_or_above() Also updates quarantine storage policy migration to use is_supervisor_or_above() directly (avoid double indirection). Applied directly to production DB via MCP apply_migration. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Security definer hardening gate requires REVOKE EXECUTE FROM anon or a '-- rls-helper:' comment for functions callable by RLS policies. Added the marker since both functions are RLS helpers. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Both functions call authorize() internally and return 401 for unauthenticated requests, but were classified as 'public' in the auth manifest. Corrected to 'authenticated' so the manifest accurately reflects behavior. 3 functions total moved from public to authenticated (semantic-search, visual-search, analyze-logo-colors). https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…e to config - product-webhook had fail-open auth: if N8N_PRODUCT_WEBHOOK_SECRET env var was not set, the secret check was skipped and any caller could invoke the webhook. Fixed to fail-closed: reject all requests when secret not configured. - Added [functions.product-webhook] verify_jwt = false to config.toml so n8n callers (no Supabase JWT) can reach the function at all. - Improved error log to structured JSON with failure reason. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…omparison - send-transactional-email, comparison-ai-advisor: replace deprecated `serve()` from deno.land/std@0.168.0 with built-in `Deno.serve()`. The std HTTP server shim is unmaintained; Deno.serve() is the Supabase-recommended runtime pattern. - comparison-ai-advisor: unify zod import to esm.sh/zod@3.23.8 matching the rest of the codebase (was deno.land/x/zod@v3.22.4, a different module graph branch causing potential duplicate module loading). - product-webhook: upgrade to timing-safe secret comparison to prevent theoretical timing oracle attacks on the webhook secret. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…nctions 13 edge functions classified as "service" in the authz manifest had no in-function auth check — any authenticated user's JWT could call them, enabling notification injection, mass email triggers, and bulk data reads. Fix: add CRON_SECRET || SERVICE_ROLE_KEY gate to each function + set verify_jwt=false in config.toml so pg_cron can reach them without a user JWT. Functions hardened: cleanup-notifications, cleanup-novelties, collections-watcher, favorites-watcher, comparison-price-watcher, process-queue, process-scheduled-reports, send-scheduled-reports, send-digest, quote-followup-reminders, connections-health-check, send-notification, ownership-audit. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- webhook-dispatcher: add isAllowedWebhookUrl() to block RFC1918/loopback/ link-local targets before fetch (defense-in-depth against SSRF via admin-created webhooks pointing to internal services) - commemorative-dates: redact raw DB error messages in public responses; log details server-side only - rate-limit-check: redact error.message in catch response (public endpoint) - elevenlabs-tts, voice-agent: upgrade zod from deno.land/x v3.22.4 to esm.sh v3.23.8 to match project canonical import https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Service functions (cleanup-notifications, cleanup-novelties, collections-watcher, favorites-watcher, comparison-price-watcher, process-queue, process-scheduled-reports, send-scheduled-reports, send-digest, quote-followup-reminders, connections-health-check, ownership-audit) were classified as "service/cron" in the authz manifest but had no actual cron schedule calling them. Migration creates all 12 schedules with: - CRON_SECRET header when app.cron_secret is configured (secure) - Graceful fallback scheduling with RAISE WARNING when secret not set Schedules: hourly for reports, daily for watchers/cleanup/reminders, every 10-15 min for queue and health-check, weekly for send-digest. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
… HTML Add escapeHtml() and safeHref() helpers and apply them to all user- controlled data interpolated into email templates. Previously, values like recipient_name, quote_number, client_name, notes, order_number, valid_until and approval_url were inserted as raw strings, enabling HTML injection that could create phishing content or inject unexpected structure into the email. safeHref() additionally validates that approval_url is an https:// URL, rejecting javascript: URIs and relative paths that could execute code in email clients that parse href attributes. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- process-scheduled-reports: add escapeHtml() to report_name, summary keys/values in HTML email templates - send-scheduled-reports: add escapeHtml() to report_name, quote/order field values (client_name, status, order_number) in HTML email tables - validate-access: upgrade geolocation API call from http to https - manage-users: block supervisors from assigning dev role during create (privilege escalation — dev role reserved for step-up promote_dev flow) - market-intelligence-insights: suppress internal error message leak in catch block, log internally only - magic-up-score: suppress internal error message leak in catch block - voice-agent: suppress internal error message leak in catch block - elevenlabs-tts: enforce voice ID allowlist (reject unapproved IDs); suppress error message leak - elevenlabs-scribe-token: suppress error message leak https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- quote-sync: add authorize(req) gate — unauthenticated callers could trigger CRM sync of any quote UUID including internal margin fields - categories-api: add authorize(req) gate — was using external service role key without any authentication check - generate-mockup, generate-ad-prompt, generate-ad-image, cnpj-lookup: suppress internal error message leaks in catch blocks (log internally) https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- dropbox-list: add authorize(req) — function had no auth at all, any caller could enumerate company Dropbox files - kit-identity-suggest: add authorize(req) + user-scoped rate limiting (previously only IP-based bot protection, JWT not verified) - mcp-keys-issue/revoke/rotate/update: remove 'detail' field from client error responses — internal error messages were leaked in the JSON body even though only dev role can reach these functions https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…ints Remove `detail` fields from all client-facing error responses in mcp-keys-issue and mcp-keys-revoke to prevent DB/RPC error messages from leaking to callers (even dev-role holders should not receive raw DB error strings via the API surface). https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Replace raw DB/auth error messages returned to clients with generic messages (logged internally). Affected functions: block-ip-temporarily, rls-matrix-export, ownership-repair, manage-users. Even admin-only endpoints should not expose internal DB schema hints via error strings. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…hook and comparison-price-watcher - product-webhook: add .max() constraints to unbounded string fields in colors (name, hex, group) and kit_items (productId, productName, sku) - comparison-price-watcher: replace String(e) error leak in 500 response with generic "internal_error" code; log internally instead https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
bi-copilot, kit-ai-builder, comparison-ai-advisor, and market-intelligence-insights now run runBotProtection with customIdentifier: user:<id> after JWT auth, preventing API key abuse and per-user AI cost exhaustion attacks. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…function Prevents AI cost exhaustion attacks on the trends-insights endpoint by adding runBotProtection with customIdentifier after JWT verification. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
The endpoint can validate arbitrary MCP keys and return their owner (created_by) and scopes. Require is_dev() before allowing access to prevent information disclosure to regular authenticated users. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
The function creates and deletes records in production tables (quotes, orders, discount_approval_requests) as a side-effect of RLS testing. Restrict to supervisors to prevent regular vendors from triggering write operations in core business tables. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
connection-tester and external-db-inspect were checking for the legacy 'admin' role which no longer exists after the role hierarchy migration (20260426010557). Migrate both to use the is_supervisor_or_above() RPC so supervisors and devs can access these diagnostic tools again. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…or_above() After the role hierarchy migration (20260426010557), the 'admin' role no longer exists in user_roles — all admin users were migrated to 'supervisor'. Functions checking for role='admin' would always deny access or send no notifications. Fixes: - connections-health-check: notify supervisors+devs, not just 'admin' - block-ip-temporarily: use is_supervisor_or_above() RPC - rls-integration-tests: use is_dev() RPC (dev-only operation) - github-credentials-test: use is_supervisor_or_above() RPC https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- useDiscountApproval: query supervisor/dev/admin instead of admin-only
(admin role removed by migration 20260426010557, no notifications sent)
- rls-matrix-export: replace has_role("admin") + has_role("dev") with
is_supervisor_or_above() so supervisors can export RLS matrix
- cnpj-lookup: add user-scoped bot protection (20 req/min) to protect
paid CNPJá API from cost-exhaustion abuse
https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
- secure-upload: strip <script>, event handlers and javascript: hrefs from SVG files before storing to prevent stored XSS via user uploads - image-proxy: add X-Content-Type-Options: nosniff to all responses; add Content-Security-Policy: script-src 'none' for SVG responses to prevent script execution when proxied SVG is opened directly https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
After migration 20260426010557, all admin/manager users were migrated to
supervisor, but is_manager_or_admin() still checked role IN ('admin','manager')
— silently denying supervisors access to RLS-protected resources.
Redefine both overloads to delegate to is_supervisor_or_above() which
correctly covers dev | supervisor | admin | manager (including legacy enum
values kept for backward compatibility).
Same pattern already applied to is_admin_or_above() in 20260513070000.
https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
Comment on lines
+102
to
+104
| const svgText = new TextDecoder().decode(fileBuffer) | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") | ||
| .replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, "") |
Comment on lines
+102
to
+103
| const svgText = new TextDecoder().decode(fileBuffer) | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") |
| // Strip scripts and event handlers from SVG uploads to prevent XSS when served. | ||
| if (file.type === "image/svg+xml") { | ||
| const svgText = new TextDecoder().decode(fileBuffer) | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") |
…sor_or_above()
After migration 20260426010557 removed 'admin' and 'manager' from user_roles,
multiple DB functions and RLS policies still checked has_role('admin') which
always returned false, silently locking supervisors out of critical operations.
Fixes:
- can_view_all_sales(): supervisors could not view all quotes/orders (CRITICAL)
- integration_credentials RLS: supervisors locked out of credentials table
- get_app_health_summary(): supervisors could not access health dashboard
- lookup_request_id(): supervisors could not use cross-layer request timeline
- ownership_repair_logs SELECT policy: supervisors could not read repair logs
- repair_ownership_orphans(): supervisors blocked from running ownership repair
- audit_rls_matrix(): supervisors could not call RLS audit function
- audit_ownership_orphans(): supervisors blocked from ownership audit
All guards replaced with is_supervisor_or_above() which covers dev, supervisor,
and legacy admin/manager enum values.
https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…o get-visitor-info authorize.ts: ROLE_RANK used AppRole type which excluded 'vendedor'. Since user_roles stores 'vendedor' (not 'agente'), ROLE_RANK['vendedor'] was undefined. In JavaScript, undefined < 2 evaluates to false, so vendedor users passed supervisor role checks silently. Fixed by: - Adding vendedor, admin, manager to ROLE_RANK with correct values - Adding ?? 0 fallback on userRank to handle any unknown future role values get-visitor-info: called ip-api.com (45 req/min free tier) with no rate limit. Added runBotProtection at 30 req/min per IP to prevent exhausting the free tier and protect against scraping abuse. https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
…th of authorize.ts
Previously the enforceServerSide path made two has_role() calls for supervisor
check: first has_role('supervisor'), then has_role('dev') as fallback. This
could fail for users with legacy 'admin'/'manager' role enum values.
Replaced with single is_supervisor_or_above() call which covers all cases via
SECURITY DEFINER, and is_dev() for the dev-only path. Reduces RTT from 2 RPC
calls to 1 when the caller has a legacy role value.
https://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Continuing independent pre-production security audit remediation (batch 2).
Error Message Redaction (Information Disclosure)
error.message/err.messageleaks in JSON responses with generic codes ("internal_error","query_failed", etc.) across 20+ edge functionssend-transactional-email,step-up-verify,bi-copilot,categories-api(public!),kit-identity-suggest,ownership-audit,ownership-repair,rls-matrix-export,rls-audit,rls-integration-tests,process-scheduled-reports,send-scheduled-reports,external-db-bridge,crm-db-bridge,bitrix-sync,external-db-inspect,webhook-dispatcherconsole.error()for server-side observabilityMissing Auth Gates
bi-copilot: Was calling expensive AI API (Lovable AI Gateway) with no authentication. Addedauthorize()gate + bounded input validation (question ≤500 chars, context ≤8KB, history ≤20 items)sync-quote-bitrix: Listed asauthenticatedin manifest but had no auth gate. Addedauthorize()gateInput Validation (Zod Runtime Validation)
step-up-verify: Replaced unsafe(await req.json()) as RequestBodyTypeScript cast with full Zod schema validation on all fields (stepenum,actionenum,challenge_iduuid, bounded strings)Timing-Safe Secret Comparison
timingSafeStringEqual()andverifyServiceAuth()to_shared/security.ts!==CRON_SECRET comparisons in all 14 service/cron edge functions:process-scheduled-reports,connections-health-check,ownership-audit,send-notification,cleanup-novelties,quote-followup-reminders,process-queue,comparison-price-watcher,collections-watcher,send-scheduled-reports,cleanup-notifications,send-digest,favorites-watcher,connections-auto-testFail-Closed Access Control (Critical)
validate-access: Catch block was returning{ allowed: true }on internal errors (fail-open). Changed to{ allowed: false }— deny on error is the correct security posture for access control gatesBatch 1 (previous commits on this branch)
cotacoestableunsafe-inline, hardenedimg-src)analyze-logo-colorslog-login-attemptdetect-new-devicesecure-upload(MIME allowlist, size limit, path sanitization)crm-db-bridgeandexternal-db-bridgeTest plan
bi-copilotreturns 401 without authenticationsync-quote-bitrixreturns 401 without authenticationstep-up-verifyreturns 400 on invalidstepvaluevalidate-accessreturns{ allowed: false }on DB failure (notallowed: true)node scripts/check-rls-dangerous-policies.mjs— expect 0 violationshttps://claude.ai/code/session_018hD2KUr3SruH7P8vbagj46