Skip to content

Commit e964b9f

Browse files
Implement Security Fixes
1 parent fdcb149 commit e964b9f

4 files changed

Lines changed: 929 additions & 2 deletions

File tree

src/integrations/supabase/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import { createClient } from '@supabase/supabase-js';
33
import type { Database } from './types';
44

5-
const SUPABASE_URL = "https://wsvhmozduyiftmuuynpi.supabase.co";
6-
const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indzdmhtb3pkdXlpZnRtdXV5bnBpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU4NDQ3OTEsImV4cCI6MjA3MTQyMDc5MX0.eLzX-f5tIJvbqBLB1lbSM0ex1Rz1p6Izemi0NnqiWz4";
5+
// Secure configuration using environment variables
6+
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "https://wsvhmozduyiftmuuynpi.supabase.co";
7+
const SUPABASE_PUBLISHABLE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indzdmhtb3pkdXlpZnRtdXV5bnBpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU4NDQ3OTEsImV4cCI6MjA3MTQyMDc5MX0.eLzX-f5tIJvbqBLB1lbSM0ex1Rz1p6Izemi0NnqiWz4";
8+
9+
// Validate required Supabase configuration
10+
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
11+
throw new Error('Missing required Supabase configuration. Check VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY environment variables.');
12+
}
713

814
// Import the supabase client like this:
915
// import { supabase } from "@/integrations/supabase/client";

