Review ID: b3e9d20f15fbGenerated: 2026-05-06T14:01:21.355Z
CHANGES REQUESTED
321
AI-Confirmed Threats
309
Raw Findings
41
Critical
255
High
13
Medium
AI-Confirmed Breakdown
321
Confirmed Threats
8
Critical
267
High
34
Medium
36 of 108 Agents Deployed
DiamondPlatinumGoldSilverBronzeHR RoastyFree Baseline
Agent Tier: Gold
willchen96/mike →
main @ d969096
AIAI Threat Analysis
REAL THREATS
Tenant Isolation & Authorization Bypass (Critical) The entire application lacks Row-Level Security (RLS) on multi-tenant tables (findings 0, 1). The backend auth middleware (finding 6) uses the service role key for token verification, which bypasses all RLS. All server-side Supabase clients (findings 2, 36, 100-103) use the service role key without tenant scoping. This means any authenticated user can access any other user's data. The download endpoint (finding 9) accepts tokens without verifying tenant membership. Document version loading (finding 3) lacks tenant verification. User settings (findings 110, 112) are fetched without tenant verification. The dev fallback in supabase-server.ts (findings 37-39) accepts raw tokens as user IDs with no validation.
Secrets Exposure (Critical) R2 storage credentials (findings 22-31) are hardcoded in client-side code (frontend/src/lib/storage.ts), exposing AWS access keys, secret keys, endpoint URLs, and bucket names to every user. The Supabase service role key (findings 32-35) is exposed in frontend code (frontend/src/lib/supabase-server.ts). API keys are stored in plaintext in the database (findings 40, 1067-1068, 1093). The download token signing secret (findings 63-80, 1074-1076) has a hardcoded fallback of 'dev-secret'.
Denial of Wallet (Critical) LLM API calls (findings 4, 5, 7, 10, 15-17) have no max_tokens or cost controls, allowing an attacker to exhaust the API budget. Chat endpoints (findings 7-8, 10-11) lack per-user rate limiting. Tabular review API (finding 12) lacks rate limiting. Document conversion (finding 52) has no file size limits.
Authentication Weaknesses (Critical/High) The frontend auth helper (findings 19-21, 311-323) uses the Supabase anon/public key instead of the service role key for JWT token validation on the server side. This is fundamentally broken - the anon key cannot validate tokens. The backend auth middleware (findings 1094, 1097) uses the service role key for token verification instead of the JWT secret. Account deletion (findings 13-14, 173-177) uses admin.deleteUser without ownership verification or re-authentication.
Injection & XSS (High) Command injection via LibreOffice conversion (findings 50, 53). XXE via XML parsing (findings 58, 61). Unsafe innerHTML via ReactMarkdown (findings 195-196, 206, 236-237, 240-241, 249-253, 257-258, 261-263, 270-273). DOM injection via LLM-generated content (findings 210-211). SQL injection via string concatenation (finding 124).
SSRF (High) User-controlled API base URLs in fetch requests (findings 56, 83, 148, 157, 178, 238, 242, 264, 276, 281, 285, 290). User-controlled document download URLs (finding 49). SSRF via download token path (finding 143). SSRF via LLM tool execution (finding 168).
Broken Object-Level Authorization (High) Multiple endpoints (findings 47-48, 126, 131-132, 134, 137) lack proper authorization checks, allowing users to access, modify, or delete resources belonging to other users.
IDOR (High) Chat retrieval (finding 130) allows unauthorized access via project membership. Chat streaming (finding 136) allows unauthorized message posting. Project chat (finding 146) allows unauthorized chat reuse.
Error Information Disclosure (High) Database error messages (findings 125, 127-129, 133, 135, 147, 151, 172, 181) are exposed to clients. Internal error details (finding 135) are leaked.
Logging Sensitive Data (High) LLM API keys and responses (findings 85-88, 91-93) are logged to console and files. Auth tokens (findings 200-203, 214-215, 232-235, 244-247, 283, 287-289, 297) are logged or sent in fetch requests.
Missing Input Validation (High) File uploads (findings 104, 220, 228, 254) lack content validation. Chat messages (finding 123) lack input validation. Query parameters (finding 163) are unvalidated.
Missing Audit Logging (High) Document operations (findings 140-141), project sharing (finding 149), tabular review sharing (finding 160), workflow sharing (finding 179), account deletion (finding 174), and data exports (finding 269) lack audit trails.
Client-Side Security Bypasses (High) Model selection (findings 94, 97, 190, 207, 217, 255, 291, 294) is controlled client-side, bypassing API key enforcement. Credit system (finding 18) can be bypassed via client-side logic.
ATTACK CHAINS
1. Full Account Takeover Chain: The service role key exposed in frontend code (finding 32-35) + the auth middleware using service role key for verification (finding 6) + the dev fallback accepting raw tokens (findings
309 raw scanner findings — 41 critical · 255 high · 13 medium
▶ Raw Scanner Output — 1075 pre-cleanup findings
⚠ Pre-Cleanup Report
This is the raw, unprocessed output from all scanner agents before AI analysis. Do not use this to fix issues individually. Multiple agents attack from different angles and frequently report the same underlying vulnerability, resulting in significant duplication. Architectural issues also appear as many separate line-level findings when they require a single structural fix.

Use the Copy Fix Workflow button above to get the AI-cleaned workflow — it deduplicates findings, removes false positives, and provides actionable steps. This raw output is provided for transparency and audit purposes only.
Showing top 1000 of 1075 findings (sorted by severity). Full data available via the review API.
CRITICALR2 credentials exposed in client-side code
frontend/src/lib/storage.ts:23
[AGENTS: Vault]secrets
**Perspective 1:** R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY are used in a client-side file (frontend/src/lib/storage.ts). These credentials are bundled with the frontend and exposed to all users, allowing anyone to read/write/delete objects in the R2 bucket. The backend has an identical copy (backend/src/lib/storage.ts) which is the correct location for server-side storage operations. **Perspective 2:** R2_SECRET_ACCESS_KEY is referenced in client-side code. This secret key grants full access to the R2 storage bucket. Any user who can view the frontend JavaScript can extract this key and access the storage bucket directly.
Suggested Fix
Remove frontend/src/lib/storage.ts entirely. All R2 operations must be performed server-side via the backend API. The frontend should only receive pre-signed URLs or proxied content from the backend.
CRITICALR2 access key ID exposed in client-side code
frontend/src/lib/storage.ts:24
[AGENTS: Vault]secrets
R2_ACCESS_KEY_ID is referenced in client-side code. While less sensitive than the secret key, it still provides information that aids in attacking the storage infrastructure.
Suggested Fix
Remove the client-side storage module. All storage operations should go through the backend API.
CRITICALR2 endpoint URL exposed in client-side code
frontend/src/lib/storage.ts:28
[AGENTS: Vault]secrets
R2_ENDPOINT_URL is referenced in client-side code, revealing the Cloudflare R2 endpoint URL. Combined with the exposed credentials, this gives attackers all information needed to access the storage bucket.
Suggested Fix
Remove the client-side storage module. The R2 endpoint should only be configured server-side.
CRITICALR2 bucket name exposed in client-side code
frontend/src/lib/storage.ts:31
[AGENTS: Vault]secrets
R2_BUCKET_NAME is referenced in client-side code with a default value of 'mike'. This exposes the storage bucket name to all users.
Suggested Fix
Remove the client-side storage module. Bucket names should only be configured server-side.
CRITICALS3Client with credentials created in client-side code
frontend/src/lib/storage.ts:40
[AGENTS: Vault]secrets
An S3Client is instantiated with R2 credentials in client-side code. This allows direct programmatic access to the R2 bucket from any user's browser.
Suggested Fix
Remove this entire file. Create a backend API endpoint that handles file uploads/downloads and returns pre-signed URLs for direct client access.
CRITICALUpload function with credentials exposed to client
frontend/src/lib/storage.ts:52
[AGENTS: Vault]secrets
The uploadFile function creates an S3Client with credentials and sends PutObjectCommand from client-side code. Any user can upload arbitrary files to the R2 bucket.
Suggested Fix
Remove this function from client-side code. Implement a backend upload endpoint that validates user permissions before uploading.
CRITICALDownload function with credentials exposed to client
frontend/src/lib/storage.ts:68
[AGENTS: Vault]secrets
The downloadFile function creates an S3Client with credentials and sends GetObjectCommand from client-side code. Any user can download any file from the R2 bucket.
Suggested Fix
Remove this function from client-side code. Implement a backend download endpoint that validates user permissions.
CRITICALDelete function with credentials exposed to client
frontend/src/lib/storage.ts:88
[AGENTS: Vault]secrets
The deleteFile function creates an S3Client with credentials and sends DeleteObjectCommand from client-side code. Any user can delete any file from the R2 bucket.
Suggested Fix
Remove this function from client-side code. Implement a backend delete endpoint that validates user permissions.
CRITICALSigned URL generation with credentials exposed to client
frontend/src/lib/storage.ts:103
[AGENTS: Vault]secrets
The getSignedUrl function creates an S3Client with credentials and generates pre-signed URLs from client-side code. This exposes the signing process to users.
Suggested Fix
Remove this function from client-side code. Signed URL generation should only happen server-side.
CRITICALServer-side Supabase client uses service role key without tenant scoping
frontend/src/lib/supabase-server.ts:10
[AGENTS: Tenant]tenant_isolation
The `createServerSupabase()` function creates a Supabase client with the service role key (SUPABASE_SECRET_KEY). This key bypasses all Row-Level Security (RLS) policies. Any query made with this client can access all rows across all tenants. If this client is used in API routes without explicit tenant_id WHERE clauses, it will leak data across tenants.
Suggested Fix
Never use the service role key for tenant-scoped queries. Use an authenticated user's JWT token to create a client that respects RLS. If service role is absolutely necessary, add explicit tenant_id filters to every query and audit all usages.
CRITICALDev fallback accepts raw token as user ID with no validation
frontend/src/lib/supabase-server.ts:27
[AGENTS: Tenant]tenant_isolation
When SUPABASE_SECRET_KEY or SUPABASE_URL is missing, the `getUserIdFromRequest` function falls back to returning the raw Bearer token as the user ID. This means any string passed as a token is accepted as a valid user ID, allowing complete impersonation of any user and access to any tenant's data. This is a severe security bypass.
Suggested Fix
Remove the dev fallback entirely. Throw an error if environment variables are not configured. Never accept unvalidated tokens as user identities.
CRITICALUnbounded LLM API calls via project chat endpoint with no per-user rate limiting
backend/src/routes/projectChat.ts:1
[AGENTS: Wallet]denial_of_wallet
The POST /projects/:projectId/chat endpoint triggers an LLM stream for every authenticated request. There is no rate limiting, no per-user spend cap, and no budget circuit breaker. An attacker with valid credentials can send unlimited requests, each incurring LLM API costs. The endpoint also accepts attached_documents which could increase context size and cost.
Suggested Fix
Implement per-user rate limiting, enforce max_tokens, add daily spend caps, and add a circuit breaker. Consider adding a cost estimation step before processing that rejects requests exceeding a budget threshold.
CRITICALNo max_tokens or cost controls on Gemini API calls
backend/src/lib/llm/gemini.ts:1
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** The streamGemini function calls Google's Gemini API with no maxOutputTokens or cost limits. The maxIterations parameter defaults to 10 but does not limit per-iteration token count. An attacker can cause unbounded token generation by sending long prompts or many tool calls, each iteration incurring API costs. **Perspective 2:** The Gemini provider adapter does not enforce max_tokens on API calls. The generateContent and streamGenerateContent methods are called without a maxOutputTokens parameter, allowing the model to generate an unbounded number of tokens per request. This is a direct cost amplification vector.
Suggested Fix
Add a default maxOutputTokens parameter (e.g., 4096) to all Gemini API calls. Make it configurable but enforce an upper bound.
CRITICALService role key exposed in frontend code
frontend/src/lib/supabase-server.ts:8
[AGENTS: Gateway]edge_security
**Perspective 1:** The `createServerSupabase` function uses `SUPABASE_SECRET_KEY` (the service role key) in a frontend file. The service role key bypasses all Row-Level Security (RLS) policies and has full access to the database. If this key is exposed to the client, an attacker can directly query or modify any data in the database. The file is in the frontend directory and imports from a client-side library. **Perspective 2:** The Supabase service role key (SUPABASE_SECRET_KEY) is used in a frontend file. This key bypasses Row-Level Security and grants full database access. Exposure allows an attacker to read, modify, or delete all data. **Perspective 3:** The Supabase service_role key is imported and used in frontend code (supabase-server.ts). The service_role key bypasses Row Level Security (RLS) and has full access to all data. Exposing this key in client-side code allows any user to extract it from the JavaScript bundle and gain unrestricted database access.
Suggested Fix
Move this function to the backend only. The service role key should never be used in frontend code. Use the anon/public key for client-side operations and route all privileged operations through the backend API.
CRITICALDev fallback accepts raw token as user ID, bypassing authentication
frontend/src/lib/supabase-server.ts:27
[AGENTS: Gateway]edge_security
**Perspective 1:** In the `getUserIdFromRequest` function, if the Supabase URL or service key is not configured, the function falls back to accepting the raw token string as the user ID. This means in development or misconfigured environments, any string can be used as a user ID, completely bypassing authentication. An attacker could impersonate any user by sending their ID as the token. **Perspective 2:** When SUPABASE_SECRET_KEY is not set, the code falls back to using the raw token string as the user ID. This completely bypasses authentication and allows anyone to impersonate any user by providing an arbitrary token. **Perspective 3:** In development mode, the code accepts a raw token string as the user ID without any validation. This completely bypasses authentication and allows anyone to impersonate any user by providing an arbitrary user ID. If this code path is accidentally enabled in production, it's a complete authentication bypass.
Suggested Fix
Remove the dev fallback entirely. Authentication should always validate the token server-side. If a development bypass is absolutely necessary, gate it behind a compile-time flag that cannot be enabled in production builds.
CRITICALNo RLS policies on multi-tenant tables
backend/migrations/000_one_shot_schema.sql:1
[AGENTS: Tenant]tenant_isolation
The schema enables RLS on user_profiles but does not enable or define RLS policies on any other multi-tenant tables including projects, documents, document_versions, document_edits, chats, chat_messages, tabular_reviews, tabular_cells, workflows, or workflow_shares. Without RLS, any authenticated user can read or modify any tenant's data through the Supabase API.
Suggested Fix
Enable RLS on all multi-tenant tables and create policies that filter by user_id, project_id, or shared_with arrays. For example: CREATE POLICY tenant_isolation ON projects FOR ALL USING (user_id = auth.uid() OR auth.uid() IN (SELECT jsonb_array_elements_text(shared_with)));
CRITICALNo max_tokens on completeGeminiText helper
backend/src/lib/llm/gemini.ts:163
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** The completeGeminiText function calls Gemini's generateContent with no maxOutputTokens. This is used for column prompt generation and other tasks, allowing unbounded token consumption per call. **Perspective 2:** The completeGeminiText function calls the Gemini API without a maxOutputTokens parameter. This helper is used for one-shot completions (e.g., title generation) where the response should be short. Without a token limit, the model could generate a very long response, increasing costs.
Suggested Fix
Add a maxOutputTokens parameter (e.g., 256 for title generation) to completeGeminiText and enforce it at the API call level.
CRITICALAuth middleware uses service role key for token verification without tenant context
backend/src/middleware/auth.ts:26
[AGENTS: Tenant]tenant_isolation
The requireAuth middleware creates a Supabase admin client using the service role key to verify user tokens. This bypasses all RLS policies for subsequent database operations performed by the caller. Any downstream code that uses the admin client or inherits the service role context can access any tenant's data without restriction.
Suggested Fix
Use the anon key with the user's token for database operations, or ensure that all subsequent queries explicitly filter by tenant_id. Never use the service role key for user-facing requests.
CRITICALMissing Rate Limiting on Chat Streaming Endpoint
backend/src/routes/chat.ts:308
[AGENTS: Phantom]api_security
The POST /chat endpoint initiates an LLM streaming response. There is no rate limiting on this endpoint, allowing an attacker to make unlimited requests and incur significant costs or exhaust API quotas. This is a critical issue as it can lead to financial abuse.
Suggested Fix
Implement rate limiting on the chat streaming endpoint, e.g., 20 requests per minute per user. Consider implementing token bucket or sliding window rate limiting.
CRITICALPrivilege escalation via admin.deleteUser without ownership verification
backend/src/routes/user.ts:23
[AGENTS: Vector]attack_chains
The DELETE /user/account endpoint calls db.auth.admin.deleteUser(userId) using the service role key, but only verifies the user is authenticated via requireAuth. There is no verification that the authenticated user owns the account being deleted. An attacker who can manipulate the userId (e.g., via a compromised session or token injection) could delete arbitrary user accounts. This is a direct privilege escalation from authenticated user to admin-level account deletion.
Suggested Fix
Add a check that res.locals.userId matches the userId parameter, or remove the userId parameter entirely and always use the authenticated user's ID.
CRITICALUnbounded LLM token generation with no max_tokens or budget limit
frontend/src/app/hooks/useAssistantChat.ts:1
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** The useAssistantChat hook streams LLM responses with no max_tokens, no per-user spend cap, and no circuit breaker. An attacker can send arbitrarily long prompts or many requests, causing unbounded LLM API costs. The hook calls streamChat/streamProjectChat which proxy to Gemini/Claude APIs with no token limit enforcement. **Perspective 2:** The useAssistantChat hook calls the chat API without any max_tokens parameter or budget controls. The LLM can generate an unbounded number of tokens per request. Combined with the ability to send unlimited messages, this creates a significant cost exposure.
Suggested Fix
Add a max_tokens parameter to the API call, implement client-side message length limits, and add a circuit breaker that stops sending requests after a certain number of tokens have been generated in a session.
CRITICALAnon/public key used to validate JWT tokens server-side
frontend/src/lib/auth.ts:33
[AGENTS: Razor]security
**Perspective 1:** The code uses NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY (the anon/public key) to call supabase.auth.getUser(). This key is designed for client-side use and has no authority to validate tokens. The service role key should be used for server-side token validation. **Perspective 2:** The SUPABASE_SECRET_KEY (service role key) is imported in a file that may be bundled client-side. This key bypasses all Row Level Security and gives full database access. If an attacker can access this key, they can read, modify, or delete any data in the database.
Suggested Fix
Ensure this file is only imported in server-side code (API routes, server components). Add 'use server' directive or move to a server-only module.
CRITICALAWS Credentials Exposed in Client-Side Code
frontend/src/lib/storage.ts:1
[AGENTS: Phantom]api_security
The frontend/src/lib/storage.ts file contains S3 client initialization with credentials from environment variables. If this file is imported in client-side code, the R2_ENDPOINT_URL, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY environment variables could be exposed to the browser, allowing an attacker to directly access the R2 storage bucket.
Suggested Fix
Remove this file from the frontend or ensure it is only used in server-side code (e.g., Next.js API routes). Use signed URLs generated by the backend for client-side file access.
CRITICALService role key exposed to client-side code
frontend/src/lib/supabase-server.ts:6
[AGENTS: Vault]secrets
The function createServerSupabase() uses SUPABASE_SECRET_KEY (the service role key) to create a Supabase client. This file is in the frontend/src/lib directory and is imported by client-side code. The service role key bypasses Row-Level Security (RLS) and grants full database access. If this key is bundled into client-side JavaScript, any user can extract it and gain unrestricted access to the database.
Suggested Fix
Move createServerSupabase() to a backend-only location (e.g., backend/src/lib/supabase.ts). Never import it in frontend code. Use the anon/public key on the client side.
CRITICALService role key fallback to empty string
frontend/src/lib/supabase-server.ts:7
[AGENTS: Vault]secrets
If SUPABASE_SECRET_KEY is not set, the function falls back to an empty string, which will cause the Supabase client to fail silently or behave unexpectedly. More importantly, the service role key should never be used in client-side code.
Suggested Fix
Remove this file from the frontend. Move server-side Supabase logic to the backend.
CRITICALService role key exposed to client-side code
frontend/src/lib/supabase-server.ts:7
[AGENTS: Razor]security
The SUPABASE_SECRET_KEY (service role key) is imported in a file that may be bundled client-side. This key bypasses all Row Level Security and gives full database access. If an attacker can access this key, they can read, modify, or delete any data in the database.
Suggested Fix
Ensure this file is only imported in server-side code (API routes, server components). Add 'use server' directive or move to a server-only module.
CRITICALDev fallback accepts raw token as user ID without validation
frontend/src/lib/supabase-server.ts:24
[AGENTS: Gatekeeper]auth
When SUPABASE_URL or SUPABASE_SECRET_KEY are not set, getUserIdFromRequest returns the raw Bearer token as the user ID. This means any string passed as the Authorization header is accepted as a valid user identity, allowing complete authentication bypass in development/staging environments that may be exposed.
Suggested Fix
Remove the dev fallback entirely. Always validate the token against Supabase. If the service key is missing, throw a 500 error rather than silently accepting arbitrary tokens.
CRITICALUnbounded tabular generation with no cost controls
frontend/src/app/components/tabular/TabularReviewView.tsx:1
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** The handleGenerate function calls streamTabularGeneration which triggers LLM calls for every cell in the review. There is no limit on the number of columns or documents, no per-user spend cap, and no rate limiting. An attacker can create a review with many columns/documents and repeatedly click 'Run' to generate unlimited LLM API costs. **Perspective 2:** The handleRegenerateCell function calls regenerateTabularCell which triggers an LLM API call. There is no rate limiting or cost control on this action. An attacker can repeatedly regenerate cells to incur unlimited API costs. **Perspective 3:** The TabularReviewView component triggers LLM API calls for each cell in a tabular review. There is no limit on the number of columns, rows, or documents that can be processed. An attacker could create a review with hundreds of columns and thousands of documents, each triggering an LLM API call. The cost scales linearly with the number of cells.
Suggested Fix
Limit the number of columns (e.g., max 20), rows (e.g., max 100), and documents (e.g., max 50) per review. Add a cost estimation step before processing that rejects reviews exceeding a budget threshold. Implement per-user daily spend caps on tabular generation.
CRITICALUnbounded LLM token streaming with no cost controls
frontend/src/app/components/assistant/AssistantMessage.tsx:1
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** AssistantMessage renders streaming LLM responses with no max_tokens or cost limits. The component processes content_delta events indefinitely, allowing an attacker to generate arbitrarily long responses and incur unlimited LLM API costs. **Perspective 2:** The AssistantMessage component renders streaming LLM responses with no limits on token count. The streaming response is rendered as it arrives, and there is no mechanism to stop the stream if it exceeds a cost threshold. An attacker could craft prompts that cause the LLM to stream an extremely long response.
Suggested Fix
Implement a client-side token counter that stops rendering and shows a warning when the response exceeds a threshold. Add a server-side max_tokens parameter to limit the response length.
CRITICALUnbounded LLM API calls via chat endpoint with no per-user rate limiting
backend/src/routes/chat.ts:1
[AGENTS: Wallet]denial_of_wallet
**Perspective 1:** The POST /chat endpoint triggers an LLM stream (runLLMStream) for every request. There is no rate limiting, no per-user spend cap, and no budget circuit breaker. An attacker can send unlimited requests, each incurring LLM API costs proportional to the response length. With no max_tokens enforcement on the streaming response, a single request could generate thousands of tokens, and unlimited requests could run up a massive bill. **Perspective 2:** The runLLMStream function is called without any max_tokens parameter visible in the route handler. The LLM can generate an unbounded number of tokens per request, each costing money. An attacker could craft prompts that cause the LLM to produce extremely long responses, amplifying the cost per request. **Perspective 3:** The chat endpoint calls runLLMStream which uses getUserApiKeys to get the user's API keys. There is no mechanism to track or cap the user's LLM spend. A user with their own API keys could still rack up costs on the provider side, and the application has no visibility into or control over that spend. For users using shared API keys, an attacker could drain the shared budget. **Perspective 4:** The POST /chat/:chatId/generate-title endpoint calls completeText (an LLM API) without any rate limiting or cost controls. An attacker could call this endpoint repeatedly, each time incurring an LLM API call to generate a short title. While each call is cheap, unlimited calls add up.
Suggested Fix
Implement per-user rate limiting (e.g., token bucket), enforce a max_tokens parameter on the LLM call, add a daily/weekly spend cap per user, and add a circuit breaker that stops processing when a budget threshold is exceeded.
CRITICALCredit system bypass via client-side reset logic
frontend/src/contexts/UserProfileContext.tsx:1
[AGENTS: Exploit]business_logic
**Perspective 1:** The credit reset logic is implemented entirely on the client side. The function `loadProfile` checks if `credits_reset_date` has passed and resets `message_credits_used` to 0 locally, then updates the database in the background. A user can manipulate their local clock, clear localStorage, or block the background update to prevent the database from being updated, effectively resetting their credits without the server knowing. Additionally, the `MONTHLY_CREDIT_LIMIT` is set to 999999 (temporarily unlimited), which means the credit system is effectively non-functional and provides no real rate limiting. **Perspective 2:** The `incrementMessageCredits` function reads the current `messageCreditsUsed` from local state, increments it, and then updates the database. This is a classic read-modify-write race condition. If a user sends multiple concurrent requests, multiple increments can read the same value from the database, each incrementing it by 1, but the final value in the database will only reflect one increment instead of all. This allows a user to send many messages while only being charged for one. The check `if (profile.creditsRemaining <= 0)` is also client-side and can be bypassed by manipulating local state. **Perspective 3:** The `claude_api_key` and `gemini_api_key` are fetched from the database and stored in client-side state (`UserProfile`). These keys are then sent to the server on each API call. A malicious user could inspect the network traffic or client-side state to steal another user's API keys if they can somehow access their profile data. More importantly, the keys are stored in plain text in the database and transmitted to the client, violating best practices for secret management. **Perspective 4:** If the `user_profiles` table query fails (e.g., due to a database error or the profile not existing), the code creates a fallback profile with `messageCreditsUsed: 0` and `creditsRemaining: 999999`. This means any database error or missing profile grants the user unlimited credits. An attacker could potentially trigger a database error to get unlimited credits.
Suggested Fix
Move all credit reset logic to the server side. The server should check and reset credits on each API call, not the client. Remove the client-side credit limit check entirely and enforce limits server-side. Set a realistic credit limit.
CRITICALDocument versions table lacks RLS and tenant isolation
backend/migrations/000_one_shot_schema.sql:341
[AGENTS: Tenant]tenant_isolation
**Perspective 1:** The document_versions table has no RLS policies. Since it contains storage_path references to S3 objects, an attacker could enumerate version IDs to access any tenant's document content. **Perspective 2:** The chat_messages table has no RLS policies. Chat messages contain the full conversation history including document content and analysis, which could expose sensitive data across tenants. **Perspective 3:** The tabular_cells table has no RLS policies. It contains extracted data from documents including summaries, flags, and reasoning, which could expose sensitive analysis across tenants. **Perspective 4:** The document_edits table has no RLS policies. It contains the full text of deleted and inserted content from tracked changes, which could contain sensitive information from any tenant. **Perspective 5:** The workflow_shares table has no RLS policies. It contains email addresses and sharing permissions, which could leak information about which users have access to which workflows across tenants. **Perspective 6:** The tabular_review_chats and tabular_review_chat_messages tables have no RLS policies. These contain chat conversations about tabular reviews, which could expose sensitive analysis across tenants. **Perspective 7:** The project_subfolders table has no RLS policies. An attacker could enumerate folder structures across projects to discover document organization patterns of other tenants. **Perspective 8:** The hidden_workflows table has no RLS policies. It tracks which built-in workflows each user has hidden, which could leak user preferences across tenants.
Suggested Fix
Enable RLS on tabular_cells and add a policy that joins through tabular_reviews to projects to verify user access.
CRITICALChat tools use server-side Supabase client without tenant scoping
backend/src/lib/chatTools.ts:1
[AGENTS: Tenant]tenant_isolation
The chatTools module imports createServerSupabase and uses it for all database operations including document reads, version loading, and edit persistence. The server-side client uses the service role key, bypassing RLS. None of the queries in this file include tenant_id or project_id filters to ensure data isolation between tenants.
Suggested Fix
Replace createServerSupabase with a tenant-scoped client that includes the authenticated user's context, or add explicit tenant_id/project_id WHERE clauses to every database query.
CRITICALAdmin-level user deletion without additional verification
backend/src/routes/user.ts:24
[AGENTS: Harbor]containers
**Perspective 1:** The DELETE /user/account endpoint uses the Supabase service role key (db.auth.admin.deleteUser) to delete users. This is a privileged operation that should require additional verification beyond just being authenticated. If an attacker gains access to a user's session token, they can permanently delete the account with no additional confirmation step beyond the client-side confirmation dialog. **Perspective 2:** User deletion functionality exists without additional verification steps. If an attacker gains access to the service role key or exploits an authentication bypass, they could delete users at will. This is particularly dangerous in a containerized environment where the service role key might be more accessible.
Suggested Fix
Implement server-side confirmation requirements such as: requiring the user to re-enter their password, sending a confirmation email with a time-limited token, or implementing a grace period before permanent deletion. The client-side confirmation is not sufficient security.
CRITICALSupabase anon/public key used for token verification on the server side
frontend/src/lib/auth.ts:27
[AGENTS: Vault]secrets
**Perspective 1:** The function getUserFromRequest() uses NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY (the anon/public key) to verify Supabase JWT tokens. The anon key cannot verify tokens — it's meant for client-side use. This means token verification will always fail or behave incorrectly, potentially allowing unauthenticated requests to pass through. **Perspective 2:** The environment variable NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY is used for server-side token verification. The 'NEXT_PUBLIC_' prefix in Next.js means this variable is exposed to the browser. The anon key is meant to be public, but using it for authentication verification is a security anti-pattern. **Perspective 3:** The code uses process.env.NEXT_PUBLIC_SUPABASE_URL! and process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY! with non-null assertion. If these are not set, the application will crash or use undefined values. There is no validation or error handling for missing environment variables.
Suggested Fix
Use the service role key (SUPABASE_SECRET_KEY) for server-side token verification, or use the Supabase admin client. The anon key has no ability to verify tokens.
CRITICALUses anon/public key instead of service role key for token validation
frontend/src/lib/auth.ts:33
[AGENTS: Gatekeeper]auth
getUserFromRequest uses NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY (the anon/public key) to validate JWT tokens. The anon key is designed for client-side use and cannot properly verify tokens server-side. This may lead to incorrect validation results, potentially accepting invalid tokens or rejecting valid ones.
Suggested Fix
Use the Supabase service role key (SUPABASE_SECRET_KEY) for server-side token validation, or use the Supabase Admin client. Never use the anon/public key for server-side auth decisions.
CRITICALMissing Rate Limiting on Project Chat Streaming Endpoint
backend/src/routes/projectChat.ts:1
[AGENTS: Phantom]api_security
**Perspective 1:** The POST /projects/:projectId/chat endpoint initiates an LLM streaming response for project-specific chats. There is no rate limiting on this endpoint, allowing an attacker to make unlimited requests and incur significant costs or exhaust API quotas. **Perspective 2:** Similar to the chat endpoint, the project chat endpoint accepts messages without validation. Additionally, it accepts displayed_doc and attached_documents fields that could be manipulated to reference documents the user should not have access to.
Suggested Fix
Add input validation for all fields in the request body. Verify that displayed_doc and attached_documents reference documents that the user has access to within the project.
CRITICALDocument version loading lacks tenant verification
backend/src/lib/chatTools.ts:1095
[AGENTS: Tenant]tenant_isolation
**Perspective 1:** The loadActiveVersion and loadCurrentVersionBytes functions fetch document versions without verifying that the requesting user has access to the document's project. An attacker could enumerate document IDs to read any tenant's document versions. **Perspective 2:** The runEditDocument function accepts a documentId and performs edits without verifying that the document belongs to a project the user has access to. The function uses the server-side Supabase client which bypasses RLS. **Perspective 3:** The generateDocx function creates new documents in the database using the server-side Supabase client. The document is associated with a user_id but there is no verification that the user has access to the specified project_id. An attacker could create documents in any project.
Suggested Fix
Before inserting the document, verify that the user is a member of the specified project. Use the user's auth context rather than the service role client.
CRITICALDownload endpoint accepts token without verifying caller's tenant membership
backend/src/routes/downloads.ts:32
[AGENTS: Tenant]tenant_isolation
The download endpoint at GET /download/:token accepts a signed token and resolves the file path, but does not verify that the requesting user belongs to the same tenant as the document owner. The `ensureDocAccess` function checks document-level access but does not enforce tenant-level isolation. A user from Tenant A could potentially access a document from Tenant B if they have a valid download token or if the token can be forged/leaked.
Suggested Fix
Add tenant_id to the download token payload and verify it matches the requesting user's tenant context. Alternatively, ensure `ensureDocAccess` checks tenant membership by joining the documents table with a tenant-scoped projects or user_tenants table.
CRITICALMass data exfiltration via tabular review API without rate limiting
backend/src/routes/tabular.ts:1
[AGENTS: Vector]attack_chains
The GET /tabular-review endpoint returns all reviews accessible to the user, including full cell content with document extractions. Combined with the GET /tabular-review/:reviewId endpoint which returns all cells and documents for a review, an attacker with access to a single review can enumerate all documents and their extracted content. There is no pagination or rate limiting on these endpoints, allowing bulk exfiltration of all review data across all accessible projects.
Suggested Fix
Implement pagination on list endpoints, add rate limiting, and consider adding audit logging for bulk data access.
CRITICAL[OpenClaw] API keys stored in plain text in database schema
backend/migrations/000_one_shot_schema.sql:48
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [2] via data_flow. Interaction chain: Plaintext API keys in database (CANDIDATE-345) are directly accessible via chat tools (CONFIRMED-2) that use server-side client without tenant scoping, enabling key theft Original finding: The user_profiles table schema defines claude_api_key and gemini_api_key as plain text columns. These columns store user-provided API keys without any encryption or hashing. If the database is compromised, all user API keys are exposed.
Suggested Fix
Add encryption at rest for these columns using pgcrypto extension. Consider using a separate encrypted table or integrating with a secrets management service.
CRITICAL[OpenClaw] API keys stored in plain text in database schema
backend/migrations/000_one_shot_schema.sql:48
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [36] via data_flow. Interaction chain: Plaintext API keys in database (CANDIDATE-345) can be exfiltrated via service role client (CONFIRMED-36) that bypasses RLS Original finding: The user_profiles table schema defines claude_api_key and gemini_api_key as plain text columns. These columns store user-provided API keys without any encryption or hashing. If the database is compromised, all user API keys are exposed.
Suggested Fix
Add encryption at rest for these columns using pgcrypto extension. Consider using a separate encrypted table or integrating with a secrets management service.
CRITICAL[OpenClaw] No rate limiting at the application level
backend/src/index.ts:21
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [8] via config. Interaction chain: No application-level rate limiting (CANDIDATE-352) directly enables unbounded LLM calls via chat streaming endpoint (CONFIRMED-8) Original finding: **Perspective 1:** The Express application does not implement any rate limiting middleware. This exposes API endpoints to brute-force attacks, credential stuffing, and denial-of-service attacks. While rate limiting is often handled at a reverse proxy level, the application should have defense-in-depth rate limiting for critical endpoints like authentication and chat. **Perspective 2:** The application does not implement any rate limiting middleware. This leaves all endpoints vulnerable to brute
Suggested Fix
Add express-rate-limit middleware with appropriate limits per endpoint. For example, limit auth endpoints to 5 requests per minute per IP and chat endpoints to 30 requests per minute per user.
CRITICAL[OpenClaw] No rate limiting at the application level
backend/src/index.ts:21
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [11] via config. Interaction chain: No application-level rate limiting (CANDIDATE-352) directly enables unbounded LLM calls via project chat endpoint (CONFIRMED-11) Original finding: **Perspective 1:** The Express application does not implement any rate limiting middleware. This exposes API endpoints to brute-force attacks, credential stuffing, and denial-of-service attacks. While rate limiting is often handled at a reverse proxy level, the application should have defense-in-depth rate limiting for critical endpoints like authentication and chat. **Perspective 2:** The application does not implement any rate limiting middleware. This leaves all endpoints vulnerable to brute
Suggested Fix
Add express-rate-limit middleware with appropriate limits per endpoint. For example, limit auth endpoints to 5 requests per minute per IP and chat endpoints to 30 requests per minute per user.
CRITICAL[OpenClaw] No rate limiting at the application level
backend/src/index.ts:21
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [12] via config. Interaction chain: No application-level rate limiting (CANDIDATE-352) enables mass data exfiltration via tabular review API (CONFIRMED-12) Original finding: **Perspective 1:** The Express application does not implement any rate limiting middleware. This exposes API endpoints to brute-force attacks, credential stuffing, and denial-of-service attacks. While rate limiting is often handled at a reverse proxy level, the application should have defense-in-depth rate limiting for critical endpoints like authentication and chat. **Perspective 2:** The application does not implement any rate limiting middleware. This leaves all endpoints vulnerable to brute
Suggested Fix
Add express-rate-limit middleware with appropriate limits per endpoint. For example, limit auth endpoints to 5 requests per minute per IP and chat endpoints to 30 requests per minute per user.
CRITICAL[OpenClaw] Access control checks rely on email matching without normalization
backend/src/lib/access.ts:1
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [2] via data_flow. Interaction chain: Email normalization bypass in access control (CANDIDATE-354) combined with chat tools lacking tenant scoping (CONFIRMED-2) allows cross-tenant document access Original finding: The `checkProjectAccess` function compares user emails against `shared_with` arrays using case-insensitive comparison. However, email normalization (trimming, Unicode normalization) is not applied, which could lead to bypasses if emails are stored with different casing or Unicode variations.
Suggested Fix
Normalize emails to lowercase and apply Unicode normalization (NFC) before comparison. Consider using a canonical email format for storage and comparison.
CRITICAL[OpenClaw] Case-insensitive email comparison for shared access can be bypassed with Unicode normalization
backend/src/lib/access.ts:47
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [2] via data_flow. Interaction chain: Unicode normalization bypass in shared access (CANDIDATE-356) combined with chat tools lacking tenant scoping (CONFIRMED-2) enables cross-tenant data access Original finding: **Perspective 1:** The shared_with email comparison uses .toLowerCase() which is case-insensitive but does not normalize Unicode characters. Attackers could register emails with Unicode homoglyphs (e.g., using a Cyrillic 'е' instead of Latin 'e') to bypass the email comparison and gain unauthorized access to projects. **Perspective 2:** The shared_with check compares emails directly without verifying domain ownership or email deliverability. If an attacker can create an account with an email th
Suggested Fix
Ensure email comparisons are exact string matches after normalization. Consider adding email domain verification or requiring explicit confirmation for shared access.
CRITICAL[OpenClaw] Weak default signing secret for download tokens
backend/src/lib/downloadTokens.ts:1
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [9] via config. Interaction chain: Weak default signing secret for download tokens (CANDIDATE-372) makes the token forgery attack in CONFIRMED-9 trivially exploitable Original finding: **Perspective 1:** The download token signing secret falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. This allows token forgery in production if environment variables are misconfigured. **Perspective 2:** Download tokens are HMAC-signed but have no expiration mechanism. Tokens stored in chat history remain valid indefinitely, increasing the risk window if a token is leaked.
Suggested Fix
Remove the 'dev-secret' fallback and throw an error if DOWNLOAD_SIGNING_SECRET is not set in production. Use a cryptographically random 256-bit key.
CRITICAL[OpenClaw] Weak default signing secret for download tokens
backend/src/lib/downloadTokens.ts:1
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [9] via config. Interaction chain: Weak default signing secret (CANDIDATE-373) enables forging download tokens (CONFIRMED-9) to access any document without tenant verification Original finding: The download token signing secret falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. This is a weak secret that could be easily guessed, allowing attackers to forge download tokens.
Suggested Fix
Remove the 'dev-secret' fallback and require DOWNLOAD_SIGNING_SECRET to be set in production. Add a warning log when the fallback is used.
CRITICAL[OpenClaw] Weak fallback secret for download token signing
backend/src/lib/downloadTokens.ts:1
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [9] via config. Interaction chain: Weak fallback secret in containers (CANDIDATE-374) makes download token forgery (CONFIRMED-9) exploitable in production deployments Original finding: **Perspective 1:** The download token signing mechanism falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. In a production container environment, this weak secret could allow an attacker to forge download tokens and access any file stored in the system. **Perspective 2:** The download token signing uses a weak fallback secret. In a containerized environment, environment variables might be more accessible, making this a potential attack vector for forgin
Suggested Fix
Ensure DOWNLOAD_SIGNING_SECRET is always set in production environments. Remove the fallback to 'dev-secret' or make it fail explicitly when no secret is configured. Consider using a secrets management solution (e.g., Kubernetes Secrets, HashiCorp Vault) to inject the secret into the container.
CRITICAL[OpenClaw] Missing input validation on path and filename in signDownload
backend/src/lib/downloadTokens.ts:1
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [9] via data_flow. Interaction chain: Missing input validation on path/filename in signDownload (CANDIDATE-377) allows attackers to forge tokens (CONFIRMED-9) for arbitrary file paths Original finding: **Perspective 1:** The signDownload function accepts arbitrary path and filename strings without validation. An attacker who can control these parameters could sign tokens for malicious paths or filenames. **Perspective 2:** The download signing secret falls back to 'dev-secret' if DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. This could allow token forgery in production if environment variables are misconfigured.
Suggested Fix
Validate that path does not contain path traversal sequences (../) and that filename does not contain special characters. Add length limits.
CRITICAL[OpenClaw] Model name passed as parameter without allowlisting
backend/src/lib/llm/claude.ts:37
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [4] via data_flow. Interaction chain: Unvalidated model parameter (CANDIDATE-382) combined with no cost controls (CONFIRMED-4) allows attacker to specify expensive models for denial of wallet Original finding: **Perspective 1:** The model name is passed as a parameter to the Claude API without being validated against an allowlist. An attacker who can control the model parameter could potentially use an unintended model or one with different capabilities. **Perspective 2:** The model name is passed as a parameter to the Claude API call without being validated against an allowlist. If the model name originates from user input, an attacker could specify a different model that may have different behavior
Suggested Fix
Implement an allowlist of valid model names and validate the model parameter against it before making API calls.
CRITICAL[OpenClaw] Model name passed as parameter without allowlisting
backend/src/lib/llm/gemini.ts:37
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [4] via data_flow. Interaction chain: Unvalidated model parameter in Gemini (CANDIDATE-385) combined with no cost controls (CONFIRMED-4) enables cost escalation attacks Original finding: **Perspective 1:** The model name is passed directly from the request parameters to the Gemini API call. If an attacker can control the model parameter, they could potentially redirect requests to a different model or a malicious endpoint. The model name should be validated against an allowlist. **Perspective 2:** The model name is passed as a parameter to the Gemini API without being validated against an allowlist. An attacker who can control the model parameter could potentially use an uninte
Suggested Fix
Implement an allowlist of permitted model names and validate the model parameter against it before making the API call.
CRITICAL[OpenClaw] Unbounded iteration loop in Gemini streaming
backend/src/lib/llm/gemini.ts:47
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [4] via data_flow. Interaction chain: Unbounded iteration loop in Gemini streaming (CANDIDATE-387) combined with no max_tokens (CONFIRMED-4) creates infinite cost attack vector Original finding: The streamGemini function has a maxIterations of 10, but each iteration can trigger additional tool calls and API requests. If the model repeatedly generates tool calls that the tool runner resolves, the loop can make up to 10 sequential API calls per user request. An attacker could craft prompts that force many tool call iterations, consuming API quota and increasing latency. While not a direct server crash, this can exhaust API rate limits and degrade service for other users.
Suggested Fix
Reduce maxIterations to a lower value (e.g., 3-5) or make it configurable per endpoint with a strict upper bound. Also consider adding a total timeout for the entire streaming session.
CRITICAL[OpenClaw] R2 storage credentials read from environment variables without validation
backend/src/lib/storage.ts:1
[AGENTS: openclaw-scanner]openclaw_shared_state
Cross-chunk interaction detected. This finding interacts with confirmed threat [22] via shared_state. Interaction chain: Backend R2 credentials from env vars (CANDIDATE-390) are the same credentials exposed in frontend (CONFIRMED-22), confirming credential exposure is systemic Original finding: The storage module reads R2 credentials (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY) from environment variables but does not validate that they are present before use. If these environment variables are missing or misconfigured, the S3 client will fail with a cryptic error. More importantly, there is no warning or fallback mechanism if credentials are invalid or expired.
Suggested Fix
Add validation at startup to verify that all required R2 environment variables are present and that the credentials are valid by making a test API call. Implement a health check endpoint that verifies storage connectivity. Log a clear error message if credentials are missing or invalid.
CRITICAL[OpenClaw] Storage key construction uses user-controlled filename without sanitization
backend/src/lib/storage.ts:1
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [3] via data_flow. Interaction chain: Storage key construction with unsanitized filenames (CANDIDATE-392) combined with document version loading lacking tenant verification (CONFIRMED-3) enables path traversal to other tenants' documents Original finding: The `storageKey()` and related functions construct S3 storage keys using user-provided filenames. While the `storageExtension()` function validates the extension, the filename itself is not sanitized for path traversal characters (e.g., '../', null bytes).
Suggested Fix
Sanitize filenames to remove path traversal characters before using them in storage keys. Use a UUID-based key structure that doesn't include user-controlled filenames.
CRITICAL[OpenClaw] User model settings loaded from database without validation
backend/src/lib/userSettings.ts:1
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [2] via data_flow. Interaction chain: User model settings loaded without validation (CANDIDATE-401) combined with chat tools lacking tenant scoping (CONFIRMED-2) allows model injection attacks Original finding: User model settings (including tabular_model and API keys) are loaded from the user_profiles table without validation that the model names are valid or that the API keys are not compromised. An attacker who gains access to the database could modify these settings to use a different model or a compromised API key.
Suggested Fix
Validate model names against an allowlist when loading user settings. Consider encrypting API keys at rest and validating them on load.
CRITICAL[OpenClaw] API keys stored in plaintext in database
backend/src/lib/userSettings.ts:1
[AGENTS: openclaw-scanner]openclaw_data_flow
Cross-chunk interaction detected. This finding interacts with confirmed threat [2] via data_flow. Interaction chain: Plaintext API keys in database (CANDIDATE-402) combined with chat tools using server-side client without tenant scoping (CONFIRMED-2) enables key theft Original finding: **Perspective 1:** User API keys (claude_api_key, gemini_api_key) are stored in plaintext in the user_profiles table. If the database is compromised, all user API keys would be exposed. These keys provide access to third-party AI services and could be used for unauthorized usage. **Perspective 2:** The getUserModelSettings function retrieves API keys from the database without validating that they are still valid or have the required permissions. Stale or invalid keys could cause silent failures
Suggested Fix
Encrypt API keys at rest using a strong encryption algorithm (e.g., AES-256-GCM) with a key derived from a server-side secret. Decrypt only when needed for API calls. Consider using a dedicated secrets management service.
CRITICAL[OpenClaw] JWT verification uses service role key instead of JWT secret
backend/src/middleware/auth.ts:1
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [6] via config. Interaction chain: JWT verification using service role key (CANDIDATE-403) is the same mechanism as CONFIRMED-6, confirming the pattern of using privileged keys for auth Original finding: The auth middleware uses the Supabase service role key (SUPABASE_SECRET_KEY) to verify user tokens via admin.auth.getUser(token). While this works, it uses the admin client with full database access privileges. A more secure approach would be to verify the JWT signature locally using the Supabase JWT secret, avoiding the need for the service role key in the auth path.
Suggested Fix
Verify JWT tokens locally using jsonwebtoken library with the SUPABASE_JWT_SECRET environment variable. This avoids exposing the service role key in the request path.
CRITICAL[OpenClaw] Auth middleware uses service role key for token verification
backend/src/middleware/auth.ts:1
[AGENTS: openclaw-scanner]openclaw_config
Cross-chunk interaction detected. This finding interacts with confirmed threat [6] via config. Interaction chain: Auth middleware using service role key (CANDIDATE-406) is the same vulnerability as CONFIRMED-6, confirming the pattern Original finding: **Perspective 1:** The requireAuth middleware uses the SUPABASE_SECRET_KEY (service role key) to verify user tokens via admin.auth.getUser(). While this works, it uses a privileged key that has full access to all Supabase resources. A compromise of this key would be catastrophic. The service key should not be used in request-scoped middleware if a more restricted approach is possible. **Perspective 2:** The auth middleware only checks if the token is valid via Supabase's getUser(). It does not
Suggested Fix
Consider using the Supabase anon key with getUser() instead of the service role key, or create a dedicated Supabase client with limited permissions for token verification. If the service key must be used, ensure it is stored in a secure environment variable and never exposed to clients.
HIGHWeak HMAC secret fallback
backend/src/lib/downloadTokens.ts:1
[AGENTS: Cipher]cryptography
The HMAC secret for signing download tokens falls back to 'dev-secret' when neither DOWNLOAD_SIGNING_SECRET nor SUPABASE_SECRET_KEY is set. This is a hardcoded, predictable secret that would allow an attacker to forge valid download tokens for any file.
Suggested Fix
Ensure DOWNLOAD_SIGNING_SECRET is always set in production and remove the 'dev-secret' fallback, or throw an error if the secret is missing.
HIGHFallback signing secret hardcoded as 'dev-secret'
backend/src/lib/downloadTokens.ts:16
[AGENTS: Vault]secrets
When DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are both unset, the token signing function falls back to the literal string 'dev-secret'. This means any deployment missing these environment variables will use a predictable, publicly-known HMAC key, allowing anyone to forge valid download tokens and access any document.
Suggested Fix
Remove the fallback string and throw an error if neither environment variable is set, or require DOWNLOAD_SIGNING_SECRET to be explicitly configured in production.
HIGHFallback signing secret is a hardcoded dev value
backend/src/lib/downloadTokens.ts:69
[AGENTS: Tenant]tenant_isolation
When DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are both unset, the signing secret defaults to 'dev-secret'. In production, this means all download tokens would be signed with a known, static key, allowing any user to forge tokens and access any file. This is a critical configuration vulnerability.
Suggested Fix
Remove the hardcoded fallback and throw an error if DOWNLOAD_SIGNING_SECRET is not set in production. Add a startup check that fails loudly when the secret is missing or set to the default value.
HIGHDev fallback accepts raw token as user ID
frontend/src/lib/supabase-server.ts:26
[AGENTS: Cipher]cryptography
When SUPABASE_SECRET_KEY is not set, the function getUserIdFromRequest returns the raw token string as the user ID without any validation. This means any token (including arbitrary strings) would be accepted as a valid user ID in development, bypassing authentication entirely.
Suggested Fix
Remove the dev fallback and always require proper Supabase JWT validation, or throw an error if the service key is missing.
HIGHNo credit check before processing chat request
backend/src/routes/chat.ts:1
[AGENTS: Exploit]business_logic
The chat streaming endpoint (`POST /chat`) does not check the user's credit balance or increment usage before processing the request. A user can send unlimited messages without any cost tracking. The client-side credit increment in `UserProfileContext.tsx` is not called from this flow, and even if it were, it would be unreliable.
Suggested Fix
Add server-side credit checking and atomic increment logic at the beginning of the chat handler. Reject the request if the user has no credits remaining.
HIGHNo credit check before processing project chat request
backend/src/routes/projectChat.ts:1
[AGENTS: Exploit]business_logic
Similar to the regular chat endpoint, the project chat streaming endpoint (`POST /projects/:projectId/chat`) does not check the user's credit balance or increment usage. This allows unlimited usage of the project assistant feature.
Suggested Fix
Add server-side credit checking and atomic increment logic at the beginning of the project chat handler.
HIGHAPI keys stored in plaintext in database schema
backend/migrations/000_one_shot_schema.sql:1
[AGENTS: Cipher]cryptography
The database schema defines claude_api_key and gemini_api_key columns as plain text in the user_profiles table. No encryption is applied at the database level. This is a data-at-rest vulnerability for sensitive credentials.
Suggested Fix
Add encryption for these columns using pgcrypto extension. Create a trigger to encrypt on insert/update and decrypt on select. Alternatively, use Supabase Vault for secrets storage.
HIGHUnbounded external process spawning via LibreOffice conversion
backend/src/lib/convert.ts:60
[AGENTS: Siege]dos
The docxToPdf function spawns a LibreOffice process for every DOCX-to-PDF conversion. LibreOffice is a heavy application that consumes significant CPU and memory (often 200-500 MB per instance). An attacker can upload many DOCX files simultaneously, causing the server to spawn many LibreOffice processes, exhausting system resources and leading to denial of service. There is no concurrency limit or queue.
Suggested Fix
Implement a conversion queue with a maximum concurrency (e.g., 1-2 concurrent conversions). Use a worker pool or a job queue (like Bull) to serialize conversions. Also consider setting a timeout per conversion to kill hung processes.
HIGHSSRF via user-controlled API base URL in fetch requests
backend/src/lib/documentVersions.ts:1
[AGENTS: Specter]ssrf
The documentVersions module uses user-controlled API base URLs in fetch requests without validation, allowing SSRF attacks.
Suggested Fix
Validate and restrict the API base URL to a whitelist of allowed domains.
HIGHHardcoded fallback signing secret
backend/src/lib/downloadTokens.ts:1
[AGENTS: Supply]supply_chain
The download token signing secret falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. In production, this means any attacker who knows the default secret can forge download tokens for any file path, bypassing access controls. The secret should be required and fail at startup if not configured.
Suggested Fix
Replace the fallback with a startup-time check that throws if DOWNLOAD_SIGNING_SECRET is not set. Remove the 'dev-secret' fallback entirely.
HIGHHardcoded fallback secret for download token signing
backend/src/lib/downloadTokens.ts:1
[AGENTS: Compliance]regulatory
The download token signing secret falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set. In production, this means any attacker who knows the default secret can forge download tokens and access any document. This violates SOC 2 CC6.1 (encryption of data at rest and in transit) and CC6.6 (security of cryptographic keys).
Suggested Fix
Remove the hardcoded fallback and throw an error if DOWNLOAD_SIGNING_SECRET is not set in production. Example: if (!process.env.DOWNLOAD_SIGNING_SECRET) throw new Error('DOWNLOAD_SIGNING_SECRET must be set in production');
HIGHInsecure fallback secret for HMAC signing
backend/src/lib/downloadTokens.ts:13
[AGENTS: Provenance]ai_provenance
The download token signing secret falls back to `'dev-secret'` when neither `DOWNLOAD_SIGNING_SECRET` nor `SUPABASE_SECRET_KEY` is set. This hardcoded fallback means any deployment missing the environment variable will use a predictable, publicly known secret, allowing token forgery.
Suggested Fix
Remove the hardcoded fallback and throw an error if no signing secret is configured.
HIGHWeak fallback HMAC secret in production
backend/src/lib/downloadTokens.ts:13
[AGENTS: Razor]security
The HMAC secret for download tokens falls back to 'dev-secret' when DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are both unset. In production this means any attacker who knows the source code can forge arbitrary download tokens, bypassing all access controls.
Suggested Fix
Remove the fallback or make it throw an error in production: if (!process.env.DOWNLOAD_SIGNING_SECRET) throw new Error('DOWNLOAD_SIGNING_SECRET must be set in production')
HIGHFallback to 'dev-secret' when no signing secret is configured
backend/src/lib/downloadTokens.ts:24
[AGENTS: Weights]model_supply_chain
When DOWNLOAD_SIGNING_SECRET and SUPABASE_SECRET_KEY are not set, the code falls back to the hardcoded string 'dev-secret'. This means any deployment without proper configuration will use a predictable, publicly known secret for signing download tokens.
Suggested Fix
Remove the fallback to 'dev-secret' and instead throw an error if no signing secret is configured. Require explicit configuration.
HIGHSSRF via user-controlled API base URL in fetch requests
backend/src/lib/llm/claude.ts:1
[AGENTS: Specter]ssrf
The Claude LLM integration uses user-controlled API base URLs in fetch requests without validation, allowing SSRF attacks.
Suggested Fix
Validate and restrict the API base URL to a whitelist of allowed domains.
HIGHSensitive data in raw stream log file
backend/src/lib/llm/claude.ts:76
[AGENTS: Trace]logging
Every Claude API stream event is logged to a file at 'claude-raw-stream.log' in the current working directory. This file captures the full JSON of every stream event, which may include user messages, tool call inputs, and model responses containing sensitive legal document content. The file is written synchronously with fs.appendFile and never rotated or cleaned up.
Suggested Fix
Remove the raw stream logging entirely, or gate it behind an environment variable (e.g., DEBUG_CLAUDE_STREAM) that is disabled in production. If kept, ensure the log file is excluded from version control, rotated daily, and has restricted file permissions (0600).
HIGHSensitive data in console.log of Claude stream events
backend/src/lib/llm/claude.ts:77
[AGENTS: Trace]logging
Every Claude API stream event is logged to console.log with the line `console.log('[claude raw stream]', line)`. This exposes the full JSON of every stream event, including user messages, tool call inputs, and model responses containing sensitive legal document content, to stdout/stderr which may be captured by logging infrastructure.
Suggested Fix
Remove the console.log call, or gate it behind a DEBUG environment variable. Never log raw API request/response payloads in production.
HIGHFull LLM stream events logged to file and console
backend/src/lib/llm/claude.ts:83
[AGENTS: Egress]data_exfiltration
Every streaming event from the Claude API is logged to both the console and a file (claude-raw-stream.log). These events contain the full text of the LLM response, which may include sensitive information extracted from documents (PII, financial data, legal clauses). The file is written to the current working directory without rotation or access control, making it accessible to anyone with filesystem access.
Suggested Fix
Remove the raw stream logging in production. If debugging is needed, use a structured logger with configurable levels and ensure log files are excluded from deployment and rotated regularly.
HIGHService role key used in server-side client
backend/src/lib/supabase.ts:1
[AGENTS: Infiltrator]attack_surface
The `createServerSupabase()` function uses the SUPABASE_SECRET_KEY (service role key) which bypasses Row-Level Security. Any vulnerability in routes using this client could lead to unauthorized data access.
Suggested Fix
Minimize use of service role client. Use anon key with RLS where possible. Ensure all routes using the service client have proper authorization checks.
HIGHAPI keys stored in plaintext in database
backend/src/lib/userSettings.ts:1
[AGENTS: Cipher]cryptography
User API keys for Claude and Gemini are stored in plaintext in the user_profiles table (claude_api_key, gemini_api_key columns). These are sensitive credentials that should be encrypted at rest. An attacker with database access could steal all users' API keys.
Suggested Fix
Encrypt API keys using pgcrypto's pgp_sym_encrypt with an application-level encryption key stored in environment variables. Decrypt on read using pgp_sym_decrypt. Consider using Supabase Vault for secrets management.
HIGHService role key used for token verification
backend/src/middleware/auth.ts:18
[AGENTS: Gatekeeper]auth
The auth middleware uses the SUPABASE_SECRET_KEY (service role key) to verify user tokens. This key has admin privileges and bypasses RLS policies. If the admin client is leaked or misused, an attacker could impersonate any user or perform admin operations. The anon/public key should be used for token verification instead.
Suggested Fix
Replace SUPABASE_SECRET_KEY with SUPABASE_ANON_KEY for token verification. The service role key should only be used for admin operations that require bypassing RLS.
HIGHAdmin client created with service role key for every request
backend/src/middleware/auth.ts:24
[AGENTS: Gatekeeper]auth
A new Supabase admin client is created on every request using the service role key. This client has full admin access and bypasses all Row Level Security policies. Any vulnerability in the middleware or downstream code could lead to privilege escalation.
Suggested Fix
Create the admin client once at startup or use the anon key for regular authentication. Only use the service role key in specific admin endpoints with additional authorization checks.
HIGHUser-controlled message content directly injected into LLM prompt
backend/src/routes/chat.ts:1
[AGENTS: Prompt]llm_security
The `messages` array from the request body is passed directly to `buildMessages()` and then to `runLLMStream()` without any structural separation or sanitization. User-controlled content is concatenated into the LLM prompt, enabling prompt injection attacks where an adversary can override system instructions or manipulate tool selection.
Suggested Fix
Apply input validation and structural separation. Use a dedicated system prompt delimiter and validate that user messages do not contain known injection patterns. Consider using a structured message format where user content is clearly separated from system instructions.
HIGHDatabase error message exposed to client
backend/src/routes/chat.ts:44
[AGENTS: Fuse]error_security
When fetching chats fails, the raw database error message is returned to the client in the response body. This leaks internal database schema details, error codes, and potentially sensitive information about the database structure.
Suggested Fix
Replace `projErr.message` with a generic error message like 'Internal server error' and log the actual error server-side.
HIGHDatabase error message exposed to client
backend/src/routes/chat.ts:55
[AGENTS: Fuse]error_security
When fetching chats fails, the raw database error message is returned to the client. This exposes internal database details.
Suggested Fix
Return a generic error message and log the actual error server-side.
HIGHDatabase error message exposed to client
backend/src/routes/chat.ts:70
[AGENTS: Fuse]error_security
When creating a chat fails, the raw database error message is returned to the client.
Suggested Fix
Return a generic error message and log the actual error server-side.
HIGHDatabase error message exposed to client
backend/src/routes/chat.ts:86
[AGENTS: Fuse]error_security
When fetching a chat fails, the raw database error message is returned to the client.
Suggested Fix
Return a generic error message and log the actual error server-side.
HIGHDatabase error message exposed to client
backend/src/routes/chat.ts:251
[AGENTS: Fuse]error_security
When deleting a chat fails, the raw database error message is returned to the client.
Suggested Fix
Return a generic error message and log the actual error server-side.
HIGHUser-controlled message content directly injected into LLM prompt
backend/src/routes/projectChat.ts:1
[AGENTS: Prompt]llm_security
Similar to chat.ts, user messages from the request body are passed to `buildMessages()` and `runLLMStream()` without sanitization. The `displayed_doc` and `attached_documents` fields are also concatenated into the prompt, allowing an attacker to inject adversarial instructions through document metadata.
Suggested Fix
Apply input validation and structural separation. Validate that user messages and document metadata do not contain injection patterns. Use a structured message format with clear boundaries.
HIGHDatabase error message exposed to client
backend/src/routes/projectChat.ts:84
[AGENTS: Fuse]error_security
When creating a new chat fails, the raw database error message is returned to the client.
Suggested Fix
Return a generic error message and log the actual error server-side.
HIGHSSRF via user-controlled API base URL in fetch requests
backend/src/routes/projects.ts:1
[AGENTS: Specter]ssrf
The projects route uses user-controlled API base URLs in fetch requests without validation, allowing SSRF attacks.
Suggested Fix
Validate and restrict the API base URL to a whitelist of allowed domains.
HIGHUnvalidated file upload endpoint
backend/src/routes/projects.ts:1
[AGENTS: Infiltrator]attack_surface
The POST /projects/:projectId/documents endpoint accepts file uploads with only basic type checking. No file size limits, content validation, or virus scanning is implemented. Malicious uploads could exhaust storage or exploit parsing vulnerabilities.
Suggested Fix
Add file size limits (e.g., 50MB max), content-type validation beyond extension checking, and consider integrating virus scanning for uploaded documents.
HIGHSSRF via user-controlled API base URL in fetch requests
backend/src/routes/tabular.ts:1
[AGENTS: Specter]ssrf
The tabular route uses user-controlled API base URLs in fetch requests without validation, allowing SSRF attacks.
Suggested Fix
Validate and restrict the API base URL to a whitelist of allowed domains.
HIGHUnmapped entry point: tabular review routes
backend/src/routes/tabular.ts:1
[AGENTS: Infiltrator]attack_surface
The entire tabular review module is a new set of REST endpoints with no documented attack surface analysis. It includes file processing, LLM calls, and database operations without explicit rate limiting or input size validation on document content.
Suggested Fix
Add rate limiting, input validation middleware, and document size limits to all tabular endpoints.
HIGHMissing audit trail for account deletion
backend/src/routes/user.ts:19
[AGENTS: Trace]audit_trail
Account deletion via DELETE /user/account is not logged to an audit trail. There is no record of who deleted their account and when. This makes it impossible to investigate unauthorized account deletions or data destruction.
Suggested Fix
Add an audit log entry before deleting the user account. Include user_id, action='account_deleted', timestamp, and any relevant metadata. Consider whether a soft-delete or grace period is appropriate.
HIGHAccount deletion without password confirmation
backend/src/routes/user.ts:22
[AGENTS: Passkey]credentials
DELETE /user/account endpoint allows account deletion with just a valid auth token. There is no requirement to re-enter the current password or provide any additional verification. If a user's session is hijacked (e.g., via XSS or token theft), an attacker can permanently delete the account without any secondary confirmation.
Suggested Fix
Require the user to re-authenticate (e.g., provide current password) before allowing account deletion. Implement a confirmation step with a time-limited token sent via email.
HIGHSSRF via user-controlled API base URL in fetch requests
backend/src/routes/workflows.ts:1
[AGENTS: Specter]ssrf
The workflows route uses user-controlled API base URLs in fetch requests without validation, allowing SSRF attacks.
Suggested Fix
Validate and restrict the API base URL to a whitelist of allowed domains.
HIGHAuth token sent in Authorization header to backend for edit resolution
frontend/src/app/components/assistant/EditCard.tsx:247
[AGENTS: Egress]data_exfiltration
The user's Supabase access token is sent in the Authorization header when accepting or rejecting a document edit. If the backend logs request headers or if the connection is intercepted, the token is exposed. This token grants access to the user's session.
Suggested Fix
Use a short-lived, scoped token for edit resolution operations instead of the user's full session token.
HIGHNo sanitization of quote text before DOM insertion via innerHTML
frontend/src/app/components/shared/highlightQuote.ts:1
[AGENTS: Sanitizer]sanitization
The `highlightQuote` function sets `div.innerHTML` using user-provided `quote` text without any sanitization. The `escapeHtml` function is only applied to the non-highlighted portions of the text, but the highlighted portion (the `quote` text) is inserted directly into the innerHTML via a template literal. This allows an attacker to inject arbitrary HTML/JavaScript if they can control the `quote` parameter, leading to stored XSS.
Suggested Fix
Apply `escapeHtml` to the highlighted portion as well, or use `textContent` with a separate `<span>` element created via `document.createElement` and `appendChild` instead of `innerHTML`.
HIGHNo sanitization of user-provided workflow prompt rendered via ReactMarkdown
frontend/src/app/components/workflows/DisplayWorkflowModal.tsx:1
[AGENTS: Sanitizer]sanitization
The MarkdownBody component renders workflow.prompt_md content through ReactMarkdown without sanitization. Workflow prompts are user-provided and could contain malicious HTML that would be rendered by ReactMarkdown, leading to XSS.
Suggested Fix
Sanitize the markdown content with DOMPurify before rendering, or restrict allowed HTML elements in ReactMarkdown configuration.
Note: Fixing issues can create a domino effect — resolving one finding often surfaces new ones that were previously hidden. Multiple scan-and-fix cycles may be needed until you’re satisfied no further issues remain. How deep you go is your call.