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 ) => ! / < s c r i p t | j a v a s c r i p t : | d a t a : / 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 ) => ! / < s c r i p t | j a v a s c r i p t : | o n \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 ( / < s c r i p t \b [ ^ < ] * (?: (? ! < \/ s c r i p t > ) < [ ^ < ] * ) * < \/ s c r i p t > / gi, '' )
42+ . replace ( / < i f r a m e \b [ ^ < ] * (?: (? ! < \/ i f r a m e > ) < [ ^ < ] * ) * < \/ i f r a m e > / gi, '' )
43+ . replace ( / j a v a s c r i p t : / gi, '' )
44+ . replace ( / o n \w + \s * = / gi, '' )
45+ . replace ( / d a t a : t e x t \/ h t m l / 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 - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] * [ a - z A - Z 0 - 9 ] * \. m y s h o p i f y \. c o m $ / . 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