diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts index afe499c6a..c02a4d886 100644 --- a/apps/api-gateway/src/connection/dtos/connection.dto.ts +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -3,6 +3,7 @@ import { ArrayNotEmpty, IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, Is import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { HandshakeProtocol } from '../enums/connections.enum'; +import { IsNotSQLInjection } from '@credebl/common/cast.helper'; export class CreateOutOfBandConnectionInvitation { @ApiPropertyOptional() @@ -61,12 +62,14 @@ export class CreateConnectionDto { @IsOptional() @IsString({ message: 'alias must be a string' }) @IsNotEmpty({ message: 'please provide valid alias' }) + @IsNotSQLInjection({ message: 'alias is required.' }) alias: string; @ApiPropertyOptional() @IsOptional() @IsString({ message: 'label must be a string' }) @IsNotEmpty({ message: 'please provide valid label' }) + @IsNotSQLInjection({ message: 'label is required.' }) label: string; @ApiPropertyOptional() diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index 380b797a3..cc7aca6ba 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -2,7 +2,7 @@ import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, Val import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; class AttributeValue { @@ -41,6 +41,7 @@ export class CreateSchemaDto { @IsString({ message: 'schemaName must be a string' }) @Transform(({ value }) => trim(value)) @IsNotEmpty({ message: 'schemaName is required' }) + @IsNotSQLInjection({ message: 'SchemaName is required.' }) schemaName: string; @ApiProperty({ diff --git a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts index 5b5e37fbc..f2be132ff 100644 --- a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts @@ -2,7 +2,7 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagge import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; import { Transform, Type } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; @ApiExtraModels() export class CreateEcosystemDto { @@ -13,6 +13,7 @@ export class CreateEcosystemDto { @MinLength(2, { message: 'Ecosystem name must be at least 2 characters.' }) @MaxLength(50, { message: 'Ecosystem name must be at most 50 characters.' }) @IsString({ message: 'Ecosystem name must be in string format.' }) + @IsNotSQLInjection({ message: 'Ecosystem name is required.' }) name: string; @ApiProperty() diff --git a/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts index 2c09343b6..a7011ee31 100644 --- a/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts @@ -2,7 +2,7 @@ import { ApiExtraModels, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; @ApiExtraModels() export class EditEcosystemDto { @@ -13,6 +13,7 @@ export class EditEcosystemDto { @MinLength(2, { message: 'Ecosystem name must be at least 2 characters.' }) @MaxLength(50, { message: 'Ecosystem name must be at most 50 characters.' }) @IsString({ message: 'Ecosystem name must be in string format.' }) + @IsNotSQLInjection({ message: 'Ecosystem name is required.' }) name?: string; @ApiPropertyOptional() diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index f8507b270..2daae2780 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -30,6 +30,18 @@ async function bootstrap(): Promise { app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb' })); + app.use(function (req, res, next) { + let err = null; + try { + decodeURIComponent(req.path); + } catch (e) { + err = e; + } + if (err) { + return res.status(500).json({ message: 'Invalid URL' }); + } + next(); + }); const options = new DocumentBuilder() .setTitle(`${process.env.PLATFORM_NAME}`) diff --git a/apps/api-gateway/src/organization/dtos/create-organization-dto.ts b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts index 61e2c0f73..8bc30fbe9 100644 --- a/apps/api-gateway/src/organization/dtos/create-organization-dto.ts +++ b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts @@ -2,7 +2,7 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagge import { IsNotEmpty, IsOptional, IsString, IsUrl, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; @ApiExtraModels() export class CreateOrganizationDto { @@ -13,6 +13,7 @@ export class CreateOrganizationDto { @MinLength(2, { message: 'Organization name must be at least 2 characters.' }) @MaxLength(50, { message: 'Organization name must be at most 50 characters.' }) @IsString({ message: 'Organization name must be in string format.' }) + @IsNotSQLInjection({ message: 'Organization name is required.' }) name: string; @ApiPropertyOptional() diff --git a/apps/api-gateway/src/organization/dtos/update-organization-dto.ts b/apps/api-gateway/src/organization/dtos/update-organization-dto.ts index 0ac4d194e..ed2029c4f 100644 --- a/apps/api-gateway/src/organization/dtos/update-organization-dto.ts +++ b/apps/api-gateway/src/organization/dtos/update-organization-dto.ts @@ -1,8 +1,8 @@ import { ApiExtraModels, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, IsBoolean, MaxLength, MinLength } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsBoolean, MaxLength, MinLength, Validate } from 'class-validator'; import { Transform } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { ImageBase64Validator, trim } from '@credebl/common/cast.helper'; @ApiExtraModels() export class UpdateOrganizationDto { @@ -31,7 +31,7 @@ export class UpdateOrganizationDto { @ApiPropertyOptional() @IsOptional() @Transform(({ value }) => trim(value)) - @IsString({ message: 'logo must be in string format.' }) + @Validate(ImageBase64Validator) logo?: string = ''; @ApiPropertyOptional() diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 51bb113a0..7e5c425c4 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -1,6 +1,6 @@ import { ApiBearerAuth, ApiExcludeEndpoint, ApiForbiddenResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { CommonService } from '@credebl/common'; -import { Controller, Get, Put, Param, UseGuards, UseFilters, Post, Body, Res, HttpStatus, Query, Delete, ParseUUIDPipe, BadRequestException } from '@nestjs/common'; +import { Controller, Get, Put, Param, UseGuards, UseFilters, Post, Body, Res, HttpStatus, Query, Delete, ParseUUIDPipe, BadRequestException, ValidationPipe, UsePipes } from '@nestjs/common'; import { OrganizationService } from './organization.service'; import { CreateOrganizationDto } from './dtos/create-organization-dto'; import IResponse from '@credebl/common/interfaces/response.interface'; @@ -472,8 +472,12 @@ export class OrganizationController { @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiParam({ + name: 'orgId' + }) @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) - async updateOrganization(@Body() updateOrgDto: UpdateOrganizationDto, @Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise { + @UsePipes(new ValidationPipe()) + async updateOrganization(@Body() updateOrgDto: UpdateOrganizationDto, @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(`Invalid format for orgId`); }})) orgId: string, @Res() res: Response, @User() reqUser: user): Promise { updateOrgDto.orgId = orgId; await this.organizationService.updateOrganization(updateOrgDto, reqUser.id, orgId); diff --git a/apps/api-gateway/src/verification/dto/get-all-proof-requests.dto.ts b/apps/api-gateway/src/verification/dto/get-all-proof-requests.dto.ts index 6c8a8920d..732c5f90a 100644 --- a/apps/api-gateway/src/verification/dto/get-all-proof-requests.dto.ts +++ b/apps/api-gateway/src/verification/dto/get-all-proof-requests.dto.ts @@ -1,25 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; +import { Transform } from 'class-transformer'; import { IsEnum, IsOptional } from 'class-validator'; import { SortValue } from '../../enum'; import { trim } from '@credebl/common/cast.helper'; import { SortFields } from '../enum/verification.enum'; +import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; -export class GetAllProofRequestsDto { - @ApiProperty({ required: false, example: '1' }) - @IsOptional() - pageNumber: number = 1; - - @ApiProperty({ required: false }) - @IsOptional() - @Transform(({ value }) => trim(value)) - @Type(() => String) - searchByText: string = ''; - - @ApiProperty({ required: false, example: '10' }) - @IsOptional() - pageSize: number = 10; - +export class GetAllProofRequestsDto extends PaginationDto { @ApiProperty({ enum: [SortValue.DESC, SortValue.ASC], required: false diff --git a/apps/api-gateway/src/verification/dto/request-proof.dto.ts b/apps/api-gateway/src/verification/dto/request-proof.dto.ts index e74db5161..e51f699f3 100644 --- a/apps/api-gateway/src/verification/dto/request-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -1,5 +1,5 @@ import { ArrayNotEmpty, IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsObject, IsOptional, IsString, ValidateIf, ValidateNested, IsUUID, ArrayUnique, ArrayMaxSize } from 'class-validator'; -import { toLowerCase, trim } from '@credebl/common/cast.helper'; +import { trim } from '@credebl/common/cast.helper'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { AutoAccept } from '@credebl/enum/enum'; @@ -72,7 +72,7 @@ export class RequestProofDto extends ProofPayload { @ApiProperty() @IsString() @Transform(({ value }) => trim(value)) - @Transform(({ value }) => toLowerCase(value)) + @IsUUID() @IsNotEmpty({ message: 'connectionId is required.' }) connectionId: string; diff --git a/apps/api-gateway/src/verification/interfaces/verification.interface.ts b/apps/api-gateway/src/verification/interfaces/verification.interface.ts index a722da72a..cac18fa67 100644 --- a/apps/api-gateway/src/verification/interfaces/verification.interface.ts +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -13,7 +13,7 @@ export interface IProofRequestSearchCriteria { pageSize: number; sortField: string; sortBy: string; - searchByText: string; + search: string; user?: IUserRequestInterface } diff --git a/apps/api-gateway/src/verification/verification.controller.ts b/apps/api-gateway/src/verification/verification.controller.ts index bed9126f0..f1abe9179 100644 --- a/apps/api-gateway/src/verification/verification.controller.ts +++ b/apps/api-gateway/src/verification/verification.controller.ts @@ -137,10 +137,10 @@ export class VerificationController { @User() user: IUserRequest, @Param('orgId') orgId: string ): Promise { - const { pageSize, searchByText, pageNumber, sortField, sortBy } = getAllProofRequests; + const { pageSize, search, pageNumber, sortField, sortBy } = getAllProofRequests; const proofRequestsSearchCriteria: IProofRequestSearchCriteria = { pageNumber, - searchByText, + search, pageSize, sortField, sortBy diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index b9172233e..a1a470019 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -217,7 +217,7 @@ export interface IProofRequestSearchCriteria { pageSize: number; sortField: string; sortBy: string; - searchByText: string; + search: string; } export interface IInvitation{ diff --git a/apps/verification/src/repositories/verification.repository.ts b/apps/verification/src/repositories/verification.repository.ts index b64b24145..f6b6fd21d 100644 --- a/apps/verification/src/repositories/verification.repository.ts +++ b/apps/verification/src/repositories/verification.repository.ts @@ -65,9 +65,9 @@ export class VerificationRepository { where: { orgId, OR: [ - { connectionId: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } }, - { state: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } }, - { presentationId: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } } + { connectionId: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } }, + { state: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } }, + { presentationId: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } } ] }, select: { @@ -90,9 +90,9 @@ export class VerificationRepository { where: { orgId, OR: [ - { connectionId: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } }, - { state: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } }, - { presentationId: { contains: proofRequestsSearchCriteria.searchByText, mode: 'insensitive' } } + { connectionId: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } }, + { state: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } }, + { presentationId: { contains: proofRequestsSearchCriteria.search, mode: 'insensitive' } } ] } }); diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index d92cb2a59..8b8cf7f68 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -1,3 +1,6 @@ +import { BadRequestException } from '@nestjs/common'; +import { ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, isBase64, isMimeType, registerDecorator } from 'class-validator'; + interface ToNumberOptions { default?: number; min?: number; @@ -55,4 +58,89 @@ export function ledgerName(value: string): string { return network; -} \ No newline at end of file +} + +export function isSafeString(value: string): boolean { + // Define a regular expression to allow alphanumeric characters, spaces, and some special characters + const safeRegex = /^[a-zA-Z0-9\s!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*$/; + + // Check if the value matches the safe regex + return safeRegex.test(value); + } + + export const IsNotSQLInjection = (validationOptions?: ValidationOptions): PropertyDecorator => (object: object, propertyName: string) => { + registerDecorator({ + name: 'isNotSQLInjection', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value) { + // Check if the value contains any common SQL injection keywords + const sqlKeywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'UNION', 'WHERE', 'AND', 'OR']; + for (const keyword of sqlKeywords) { + if (value.includes(keyword)) { + return false; // Value contains a SQL injection keyword + } + } + return true; // Value does not contain any SQL injection keywords + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} contains SQL injection keywords.`; + } + } + }); + }; + +@ValidatorConstraint({ name: 'customText', async: false }) +export class ImageBase64Validator implements ValidatorConstraintInterface { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars + validate(value: string, args: ValidationArguments) { + // Implement your custom validation logic here + // Validation to allow option param logo + if ('' == value) { + return true; + } + if (!value || 'string' !== typeof value) { + throw new BadRequestException('Invalid base64 string'); + } + const parts = value.split(','); + if (2 !== parts.length) { + throw new BadRequestException('Invalid data URI'); + } + // eslint-disable-next-line prefer-destructuring + const mimeType = parts[0].split(';')[0].split(':')[1]; + // eslint-disable-next-line prefer-destructuring + const base64Data = parts[1]; + + // Validate MIME type + if (!isMimeType(mimeType)) { + throw new BadRequestException('Please provide valid MIME type'); + } + // Validate base64 data + if (!isBase64(base64Data) || '' == base64Data || null == base64Data) { + throw new BadRequestException('Invalid base64 string'); + } + return true; + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars + defaultMessage(_args: ValidationArguments) { + return 'Default message received from [ImageBase64Validator]'; + } +} + +// IS NOT UUID validation +// export const IsNotUUID = (validationOptions?: ValidationOptions): PropertyDecorator => (object: object, propertyName: string) => { +// registerDecorator({ +// name: 'isNotUUID', +// target: object.constructor, +// propertyName, +// options: validationOptions, +// validator: { +// validate(value) { +// return !isUUID(value); +// } +// } +// }); +// }; \ No newline at end of file diff --git a/libs/common/src/dtos/pagination.dto.ts b/libs/common/src/dtos/pagination.dto.ts index 17bf21fad..ce47de8c8 100644 --- a/libs/common/src/dtos/pagination.dto.ts +++ b/libs/common/src/dtos/pagination.dto.ts @@ -1,12 +1,12 @@ -import { Transform, Type } from 'class-transformer'; -import { toNumber } from '@credebl/common/cast.helper'; +import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, Min } from 'class-validator'; +import { IsNumber, IsOptional, Max, Min } from 'class-validator'; export class PaginationDto { + @Type(() => Number) @ApiProperty({ required: false, default: 1 }) @IsOptional() - @Transform(({ value }) => toNumber(value)) + @IsNumber() @Min(1, { message: 'Page number must be greater than 0' }) pageNumber = 1; @@ -15,10 +15,12 @@ export class PaginationDto { @Type(() => String) search = ''; + @Type(() => Number) @ApiProperty({ required: false, default: 10 }) @IsOptional() - @Transform(({ value }) => toNumber(value)) + @IsNumber() @Min(1, { message: 'Page size must be greater than 0' }) + @Max(100, { message: 'Page size must be less than 100' }) pageSize = 10; -} +} \ No newline at end of file