diff --git a/nest/src/dtos/compile.dto.ts b/nest/src/dtos/compile.dto.ts index cff1e35..77013c8 100644 --- a/nest/src/dtos/compile.dto.ts +++ b/nest/src/dtos/compile.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; +import { IsSafePath } from 'src/validators/safe-path.decorator'; export class CompileRustDto { @ApiProperty({ @@ -8,5 +9,6 @@ export class CompileRustDto { }) @IsNotEmpty() @IsString() + @IsSafePath() entryPoint: string; } diff --git a/nest/src/dtos/github.dto.ts b/nest/src/dtos/github.dto.ts index e118764..14d1336 100644 --- a/nest/src/dtos/github.dto.ts +++ b/nest/src/dtos/github.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; +import { IsSafeString } from 'src/validators/safe-string.decorator'; +import { IsSafeUrl } from 'src/validators/safe-url.decorator'; export class GithubDto { @ApiProperty({ @@ -8,6 +10,7 @@ export class GithubDto { }) @IsNotEmpty() @IsString() + @IsSafeUrl() repo: string; @ApiProperty({ @@ -16,6 +19,7 @@ export class GithubDto { }) @IsNotEmpty() @IsString() + @IsSafeString() sha: string; } diff --git a/nest/src/dtos/verify.dto.ts b/nest/src/dtos/verify.dto.ts index c06f32f..09fc618 100644 --- a/nest/src/dtos/verify.dto.ts +++ b/nest/src/dtos/verify.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; +import { IsSafePath } from 'src/validators/safe-path.decorator'; export class VerifyRustDto { @ApiProperty({ @@ -8,6 +9,7 @@ export class VerifyRustDto { }) @IsNotEmpty() @IsString() + @IsSafePath() entryPoint: string; @ApiProperty({ diff --git a/nest/src/main.ts b/nest/src/main.ts index 0ba0581..b022b65 100644 --- a/nest/src/main.ts +++ b/nest/src/main.ts @@ -38,6 +38,7 @@ async function bootstrap() { forbidNonWhitelisted: true, // Throw errors if non-whitelisted values are provided transform: true, // Automatically transform payloads to be objects typed according to their DTO classes disableErrorMessages: false, // Optionally set this to true in production mode + validateCustomDecorators: true, // Enable usage of custom decorators }), ); diff --git a/nest/src/validators/safe-path.decorator.ts b/nest/src/validators/safe-path.decorator.ts new file mode 100644 index 0000000..36f2945 --- /dev/null +++ b/nest/src/validators/safe-path.decorator.ts @@ -0,0 +1,33 @@ +import { + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator, +} from 'class-validator'; + +@ValidatorConstraint({ async: false }) +class IsSafePathConstraint implements ValidatorConstraintInterface { + validate(path: string, args: ValidationArguments) { + if (typeof path !== 'string') return false; + + const isSuspicious = /[;&|`$<>]/.test(path); + return !isSuspicious; + } + + defaultMessage(args: ValidationArguments) { + return `The path "${args.value}" contains invalid characters. Only safe paths are allowed.`; + } +} + +export function IsSafePath(validationOptions?: ValidationOptions) { + return function (object: any, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsSafePathConstraint, + }); + }; +} diff --git a/nest/src/validators/safe-string.decorator.ts b/nest/src/validators/safe-string.decorator.ts new file mode 100644 index 0000000..714d8d6 --- /dev/null +++ b/nest/src/validators/safe-string.decorator.ts @@ -0,0 +1,32 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: false }) +export class IsSafeStringConstraint implements ValidatorConstraintInterface { + validate(text: string) { + if (typeof text !== 'string') return false; + + const sanitizedText = text.replace(/[^a-zA-Z0-9_.-]/g, ''); + return sanitizedText === text; + } + + defaultMessage() { + return 'Input contains invalid characters'; + } +} + +export function IsSafeString(validationOptions?: ValidationOptions) { + return function (object: any, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsSafeStringConstraint, + }); + }; +} diff --git a/nest/src/validators/safe-url.decorator.ts b/nest/src/validators/safe-url.decorator.ts new file mode 100644 index 0000000..38c9169 --- /dev/null +++ b/nest/src/validators/safe-url.decorator.ts @@ -0,0 +1,38 @@ +import { + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator, +} from 'class-validator'; + +@ValidatorConstraint({ async: false }) +export class IsSafeUrlConstraint implements ValidatorConstraintInterface { + validate(url: string, args: ValidationArguments) { + if (typeof url !== 'string') return false; + + // Define the pattern for a safe URL here. This is a simplistic approach; + // consider using more sophisticated validation depending on your requirements. + const unsafePatterns = /(;|&|\||`|\$)/; + + // URL is considered safe if it doesn't match unsafe patterns + return !unsafePatterns.test(url); + } + + defaultMessage(args: ValidationArguments) { + return 'The URL contains unsafe characters that could lead to command line injection.'; + } +} + +export function IsSafeUrl(validationOptions?: ValidationOptions) { + return function (object: any, propertyName: string) { + registerDecorator({ + name: 'isSafeUrl', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsSafeUrlConstraint, + }); + }; +}