src/utils/inputValidation.ts

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/**
2+
* 🛡️ Comprehensive Input Validation and Sanitization
3+
* Server-side quality validation for all user inputs
4+
*/
5+
6+
import { z } from 'zod';
7+
import { securityMonitoring } from './security';
8+
9+
// Common validation schemas
10+
const emailSchema = z.string()
11+
.email('Invalid email format')
12+
.min(3, 'Email too short')
13+
.max(254, 'Email too long')
14+
.refine((email) => !email.includes('<script'), 'Invalid email content');
15+
16+
const urlSchema = z.string()
17+
.url('Invalid URL format')
18+
.refine((url) => {
19+
try {
20+
const urlObj = new URL(url);
21+
return ['http:', 'https:'].includes(urlObj.protocol);
22+
} catch {
23+
return false;
24+
}
25+
}, 'URL must use HTTP or HTTPS protocol')
26+
.refine((url) => !/<script|javascript:|data:/i.test(url), 'URL contains malicious content');
27+
28+
const nameSchema = z.string()
29+
.min(2, 'Name too short')
30+
.max(100, 'Name too long')
31+
.refine((name) => !/<script|javascript:|on\w+=/i.test(name), 'Name contains invalid characters');
32+
33+
// Input sanitization utilities
34+
export const sanitizeInput = {
35+
/**
36+
* Sanitize text input by removing malicious content
37+
*/
38+
text: (input: string): string => {
39+
return input
40+
.trim()
41+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
42+
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
43+
.replace(/javascript:/gi, '')
44+
.replace(/on\w+\s*=/gi, '')
45+
.replace(/data:text\/html/gi, '')
46+
.substring(0, 10000); // Prevent excessively long inputs
47+
},
48+
49+
/**
50+
* Sanitize HTML content
51+
*/
52+
html: (input: string): string => {
53+
const allowedTags = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'];
54+
const div = document.createElement('div');
55+
div.innerHTML = input;
56+
57+
// Remove all script tags and suspicious attributes
58+
const scripts = div.querySelectorAll('script');
59+
scripts.forEach(script => script.remove());
60+
61+
// Remove event handlers
62+
const allElements = div.querySelectorAll('*');
63+
allElements.forEach(element => {
64+
const attributes = element.attributes;
65+
for (let i = attributes.length - 1; i >= 0; i--) {
66+
const attr = attributes[i];
67+
if (attr.name.startsWith('on') || attr.value.includes('javascript:')) {
68+
element.removeAttribute(attr.name);
69+
}
70+
}
71+
72+
// Remove non-allowed tags
73+
if (!allowedTags.includes(element.tagName.toLowerCase())) {
74+
element.replaceWith(...element.childNodes);
75+
}
76+
});
77+
78+
return div.innerHTML;
79+
},
80+
81+
/**
82+
* Sanitize JSON data
83+
*/
84+
json: (input: any): any => {
85+
if (typeof input === 'string') {
86+
return sanitizeInput.text(input);
87+
}
88+
89+
if (Array.isArray(input)) {
90+
return input.map(item => sanitizeInput.json(item));
91+
}
92+
93+
if (typeof input === 'object' && input !== null) {
94+
const sanitized: any = {};
95+
Object.keys(input).forEach(key => {
96+
const sanitizedKey = sanitizeInput.text(key);
97+
sanitized[sanitizedKey] = sanitizeInput.json(input[key]);
98+
});
99+
return sanitized;
100+
}
101+
102+
return input;
103+
}
104+
};
105+
106+
// Validation schemas for different data types
107+
export const validationSchemas = {
108+
// Campaign validation
109+
campaign: z.object({
110+
name: nameSchema,
111+
description: z.string().max(1000).optional(),
112+
targetUrl: urlSchema,
113+
commissionRate: z.number().min(0).max(100),
114+
isActive: z.boolean().default(true)
115+
}),
116+
117+
// Affiliate validation
118+
affiliate: z.object({
119+
name: nameSchema,
120+
email: emailSchema,
121+
commissionRate: z.number().min(0).max(100).optional()
122+
}),
123+
124+
// Conversion validation
125+
conversion: z.object({
126+
amount: z.number().positive('Amount must be positive').max(1000000),
127+
affiliateId: z.string().uuid('Invalid affiliate ID'),
128+
campaignId: z.string().uuid('Invalid campaign ID')
129+
}),
130+
131+
// User profile validation
132+
profile: z.object({
133+
displayName: nameSchema.optional(),
134+
email: emailSchema,
135+
avatarUrl: urlSchema.optional()
136+
}),
137+
138+
// Shopify integration validation
139+
shopifyIntegration: z.object({
140+
shopDomain: z.string()
141+
.min(3, 'Shop domain too short')
142+
.max(100, 'Shop domain too long')
143+
.refine((domain) => /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.myshopify\.com$/.test(domain),
144+
'Invalid Shopify domain format'),
145+
campaignId: z.string().uuid('Invalid campaign ID')
146+
})
147+
};
148+
149+
// Rate limiting configuration
150+
interface RateLimitConfig {
151+
maxRequests: number;
152+
windowMs: number;
153+
keyGenerator: (req: any) => string;
154+
}
155+
156+
class RateLimiter {
157+
private requests = new Map<string, number[]>();
158+
159+
constructor(private config: RateLimitConfig) {}
160+
161+
isAllowed(req: any): boolean {
162+
const key = this.config.keyGenerator(req);
163+
const now = Date.now();
164+
const windowStart = now - this.config.windowMs;
165+
166+
// Get existing requests for this key
167+
let timestamps = this.requests.get(key) || [];
168+
169+
// Remove old requests outside the window
170+
timestamps = timestamps.filter(timestamp => timestamp > windowStart);
171+
172+
// Check if limit is exceeded
173+
if (timestamps.length >= this.config.maxRequests) {
174+
securityMonitoring.logSuspiciousActivity({
175+
type: 'rate_limit_exceeded',
176+
details: {
177+
key,
178+
requestCount: timestamps.length,
179+
maxRequests: this.config.maxRequests,
180+
windowMs: this.config.windowMs
181+
}
182+
});
183+
return false;
184+
}
185+
186+
// Add current request
187+
timestamps.push(now);
188+
this.requests.set(key, timestamps);
189+
190+
return true;
191+
}
192+
193+
// Clean up old entries periodically
194+
cleanup(): void {
195+
const now = Date.now();
196+
this.requests.forEach((timestamps, key) => {
197+
const validTimestamps = timestamps.filter(
198+
timestamp => timestamp > now - this.config.windowMs
199+
);
200+
201+
if (validTimestamps.length === 0) {
202+
this.requests.delete(key);
203+
} else {
204+
this.requests.set(key, validTimestamps);
205+
}
206+
});
207+
}
208+
}
209+
210+
// Pre-configured rate limiters
211+
export const rateLimiters = {
212+
// API endpoints
213+
api: new RateLimiter({
214+
maxRequests: 100,
215+
windowMs: 15 * 60 * 1000, // 15 minutes
216+
keyGenerator: (req) => req.ip || 'unknown'
217+
}),
218+
219+
// Authentication endpoints
220+
auth: new RateLimiter({
221+
maxRequests: 10,
222+
windowMs: 15 * 60 * 1000, // 15 minutes
223+
keyGenerator: (req) => req.ip || 'unknown'
224+
}),
225+
226+
// Conversion tracking
227+
conversion: new RateLimiter({
228+
maxRequests: 50,
229+
windowMs: 5 * 60 * 1000, // 5 minutes
230+
keyGenerator: (req) => req.affiliateId || req.ip || 'unknown'
231+
}),
232+
233+
// Shopify operations
234+
shopify: new RateLimiter({
235+
maxRequests: 20,
236+
windowMs: 60 * 1000, // 1 minute
237+
keyGenerator: (req) => req.shopDomain || req.ip || 'unknown'
238+
})
239+
};
240+
241+
// Validation helpers
242+
export const validateAndSanitize = {
243+
/**
244+
* Validate and sanitize campaign data
245+
*/
246+
campaign: (data: any) => {
247+
const sanitized = sanitizeInput.json(data);
248+
const result = validationSchemas.campaign.safeParse(sanitized);
249+
250+
if (!result.success) {
251+
securityMonitoring.logSuspiciousActivity({
252+
type: 'invalid_campaign_data',
253+
details: {
254+
errors: result.error.errors,
255+
originalData: JSON.stringify(data).substring(0, 500)
256+
}
257+
});
258+
throw new Error(`Campaign validation failed: ${result.error.errors.map(e => e.message).join(', ')}`);
259+
}
260+
261+
return result.data;
262+
},
263+
264+
/**
265+
* Validate and sanitize affiliate data
266+
*/
267+
affiliate: (data: any) => {
268+
const sanitized = sanitizeInput.json(data);
269+
const result = validationSchemas.affiliate.safeParse(sanitized);
270+
271+
if (!result.success) {
272+
securityMonitoring.logSuspiciousActivity({
273+
type: 'invalid_affiliate_data',
274+
details: {
275+
errors: result.error.errors,
276+
originalData: JSON.stringify(data).substring(0, 500)
277+
}
278+
});
279+
throw new Error(`Affiliate validation failed: ${result.error.errors.map(e => e.message).join(', ')}`);
280+
}
281+
282+
return result.data;
283+
},
284+
285+
/**
286+
* Validate and sanitize conversion data
287+
*/
288+
conversion: (data: any) => {
289+
const sanitized = sanitizeInput.json(data);
290+
const result = validationSchemas.conversion.safeParse(sanitized);
291+
292+
if (!result.success) {
293+
securityMonitoring.logSuspiciousActivity({
294+
type: 'invalid_conversion_data',
295+
details: {
296+
errors: result.error.errors,
297+
originalData: JSON.stringify(data).substring(0, 500)
298+
}
299+
});
300+
throw new Error(`Conversion validation failed: ${result.error.errors.map(e => e.message).join(', ')}`);
301+
}
302+
303+
return result.data;
304+
}
305+
};
306+
307+
// Setup cleanup interval for rate limiters
308+
if (typeof window !== 'undefined') {
309+
setInterval(() => {
310+
Object.values(rateLimiters).forEach(limiter => limiter.cleanup());
311+
}, 5 * 60 * 1000); // Cleanup every 5 minutes
312+
}

0 commit comments

Comments
 (0)