diff --git a/package-lock.json b/package-lock.json index 35fd312a..e6b91394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,8 @@ "uuid": "^9.0.1", "web3": "^4.3.0", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1" + "winston-daily-rotate-file": "^4.7.1", + "xss": "^1.0.15" }, "devDependencies": { "@commitlint/cli": "^18.4.3", @@ -77,6 +78,7 @@ "@types/cors": "^2.8.17", "@types/crypto-js": "^4.2.1", "@types/express": "^4.17.21", + "@types/file-type": "^10.6.0", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", @@ -4831,6 +4833,16 @@ "@types/send": "*" } }, + "node_modules/@types/file-type": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/@types/file-type/-/file-type-10.6.0.tgz", + "integrity": "sha512-qn06Cjx7HZDMmDIqMn+smKr7JYQMVXoHqwMC2vdg19CxGpL0Q6KwuvnVEkws2sH5wqrWXExmvuztXtWSHs3MBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -7808,6 +7820,12 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, "node_modules/csv-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", @@ -18675,6 +18693,28 @@ } } }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 302432e9..3e21388e 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,8 @@ "uuid": "^9.0.1", "web3": "^4.3.0", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1" + "winston-daily-rotate-file": "^4.7.1", + "xss": "^1.0.15" }, "devDependencies": { "@commitlint/cli": "^18.4.3", @@ -111,6 +112,7 @@ "@types/cors": "^2.8.17", "@types/crypto-js": "^4.2.1", "@types/express": "^4.17.21", + "@types/file-type": "^10.6.0", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", @@ -140,7 +142,6 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.3.2" }, - "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" diff --git a/src/common/controllers/audit.controller.ts b/src/common/controllers/audit.controller.ts index 8b83882a..207cf96f 100644 --- a/src/common/controllers/audit.controller.ts +++ b/src/common/controllers/audit.controller.ts @@ -27,7 +27,6 @@ import { Action } from '../../rbac/enums/action.enum'; import { RequireScopes } from '../decorators/require-scopes.decorator'; import { AuditInterceptor } from '../interceptors/audit.interceptor'; -// Audit controller @ApiTags('Audit & Compliance') @Controller('audit') @UseGuards(JwtAuthGuard, RbacGuard) diff --git a/src/common/middleware/request-size-limiter.middleware.ts b/src/common/middleware/request-size-limiter.middleware.ts new file mode 100644 index 00000000..fb80cdb4 --- /dev/null +++ b/src/common/middleware/request-size-limiter.middleware.ts @@ -0,0 +1,52 @@ +import { Injectable, NestMiddleware, BadRequestException } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class RequestSizeLimiterMiddleware implements NestMiddleware { + private readonly maxRequestSize: number; + private readonly maxUrlLength: number; + private readonly maxHeadersCount: number; + + constructor() { + // Get values from config or use defaults + this.maxRequestSize = parseInt(process.env.MAX_REQUEST_SIZE || '10485760', 10); // 10MB default + this.maxUrlLength = parseInt(process.env.MAX_URL_LENGTH || '2048', 10); // 2KB default + this.maxHeadersCount = parseInt(process.env.MAX_HEADERS_COUNT || '50', 10); // 50 headers default + } + + use(req: Request, res: Response, next: NextFunction) { + // Check URL length + if (req.url.length > this.maxUrlLength) { + throw new BadRequestException(`Request URL exceeds maximum allowed length of ${this.maxUrlLength} characters`); + } + + // Check number of headers + const headerCount = Object.keys(req.headers).length; + if (headerCount > this.maxHeadersCount) { + throw new BadRequestException(`Request exceeds maximum allowed headers count of ${this.maxHeadersCount}`); + } + + // Check content length header if present + const contentLength = req.headers['content-length']; + if (contentLength) { + const size = parseInt(contentLength, 10); + if (size > this.maxRequestSize) { + throw new BadRequestException(`Request body exceeds maximum allowed size of ${this.maxRequestSize} bytes`); + } + } + + // For chunked requests or when content-length is not available, monitor stream size + let receivedSize = 0; + if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { + req.on('data', (chunk: Buffer) => { + receivedSize += chunk.length; + if (receivedSize > this.maxRequestSize) { + req.destroy(); + throw new BadRequestException(`Request body exceeds maximum allowed size of ${this.maxRequestSize} bytes`); + } + }); + } + + next(); + } +} diff --git a/src/common/validators/file-upload.validator.ts b/src/common/validators/file-upload.validator.ts new file mode 100644 index 00000000..cc03ac6b --- /dev/null +++ b/src/common/validators/file-upload.validator.ts @@ -0,0 +1,307 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HttpStatus } from '@nestjs/common'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileTypeFromBuffer } from 'file-type'; + +// Common file types for documents and images +const ALLOWED_MIME_TYPES = [ + // Images + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'image/svg+xml', + 'image/tiff', + 'image/bmp', + 'image/x-icon', + + // Documents + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'text/plain', + 'text/csv', + 'application/json', + 'application/xml', + 'application/zip', + 'application/x-rar-compressed', + 'application/x-tar', + 'application/gzip', + 'application/x-7z-compressed', +]; + +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + +/** + * Custom validator constraint for file upload validation + */ +@ValidatorConstraint({ async: true }) +export class FileUploadValidatorConstraint implements ValidatorConstraintInterface { + async validate(file: Express.Multer.File) { + if (!file) { + return false; + } + + // Check file size + if (file.size > MAX_FILE_SIZE) { + return false; + } + + // Check MIME type + const detectedType = await fileTypeFromBuffer(file.buffer); + if (!detectedType || !ALLOWED_MIME_TYPES.includes(detectedType.mime)) { + return false; + } + + // Check file extension + const fileExtension = path.extname(file.originalname).toLowerCase(); + const validExtensions = this.getAllowedExtensions(); + if (!validExtensions.some(ext => ext === fileExtension)) { + return false; + } + + // Additional security checks for potential malicious content + if (await this.containsMaliciousContent(file.buffer)) { + return false; + } + + return true; + } + + private getAllowedExtensions(): string[] { + const extensionsMap: { [key: string]: string[] } = { + 'image/jpeg': ['.jpg', '.jpeg'], + 'image/png': ['.png'], + 'image/webp': ['.webp'], + 'image/gif': ['.gif'], + 'image/svg+xml': ['.svg'], + 'image/tiff': ['.tiff', '.tif'], + 'image/bmp': ['.bmp'], + 'image/x-icon': ['.ico'], + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/vnd.ms-powerpoint': ['.ppt'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], + 'text/plain': ['.txt'], + 'text/csv': ['.csv'], + 'application/json': ['.json'], + 'application/xml': ['.xml'], + 'application/zip': ['.zip'], + 'application/x-rar-compressed': ['.rar'], + 'application/x-tar': ['.tar'], + 'application/gzip': ['.gz'], + 'application/x-7z-compressed': ['.7z'], + }; + + const extensions: string[] = []; + ALLOWED_MIME_TYPES.forEach(mimeType => { + if (extensionsMap[mimeType]) { + extensions.push(...extensionsMap[mimeType]); + } + }); + + return extensions; + } + + private async containsMaliciousContent(buffer: Buffer): Promise { + // Check for common malicious patterns in the file content + const maliciousPatterns = [ + /)<[^<]*)*<\/script>/gi, // JavaScript in HTML + /)<[^<]*)*<\/iframe>/gi, // iframe in HTML + /)<[^<]*)*<\/object>/gi, // object in HTML + /)<[^<]*)*<\/embed>/gi, // embed in HTML + /]+rel=["']stylesheet["'][^>]*href=["']javascript:/gi, // JS in link href + /]+http-equiv=["']refresh["'][^>]*content=["']\d+;url=javascript:/gi, // JS in meta refresh + /vbscript:/gi, // VBScript protocol + /javascript:/gi, // JavaScript protocol + /data:text\/html/gi, // Data URI with HTML + /eval\s*\(/gi, // eval function + /expression\s*\(/gi, // expression function (IE) + /onload\s*=/gi, // onload event + /onerror\s*=/gi, // onerror event + /onclick\s*=/gi, // onclick event + /onmouseover\s*=/gi, // onmouseover event + /onfocus\s*=/gi, // onfocus event + /onblur\s*=/gi, // onblur event + ]; + + const content = buffer.toString('utf-8').toLowerCase(); + + for (const pattern of maliciousPatterns) { + if (pattern.test(content)) { + return true; + } + } + + return false; + } + + defaultMessage() { + return 'File upload validation failed: invalid file type, size, or contains malicious content'; + } +} + +/** + * Decorator to validate uploaded files + */ +export function IsValidFileUpload(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [], + validator: FileUploadValidatorConstraint, + }); + }; +} + +/** + * Function to validate file upload with custom parameters + */ +export async function validateFileUpload( + file: Express.Multer.File, + allowedMimeTypes: string[] = ALLOWED_MIME_TYPES, + maxSize: number = MAX_FILE_SIZE, +): Promise<{ isValid: boolean; errors: string[] }> { + const errors: string[] = []; + + if (!file) { + errors.push('File is required'); + return { isValid: false, errors }; + } + + // Check file size + if (file.size > maxSize) { + errors.push(`File size exceeds maximum allowed size of ${maxSize / (1024 * 1024)} MB`); + } + + // Check MIME type + const detectedType = await fileTypeFromBuffer(file.buffer); + if (!detectedType || !allowedMimeTypes.includes(detectedType.mime)) { + errors.push(`File type ${detectedType?.mime || 'unknown'} is not allowed`); + } + + // Check file extension + const fileExtension = path.extname(file.originalname).toLowerCase(); + const validExtensions = getAllowedExtensionsForMimeTypes(allowedMimeTypes); + if (!validExtensions.some(ext => ext === fileExtension)) { + errors.push(`File extension ${fileExtension} is not allowed`); + } + + // Additional security checks for potential malicious content + if (await containsMaliciousContent(file.buffer)) { + errors.push('File contains potentially malicious content'); + } + + return { isValid: errors.length === 0, errors }; +} + +/** + * Helper function to get allowed extensions for given mime types + */ +function getAllowedExtensionsForMimeTypes(allowedMimeTypes: string[]): string[] { + const extensionsMap: { [key: string]: string[] } = { + 'image/jpeg': ['.jpg', '.jpeg'], + 'image/png': ['.png'], + 'image/webp': ['.webp'], + 'image/gif': ['.gif'], + 'image/svg+xml': ['.svg'], + 'image/tiff': ['.tiff', '.tif'], + 'image/bmp': ['.bmp'], + 'image/x-icon': ['.ico'], + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/vnd.ms-powerpoint': ['.ppt'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], + 'text/plain': ['.txt'], + 'text/csv': ['.csv'], + 'application/json': ['.json'], + 'application/xml': ['.xml'], + 'application/zip': ['.zip'], + 'application/x-rar-compressed': ['.rar'], + 'application/x-tar': ['.tar'], + 'application/gzip': ['.gz'], + 'application/x-7z-compressed': ['.7z'], + }; + + const extensions: string[] = []; + allowedMimeTypes.forEach(mimeType => { + if (extensionsMap[mimeType]) { + extensions.push(...extensionsMap[mimeType]); + } + }); + + return extensions; +} + +/** + * Helper function to check for malicious content in buffer + */ +async function containsMaliciousContent(buffer: Buffer): Promise { + // Check for common malicious patterns in the file content + const maliciousPatterns = [ + /)<[^<]*)*<\/script>/gi, // JavaScript in HTML + /)<[^<]*)*<\/iframe>/gi, // iframe in HTML + /)<[^<]*)*<\/object>/gi, // object in HTML + /)<[^<]*)*<\/embed>/gi, // embed in HTML + /]+rel=["']stylesheet["'][^>]*href=["']javascript:/gi, // JS in link href + /]+http-equiv=["']refresh["'][^>]*content=["']\d+;url=javascript:/gi, // JS in meta refresh + /vbscript:/gi, // VBScript protocol + /javascript:/gi, // JavaScript protocol + /data:text\/html/gi, // Data URI with HTML + /eval\s*\(/gi, // eval function + /expression\s*\(/gi, // expression function (IE) + /onload\s*=/gi, // onload event + /onerror\s*=/gi, // onerror event + /onclick\s*=/gi, // onclick event + /onmouseover\s*=/gi, // onmouseover event + /onfocus\s*=/gi, // onfocus event + /onblur\s*=/gi, // onblur event + ]; + + const content = buffer.toString('utf-8').toLowerCase(); + + for (const pattern of maliciousPatterns) { + if (pattern.test(content)) { + return true; + } + } + + return false; +} + +/** + * Get file type information + */ +export async function getFileTypeInfo( + buffer: Buffer, +): Promise<{ mimeType: string; extension: string; isValid: boolean }> { + const detectedType = await fileTypeFromBuffer(buffer); + + if (!detectedType) { + return { mimeType: 'unknown', extension: '', isValid: false }; + } + + return { + mimeType: detectedType.mime, + extension: `.${detectedType.ext}`, + isValid: ALLOWED_MIME_TYPES.includes(detectedType.mime), + }; +} diff --git a/src/common/validators/sql-injection.validator.ts b/src/common/validators/sql-injection.validator.ts new file mode 100644 index 00000000..ac82e376 --- /dev/null +++ b/src/common/validators/sql-injection.validator.ts @@ -0,0 +1,120 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +// Common SQL injection patterns +const SQL_INJECTION_PATTERNS = [ + /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/gi, + /(;|\-\-|\#|\/\*|\*\/)/g, + /(\b(OR|AND)\b\s*\d+\s*[=<>]\s*\d+)/gi, + /(=\s*'?\d+'\s*(OR|AND))/gi, + /(--|#|\/\*|\*\/)/g, + /\b(OR|AND)\b\s+1\s*=\s*1/gi, + /\b(OR|AND)\b\s+'1'\s*=\s*'1'/gi, +]; + +/** + * Custom validator constraint for SQL injection prevention + */ +@ValidatorConstraint({ async: false }) +export class SqlInjectionValidatorConstraint implements ValidatorConstraintInterface { + validate(value: any) { + if (typeof value !== 'string') { + return true; // Only validate strings + } + + // Check against known SQL injection patterns + for (const pattern of SQL_INJECTION_PATTERNS) { + if (pattern.test(value)) { + return false; + } + } + + return true; + } + + defaultMessage() { + return 'The input contains potentially malicious SQL injection content'; + } +} + +/** + * Decorator to validate input against SQL injection + */ +export function IsNotSqlInjection(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [], + validator: SqlInjectionValidatorConstraint, + }); + }; +} + +/** + * Function to check if a string contains potential SQL injection patterns + */ +export function containsSqlInjection(input: string): boolean { + if (typeof input !== 'string') { + return false; + } + + for (const pattern of SQL_INJECTION_PATTERNS) { + if (pattern.test(input)) { + return true; + } + } + + return false; +} + +/** + * Function to sanitize input against SQL injection + */ +export function sanitizeSqlInjection(input: string): string { + if (typeof input !== 'string') { + return input; + } + + let sanitized = input; + + // Remove potentially dangerous SQL keywords and characters + sanitized = sanitized.replace(/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/gi, ''); + sanitized = sanitized.replace(/(;|\-\-|\#|\/\*|\*\/)/g, ''); + sanitized = sanitized.replace(/(\b(OR|AND)\b\s*\d+\s*[=<>]\s*\d+)/gi, ''); + sanitized = sanitized.replace(/('|--|#|\/\*|\*\/)/g, ''); + + return sanitized.trim(); +} + +/** + * Function to sanitize an object against SQL injection + */ +export function sanitizeObjectSqlInjection(obj: any): any { + if (obj === null || obj === undefined) { + return obj; + } + + if (typeof obj === 'string') { + return sanitizeSqlInjection(obj); + } + + if (typeof obj === 'object') { + const sanitized: any = Array.isArray(obj) ? [] : {}; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + sanitized[key] = sanitizeObjectSqlInjection(obj[key]); + } + } + + return sanitized; + } + + return obj; +} diff --git a/src/common/validators/xss.validator.ts b/src/common/validators/xss.validator.ts new file mode 100644 index 00000000..97a11f47 --- /dev/null +++ b/src/common/validators/xss.validator.ts @@ -0,0 +1,108 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import * as xss from 'xss'; + +/** + * Custom validator constraint for XSS protection + */ +@ValidatorConstraint({ async: false }) +export class XssValidatorConstraint implements ValidatorConstraintInterface { + validate(value: any) { + if (typeof value !== 'string') { + return true; // Only validate strings + } + + // Check if value contains potential XSS patterns + // Instead of sanitizing, we'll check for dangerous patterns + const dangerousPatterns = [ + /]/i, // Script tags + /]/i, // Iframe tags + /]/i, // Object tags + /]/i, // Embed tags + /].*?javascript:/i, // Form with JS + /javascript:/i, // JavaScript protocol + /vbscript:/i, // VBScript protocol + /data:text\/html/i, // Data HTML URIs + /onload\s*=|onerror\s*=|onclick\s*=|onmouseover\s*=|onfocus\s*=|onblur\s*=/i, // Event handlers + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(value)) { + return false; + } + } + + return true; + } + + defaultMessage() { + return 'The input contains potentially malicious content (XSS)'; + } +} + +/** + * Decorator to validate and sanitize input against XSS attacks + */ +export function IsXssSafe(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [], + validator: XssValidatorConstraint, + }); + }; +} + +/** + * Function to sanitize input against XSS + */ +export function sanitizeXss(input: string): string { + if (typeof input !== 'string') { + return input; + } + + if (typeof input !== 'string') { + return input; + } + + // For sanitization, we'll use the xss library with a configuration + // that removes all HTML tags but preserves plain text + const options = { + whiteList: {}, // Allow no HTML tags for maximum security + }; + + return xss.filterXSS(input, options); +} + +/** + * Function to sanitize an object against XSS + */ +export function sanitizeObjectXss(obj: any): any { + if (obj === null || obj === undefined) { + return obj; + } + + if (typeof obj === 'string') { + return sanitizeXss(obj); + } + + if (typeof obj === 'object') { + const sanitized: any = Array.isArray(obj) ? [] : {}; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + sanitized[key] = sanitizeObjectXss(obj[key]); + } + } + + return sanitized; + } + + return obj; +} diff --git a/src/properties/dto/create-property.dto.ts b/src/properties/dto/create-property.dto.ts index 3f0c63d9..9c1e1a8d 100644 --- a/src/properties/dto/create-property.dto.ts +++ b/src/properties/dto/create-property.dto.ts @@ -14,6 +14,8 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsXssSafe } from '../../common/validators/xss.validator'; +import { IsNotSqlInjection } from '../../common/validators/sql-injection.validator'; export enum PropertyType { RESIDENTIAL = 'RESIDENTIAL', @@ -38,6 +40,8 @@ export class AddressDto { @IsString({ message: 'Street must be a string' }) @IsNotEmpty({ message: 'Street is required' }) @MaxLength(255, { message: 'Street must not exceed 255 characters' }) + @IsXssSafe({ message: 'Street address contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Street address contains potential SQL injection' }) street: string; @ApiProperty({ @@ -48,6 +52,8 @@ export class AddressDto { @IsString({ message: 'City must be a string' }) @IsNotEmpty({ message: 'City is required' }) @MaxLength(100, { message: 'City must not exceed 100 characters' }) + @IsXssSafe({ message: 'City name contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'City name contains potential SQL injection' }) city: string; @ApiPropertyOptional({ @@ -58,6 +64,8 @@ export class AddressDto { @IsOptional() @IsString({ message: 'State must be a string' }) @MaxLength(100, { message: 'State must not exceed 100 characters' }) + @IsXssSafe({ message: 'State/province contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'State/province contains potential SQL injection' }) state?: string; @ApiPropertyOptional({ @@ -68,6 +76,8 @@ export class AddressDto { @IsOptional() @IsString({ message: 'Postal code must be a string' }) @MaxLength(20, { message: 'Postal code must not exceed 20 characters' }) + @IsXssSafe({ message: 'Postal code contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Postal code contains potential SQL injection' }) postalCode?: string; @ApiProperty({ @@ -78,6 +88,8 @@ export class AddressDto { @IsString({ message: 'Country must be a string' }) @IsNotEmpty({ message: 'Country is required' }) @MaxLength(100, { message: 'Country must not exceed 100 characters' }) + @IsXssSafe({ message: 'Country name contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Country name contains potential SQL injection' }) country: string; } @@ -90,6 +102,8 @@ export class CreatePropertyDto { @IsString({ message: 'Title must be a string' }) @IsNotEmpty({ message: 'Title is required' }) @MaxLength(200, { message: 'Title must not exceed 200 characters' }) + @IsXssSafe({ message: 'Property title contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Property title contains potential SQL injection' }) title: string; @ApiPropertyOptional({ @@ -100,6 +114,8 @@ export class CreatePropertyDto { @IsOptional() @IsString({ message: 'Description must be a string' }) @MaxLength(5000, { message: 'Description must not exceed 5000 characters' }) + @IsXssSafe({ message: 'Property description contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Property description contains potential SQL injection' }) description?: string; @ApiProperty({ @@ -132,6 +148,8 @@ export class CreatePropertyDto { @IsString({ each: true, message: 'Each feature must be a string' }) @ArrayMaxSize(50, { message: 'Cannot have more than 50 features' }) @MaxLength(100, { each: true, message: 'Each feature must not exceed 100 characters' }) + @IsXssSafe({ each: true, message: 'Feature contains potentially malicious content' }) + @IsNotSqlInjection({ each: true, message: 'Feature contains potential SQL injection' }) features?: string[]; @ApiPropertyOptional({ diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index c8752e0e..0c4a6158 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -2,6 +2,8 @@ import { IsEmail, IsString, IsOptional, MinLength, IsNotEmpty, MaxLength, Matche import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsEthereumAddress } from '../../common/validators/is-ethereum-address.validator'; import { IsStrongPassword } from '../../common/validators/is-strong-password.validator'; +import { IsXssSafe } from '../../common/validators/xss.validator'; +import { IsNotSqlInjection } from '../../common/validators/sql-injection.validator'; export class CreateUserDto { @ApiProperty({ @@ -12,6 +14,8 @@ export class CreateUserDto { @IsEmail({}, { message: 'Please provide a valid email address' }) @IsNotEmpty({ message: 'Email is required' }) @MaxLength(255, { message: 'Email must not exceed 255 characters' }) + @IsXssSafe({ message: 'Email contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Email contains potential SQL injection' }) email: string; @ApiProperty({ @@ -27,6 +31,8 @@ export class CreateUserDto { @Matches(/^[a-zA-Z\s'-]+$/, { message: 'First name can only contain letters, spaces, hyphens, and apostrophes', }) + @IsXssSafe({ message: 'First name contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'First name contains potential SQL injection' }) firstName: string; @ApiProperty({ @@ -42,6 +48,8 @@ export class CreateUserDto { @Matches(/^[a-zA-Z\s'-]+$/, { message: 'Last name can only contain letters, spaces, hyphens, and apostrophes', }) + @IsXssSafe({ message: 'Last name contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Last name contains potential SQL injection' }) lastName: string; @ApiProperty({ @@ -54,6 +62,8 @@ export class CreateUserDto { @IsNotEmpty({ message: 'Password is required' }) @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(128, { message: 'Password must not exceed 128 characters' }) + @IsXssSafe({ message: 'Password contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Password contains potential SQL injection' }) @IsStrongPassword() password: string; @@ -63,5 +73,7 @@ export class CreateUserDto { }) @IsOptional() @IsEthereumAddress({ message: 'Invalid Ethereum wallet address format' }) + @IsXssSafe({ message: 'Wallet address contains potentially malicious content' }) + @IsNotSqlInjection({ message: 'Wallet address contains potential SQL injection' }) walletAddress?: string; }