-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[Security] Fix CRITICAL vulnerability: V-001 #3083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,36 @@ | ||
| -- Audit logging function for security events | ||
| CREATE TABLE IF NOT EXISTS audit_logs ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| event_type TEXT NOT NULL, | ||
| user_id UUID, | ||
| resource_type TEXT, | ||
| resource_id UUID, | ||
| action TEXT NOT NULL, | ||
| details JSONB, | ||
| ip_address INET, | ||
| created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, | ||
| created_by UUID DEFAULT auth.uid() | ||
| ); | ||
|
|
||
| -- Create index on audit_logs for efficient querying | ||
| CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); | ||
| CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at); | ||
| CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type ON audit_logs(event_type); | ||
|
|
||
| -- Helper function to log security events | ||
| CREATE OR REPLACE FUNCTION log_security_event( | ||
| event_type TEXT, | ||
| action TEXT, | ||
| resource_type TEXT DEFAULT NULL, | ||
| resource_id UUID DEFAULT NULL, | ||
| details JSONB DEFAULT NULL | ||
| ) RETURNS VOID AS $$ | ||
| BEGIN | ||
| INSERT INTO audit_logs (event_type, user_id, resource_type, resource_id, action, details, created_by) | ||
| VALUES (event_type, auth.uid(), resource_type, resource_id, action, details, auth.uid()); | ||
| END; | ||
| $$ LANGUAGE plpgsql SECURITY DEFINER; | ||
|
|
||
| -- Helper function to check if user has specific roles for a project | ||
| CREATE OR REPLACE FUNCTION user_has_project_access( | ||
| project_id_param UUID, | ||
|
|
@@ -78,6 +111,13 @@ FOR SELECT | |
| TO authenticated | ||
| USING (user_has_project_access(canvas.project_id, ARRAY['owner', 'admin'])); | ||
|
|
||
| DROP POLICY IF EXISTS "canvas_deny_select_policy" ON canvas; | ||
| -- Deny SELECT access to unauthenticated users | ||
| CREATE POLICY "canvas_deny_select_policy" ON canvas | ||
| FOR SELECT | ||
| TO public | ||
| USING (false); | ||
|
|
||
|
Comment on lines
+114
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cd apps/backend/supabase/migrations && wc -l 0006_rls.sqlRepository: onlook-dev/onlook Length of output: 77 🏁 Script executed: head -n 150 apps/backend/supabase/migrations/0006_rls.sql | tail -n +100Repository: onlook-dev/onlook Length of output: 2057 🏁 Script executed: cat -n apps/backend/supabase/migrations/0006_rls.sql | grep -A 50 "canvas"Repository: onlook-dev/onlook Length of output: 9750 🏁 Script executed: find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.py" \) -not -path "*/node_modules/*" -not -path "*/.next/*" | head -20Repository: onlook-dev/onlook Length of output: 1127 🏁 Script executed: rg "canvas" --type ts --type js -i -l | grep -E "(server|backend|api)" | head -10Repository: onlook-dev/onlook Length of output: 540 🏁 Script executed: rg "from.*canvas|select.*canvas" --type ts --type js -i -A 2 | head -50Repository: onlook-dev/onlook Length of output: 3242 🏁 Script executed: cat -n apps/web/client/src/server/api/routers/project/frame.ts | head -100Repository: onlook-dev/onlook Length of output: 3096 🏁 Script executed: cat -n apps/web/client/src/server/api/routers/project/project.ts | head -80Repository: onlook-dev/onlook Length of output: 3487 🏁 Script executed: rg "supabase|createClient" --type ts --type js apps/web/client/src/server -A 3 | head -60Repository: onlook-dev/onlook Length of output: 2555 🏁 Script executed: find . -type f -name "client.ts" -path "*/db/*" | head -5Repository: onlook-dev/onlook Length of output: 88 🏁 Script executed: cat -n packages/db/src/client.tsRepository: onlook-dev/onlook Length of output: 767 🏁 Script executed: rg "createAdminClient|createClient" --type ts apps/web/client/src/utils/supabase -A 10Repository: onlook-dev/onlook Length of output: 6509 🏁 Script executed: cat -n apps/web/client/src/server/api/trpc.ts | head -50Repository: onlook-dev/onlook Length of output: 1944 Consider using The concern about privileged roles breaking is not applicable here—the backend accesses the database directly via a postgres connection (not RLS-aware), completely bypassing this policy. However, since the comment states the intent is to "Deny SELECT access to unauthenticated users," Suggested adjustment CREATE POLICY "canvas_deny_select_policy" ON canvas
FOR SELECT
-TO public
+TO anon
USING (false);🤖 Prompt for AI Agents |
||
| DROP POLICY IF EXISTS "canvas_update_policy" ON canvas; | ||
| -- 3. UPDATE: Allow users with 'owner' or 'admin' role in user_projects for the canvas's project | ||
| CREATE POLICY "canvas_update_policy" ON canvas | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,69 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { env } from '@/env'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Simple HTML sanitization function to prevent XSS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function sanitizeHtml(input: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/&/g, '&') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/</g, '<') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/>/g, '>') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/"/g, '"') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/'/g, ''') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\//g, '/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Rate limiter: simple in-memory store (in production, use Redis or similar) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestCounts = new Map<string, number[]>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_MAX_REQUESTS = 5; // 5 requests per minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isRateLimited(clientIp: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const windowStart = now - RATE_LIMIT_WINDOW_MS; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!requestCounts.has(clientIp)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requestCounts.set(clientIp, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timestamps = requestCounts.get(clientIp)!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove old timestamps outside the window | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const recentTimestamps = timestamps.filter(t => t > windowStart); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (recentTimestamps.length >= RATE_LIMIT_MAX_REQUESTS) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentTimestamps.push(now); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requestCounts.set(clientIp, recentTimestamps); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In-memory rate limiter has limitations in serverless/multi-instance deployments. The comment acknowledges this, but there are additional concerns:
Add periodic cleanup to prevent memory growth+// Cleanup old entries periodically (every 100 requests)
+let requestCounter = 0;
+const CLEANUP_INTERVAL = 100;
+
+function cleanupOldEntries(): void {
+ const now = Date.now();
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
+ for (const [ip, timestamps] of requestCounts.entries()) {
+ const recent = timestamps.filter(t => t > windowStart);
+ if (recent.length === 0) {
+ requestCounts.delete(ip);
+ } else {
+ requestCounts.set(ip, recent);
+ }
+ }
+}
+
function isRateLimited(clientIp: string): boolean {
+ if (++requestCounter % CLEANUP_INTERVAL === 0) {
+ cleanupOldEntries();
+ }
const now = Date.now();📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract client IP for rate limiting | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clientIp = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.headers.get('x-real-ip') || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'unknown'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check rate limit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isRateLimited(clientIp)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(JSON.stringify({ error: 'Too many requests' }), { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 429, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name, email, utm_source, utm_medium, utm_campaign, utm_term, utm_content } = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create Zod schema for validation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emailCaptureSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: z.string().trim().min(1, 'Name is required'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: z.string().trim().email('Invalid email format'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_source: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_medium: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_campaign: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_term: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_content: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: z.string().trim().min(1, 'Name is required').max(255, 'Name is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: z.string().trim().email('Invalid email format').max(255, 'Email is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_source: z.string().optional().max(255, 'utm_source is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_medium: z.string().optional().max(255, 'utm_medium is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_campaign: z.string().optional().max(255, 'utm_campaign is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_term: z.string().optional().max(255, 'utm_term is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_content: z.string().optional().max(255, 'utm_content is too long'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate input data with Zod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,6 +87,17 @@ export async function POST(request: Request) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validatedData = validationResult.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Sanitize string fields to prevent XSS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sanitizedData = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: sanitizeHtml(validatedData.name), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: validatedData.email, // Email is already validated and safe | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_source: validatedData.utm_source ? sanitizeHtml(validatedData.utm_source) : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_medium: validatedData.utm_medium ? sanitizeHtml(validatedData.utm_medium) : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_campaign: validatedData.utm_campaign ? sanitizeHtml(validatedData.utm_campaign) : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_term: validatedData.utm_term ? sanitizeHtml(validatedData.utm_term) : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utm_content: validatedData.utm_content ? sanitizeHtml(validatedData.utm_content) : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headerName = env.N8N_LANDING_FORM_HEADER_NAME; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headerValue = env.N8N_LANDING_FORM_HEADER_VALUE; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const landingFormUrl = env.N8N_LANDING_FORM_URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,14 +111,14 @@ export async function POST(request: Request) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(landingFormUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url.searchParams.append('name', validatedData.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url.searchParams.append('email', validatedData.email); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validatedData.utm_source) url.searchParams.append('utm_source', validatedData.utm_source); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validatedData.utm_medium) url.searchParams.append('utm_medium', validatedData.utm_medium); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validatedData.utm_campaign) url.searchParams.append('utm_campaign', validatedData.utm_campaign); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validatedData.utm_term) url.searchParams.append('utm_term', validatedData.utm_term); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validatedData.utm_content) url.searchParams.append('utm_content', validatedData.utm_content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url.searchParams.append('name', sanitizedData.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url.searchParams.append('email', sanitizedData.email); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sanitizedData.utm_source) url.searchParams.append('utm_source', sanitizedData.utm_source); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sanitizedData.utm_medium) url.searchParams.append('utm_medium', sanitizedData.utm_medium); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sanitizedData.utm_campaign) url.searchParams.append('utm_campaign', sanitizedData.utm_campaign); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sanitizedData.utm_term) url.searchParams.append('utm_term', sanitizedData.utm_term); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sanitizedData.utm_content) url.searchParams.append('utm_content', sanitizedData.utm_content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build auth headers: use custom header if provided | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authHeaders: Record<string, string> = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,8 +6,60 @@ import type { | |||||
| DeploymentResponse | ||||||
| } from '@onlook/models'; | ||||||
|
|
||||||
| // File upload validation constants | ||||||
| const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file | ||||||
| const MAX_TOTAL_SIZE = 500 * 1024 * 1024; // 500MB total | ||||||
| const MAX_FILES = 1000; | ||||||
| const ALLOWED_FILE_TYPES = [ | ||||||
| 'text/html', | ||||||
| 'text/css', | ||||||
| 'application/javascript', | ||||||
| 'text/javascript', | ||||||
| 'application/json', | ||||||
| 'image/jpeg', | ||||||
| 'image/png', | ||||||
| 'image/gif', | ||||||
| 'image/svg+xml', | ||||||
| 'application/pdf', | ||||||
| ]; | ||||||
|
|
||||||
| export class FreestyleAdapter implements HostingProviderAdapter { | ||||||
| private validateFileUploads(files: Record<string, Blob>): void { | ||||||
| // Validate number of files | ||||||
| const fileCount = Object.keys(files).length; | ||||||
| if (fileCount === 0) { | ||||||
| throw new Error('No files provided'); | ||||||
| } | ||||||
| if (fileCount > MAX_FILES) { | ||||||
| throw new Error(`Too many files. Maximum allowed: ${MAX_FILES}`); | ||||||
| } | ||||||
|
|
||||||
| // Validate individual files and calculate total size | ||||||
| let totalSize = 0; | ||||||
| for (const [filename, file] of Object.entries(files)) { | ||||||
| // Validate file size | ||||||
| if (file.size > MAX_FILE_SIZE) { | ||||||
| throw new Error(`File "${filename}" exceeds maximum size of ${MAX_FILE_SIZE / 1024 / 1024}MB`); | ||||||
| } | ||||||
|
|
||||||
| // Validate file type | ||||||
| if (file.type && !ALLOWED_FILE_TYPES.includes(file.type)) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In
Suggested change
|
||||||
| throw new Error(`File type "${file.type}" for "${filename}" is not allowed`); | ||||||
| } | ||||||
|
|
||||||
| totalSize += file.size; | ||||||
| } | ||||||
|
|
||||||
| // Validate total size | ||||||
| if (totalSize > MAX_TOTAL_SIZE) { | ||||||
| throw new Error(`Total file size exceeds maximum of ${MAX_TOTAL_SIZE / 1024 / 1024}MB`); | ||||||
| } | ||||||
| } | ||||||
|
Comment on lines
+27
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the file and check its structure
cd apps/web/client/src/server/api/routers/domain/adapters && pwd && ls -la | head -20Repository: onlook-dev/onlook Length of output: 307 🏁 Script executed: #!/bin/bash
# Look for DeploymentFile interface/type definition
rg -n 'interface DeploymentFile|type DeploymentFile' --type=ts -A 5Repository: onlook-dev/onlook Length of output: 429 🏁 Script executed: #!/bin/bash
# Check the freestyle.ts file for the method signature and imports
cat -n apps/web/client/src/server/api/routers/domain/adapters/freestyle.ts | head -80Repository: onlook-dev/onlook Length of output: 3172 🏁 Script executed: #!/bin/bash
# Find DeploymentRequest interface
rg -n 'interface DeploymentRequest|type DeploymentRequest' --type=ts -A 10Repository: onlook-dev/onlook Length of output: 724 🏁 Script executed: #!/bin/bash
# Also check the freestyle-sandboxes SDK to understand what deployWeb expects
rg -n 'deployWeb' --type=ts -B 2 -A 2Repository: onlook-dev/onlook Length of output: 563 Critical type mismatch breaks entire file validation logic. The method signature expects
As a result, the validation logic fails at runtime:
This breaks both file size validation and file type allowlisting. The method must either accept and validate 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| async deploy(request: DeploymentRequest): Promise<DeploymentResponse> { | ||||||
| // Validate file uploads before processing | ||||||
| this.validateFileUploads(request.files); | ||||||
|
|
||||||
| const sdk = initializeFreestyleSdk(); | ||||||
|
|
||||||
| const res = await sdk.deployWeb( | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 174
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 14845
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1202
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 18337
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 3381
🌐 Web query:
PostgreSQL SECURITY DEFINER search_path security best practice💡 Result:
Short best-practice checklist for SECURITY DEFINER + search_path (concise):
Key references:
If you want, I can:
🌐 Web query:
Supabase audit logging table RLS security recommendations💡 Result:
Short checklist — RLS for Supabase audit-log tables
If you want, I can:
Citations:
Pin
search_pathin all threeSECURITY DEFINERfunctions and explicitly lock downaudit_logstable access.Three functions in this migration are marked
SECURITY DEFINERwithout pinnedsearch_path:log_security_event()(line 32),user_has_project_access()(line 47), anduser_has_canvas_access()(line 63). This is a known vulnerability; unbound search_path in SECURITY DEFINER functions can be exploited via search_path hijacking attacks. Additionally,audit_logstable has no RLS enabled and no explicit privilege controls—it will inherit default PUBLIC grants, potentially exposing sensitive audit data. Theip_addresscolumn is defined but never populated by the helper function.Add
SET search_path = pg_catalog, public;to all three function definitions:For
audit_logs, enable RLS and lock down access:Schema-qualify the table reference in
log_security_event(): change line 29 fromINSERT INTO audit_logstoINSERT INTO public.audit_logs.🤖 Prompt for AI Agents