From 681081896255377dab15aa4c6ae89359ee4a7757 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 25 Jan 2022 13:21:05 +0100 Subject: [PATCH 1/2] CVE-2019-18413. Patch for potential SQL injections --- package-lock.json | 69 ++++++++++--------- package.json | 7 +- .../user-management/auth.controller.ts | 3 +- .../chirpstack-paginated-list.dto.ts | 4 ++ src/entities/dto/create-device-model.dto.ts | 4 +- src/entities/dto/list-all-entities.dto.ts | 12 +++- ...st-all-iot-devices-minimal-response.dto.ts | 7 +- src/entities/dto/login.dto.ts | 3 + src/entities/dto/receive-data.dto.ts | 13 +++- .../create-sigfox-group-request.dto.ts | 4 ++ .../internal/sigfox-get-all-request.dto.ts | 3 + .../dto/sigfox/sigfox-callback.dto.ts | 14 ++++ src/entities/dto/test-payload-decoder.dto.ts | 5 ++ .../create-organization.dto.ts | 3 + src/helpers/string-to-number-validator.ts | 12 ++++ src/loaders/nestjs.ts | 4 ++ 16 files changed, 122 insertions(+), 45 deletions(-) create mode 100644 src/helpers/string-to-number-validator.ts diff --git a/package-lock.json b/package-lock.json index 98f7b797..6a1b55be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1715,9 +1715,9 @@ } }, "@nestjs/mapped-types": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.1.1.tgz", - "integrity": "sha512-FROYmmZ2F+tLJP/aHasPMX40iUHQPtEAzOAcfAp21baebN5iLUrdyTuphoXjIqubfPFSwtnAGpVm9kLJjQ//ig==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.4.1.tgz", + "integrity": "sha512-JXrw2LMangSU3vnaXWXVX47GRG1FbbNh4aVBbidDjxT3zlghsoNQY6qyWtT001MCl8lJGo8I6i6+DurBRRxl/Q==" }, "@nestjs/passport": { "version": "7.1.5", @@ -1798,13 +1798,20 @@ } }, "@nestjs/swagger": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-4.7.6.tgz", - "integrity": "sha512-9cj/rAqqnUxHnGqgO9GE1HU5x0BxQmWgWgenLh/cwdW4dFys7JI3xN2RWaEtZCVobvKYUWBPNvXnoduYI/1OKg==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-4.8.2.tgz", + "integrity": "sha512-RSUwcVxrzXF7/b/IZ5lXnYHJ6jIGS9wWRTJKIt1kIaCNWT+0wRfTlAyhQkzs2g35/PTXJEcdIwwY7mBO/bwHzw==", "requires": { - "@nestjs/mapped-types": "0.1.1", - "lodash": "4.17.20", + "@nestjs/mapped-types": "0.4.1", + "lodash": "4.17.21", "path-to-regexp": "3.2.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } } }, "@nestjs/testing": { @@ -2387,9 +2394,10 @@ "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, "@types/validator": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.0.0.tgz", - "integrity": "sha512-WAy5txG7aFX8Vw3sloEKp5p/t/Xt8jD3GRD9DacnFv6Vo8ubudAsRTXgxpQwU0mpzY/H8U4db3roDuCMjShBmw==" + "version": "13.7.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.1.tgz", + "integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==", + "dev": true }, "@types/webpack": { "version": "4.41.25", @@ -3547,9 +3555,9 @@ "dev": true }, "class-transformer": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.3.1.tgz", - "integrity": "sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "class-utils": { "version": "0.3.6", @@ -3575,14 +3583,12 @@ } }, "class-validator": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.12.2.tgz", - "integrity": "sha512-TDzPzp8BmpsbPhQpccB3jMUE/3pK0TyqamrK0kcx+ZeFytMA+O6q87JZZGObHHnoo9GM8vl/JppIyKWeEA/EVw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz", + "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==", "requires": { - "@types/validator": "13.0.0", - "google-libphonenumber": "^3.2.8", - "tslib": ">=1.9.0", - "validator": "13.0.0" + "libphonenumber-js": "^1.9.43", + "validator": "^13.7.0" } }, "cli-cursor": { @@ -5619,11 +5625,6 @@ } } }, - "google-libphonenumber": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.10.tgz", - "integrity": "sha512-TsckE9O8QgqaIeaOXPjcJa4/kX3BzFdO1oCbMfmUpRZckml4xJhjJVxaT9Mdt/VrZZkT9lX44eHAEWfJK1tHtw==" - }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -9230,6 +9231,11 @@ "type-check": "~0.4.0" } }, + "libphonenumber-js": { + "version": "1.9.44", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.44.tgz", + "integrity": "sha512-zhw8nUMJuQf7jG1dZfEOKKOS6M3QYIv3HnvB/vGohNd0QfxIQcObH3a6Y6s350H+9xgBeOXClOJkS0hJ0yvS3g==" + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -12332,11 +12338,6 @@ "tsconfig-paths": "^3.4.0" } }, - "tslib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", - "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" - }, "tsutils": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", @@ -12652,9 +12653,9 @@ } }, "validator": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.0.0.tgz", - "integrity": "sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==" + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" }, "vary": { "version": "1.1.2", diff --git a/package.json b/package.json index 1fe4233f..d7152e85 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@nestjs/passport": "^7.1.5", "@nestjs/platform-express": "^7.6.1", "@nestjs/schedule": "^0.4.1", - "@nestjs/swagger": "^4.7.6", + "@nestjs/swagger": "^4.8.2", "@nestjs/typeorm": "^7.1.5", "@types/bcryptjs": "^2.4.2", "@types/geojson": "^7946.0.7", @@ -48,8 +48,8 @@ "axios-cache-adapter": "^2.5.0", "bcryptjs": "^2.4.3", "bluebird": "^3.7.2", - "class-transformer": "^0.3.1", - "class-validator": "^0.12.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.13.2", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "kafkajs": "^1.15.0", @@ -85,6 +85,7 @@ "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.10", + "@types/validator": "^13.7.1", "@typescript-eslint/eslint-plugin": "^4.10.0", "@typescript-eslint/parser": "^4.10.0", "eslint": "^7.15.0", diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index ed326138..b8c7c28a 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -142,8 +142,7 @@ export class AuthController { @UseGuards(LocalAuthGuard) async login( @Request() req: AuthenticatedRequestLocalStrategy, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @Body() body: LoginDto + @Body() _: LoginDto ): Promise { const { email, id } = req.user; return this.authService.issueJwt(email, id, false); diff --git a/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts b/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts index d16e5e57..8752d23a 100644 --- a/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts @@ -1,10 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; export class ChirpstackPaginatedListDto { @ApiProperty({ type: Number, required: false }) + @IsOptional() limit? = 100; @ApiProperty({ type: Number, required: false }) + @IsOptional() offset? = 0; @ApiProperty({ type: Number, required: false }) + @IsOptional() organizationId?: number; } diff --git a/src/entities/dto/create-device-model.dto.ts b/src/entities/dto/create-device-model.dto.ts index c55c5c70..64ed0da1 100644 --- a/src/entities/dto/create-device-model.dto.ts +++ b/src/entities/dto/create-device-model.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNumber } from "class-validator"; +import { IsDefined, IsNumber } from "class-validator"; export class CreateDeviceModelDto { @ApiProperty({ required: true }) @@ -7,5 +7,7 @@ export class CreateDeviceModelDto { belongsToId: number; @ApiProperty({ required: true }) + // @IsJSON or @IsString does not work. Will be validated during the flow + @IsDefined() body: JSON; } diff --git a/src/entities/dto/list-all-entities.dto.ts b/src/entities/dto/list-all-entities.dto.ts index 677af0cb..a5c7fdfa 100644 --- a/src/entities/dto/list-all-entities.dto.ts +++ b/src/entities/dto/list-all-entities.dto.ts @@ -1,13 +1,23 @@ +import { StringToNumber } from "@helpers/string-to-number-validator"; import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; export class ListAllEntitiesDto { @ApiProperty({ type: Number, required: false }) + @IsOptional() + @StringToNumber() limit? = 100; @ApiProperty({ type: Number, required: false }) + @IsOptional() + @StringToNumber() offset? = 0; @ApiProperty({ type: String, required: false }) + @IsOptional() + @IsString() sort?: "ASC" | "DESC"; @ApiProperty({ type: String, required: false }) + @IsOptional() + @IsString() orderOn?: | "id" | "name" @@ -17,5 +27,5 @@ export class ListAllEntitiesDto { | "type" | "organisations" | "active" - | "groupName" + | "groupName"; } diff --git a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts index 67e70b4b..d3720f2f 100644 --- a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts +++ b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; export class ListAllIoTDevicesMinimalResponseDto { @ApiProperty() @@ -35,8 +36,10 @@ export class IoTDeviceMinimalRaw { export class PayloadDecoderIoDeviceMinimalQuery { @ApiPropertyOptional() - limit?: number = 20; + @IsOptional() + limit? = 20; @ApiPropertyOptional() - offset?: number = 0; + @IsOptional() + offset? = 0; } diff --git a/src/entities/dto/login.dto.ts b/src/entities/dto/login.dto.ts index b24669dd..eaae5a64 100644 --- a/src/entities/dto/login.dto.ts +++ b/src/entities/dto/login.dto.ts @@ -1,8 +1,11 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; export class LoginDto { @ApiProperty({ default: "john@localhost.dk" }) + @IsString() username: string; @ApiProperty({ default: "hunter2" }) + @IsString() password: string; } diff --git a/src/entities/dto/receive-data.dto.ts b/src/entities/dto/receive-data.dto.ts index 74c19872..55ec935a 100644 --- a/src/entities/dto/receive-data.dto.ts +++ b/src/entities/dto/receive-data.dto.ts @@ -1,6 +1,15 @@ +import { Exclude } from "class-transformer"; +import { IsOptional } from "class-validator"; + /** * This only exists to nudge Swagger to make an JSON body for us to post. * - * Intentionally left blank. + * Validation won't work for empty objects and we can't disable it, seemingly. + * + * @see https://github.com/typestack/class-validator/issues/1503 */ -export class ReceiveDataDto {} +export class ReceiveDataDto { + @Exclude() + @IsOptional() + ignoreMe: unknown; +} diff --git a/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts b/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts index 0f533576..9060f24c 100644 --- a/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts +++ b/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts @@ -1,12 +1,16 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber, IsString } from "class-validator"; export class CreateSigFoxGroupRequestDto { @ApiProperty({ required: true }) + @IsNumber() organizationId: number; @ApiProperty({ required: true }) + @IsString() username: string; @ApiProperty({ required: true }) + @IsString() password: string; } diff --git a/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts b/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts index def69568..7bd0dbe8 100644 --- a/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts +++ b/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts @@ -1,3 +1,6 @@ +import { StringToNumber } from "@helpers/string-to-number-validator"; + export class SigFoxGetAllRequestDto { + @StringToNumber() organizationId: number; } diff --git a/src/entities/dto/sigfox/sigfox-callback.dto.ts b/src/entities/dto/sigfox/sigfox-callback.dto.ts index 6ff544d9..23188ff0 100644 --- a/src/entities/dto/sigfox/sigfox-callback.dto.ts +++ b/src/entities/dto/sigfox/sigfox-callback.dto.ts @@ -1,23 +1,37 @@ +import { IsNumber, IsOptional, IsString } from "class-validator"; + /** * Callback as expected from SigFox * Docs: https://support.sigfox.com/docs/uplink */ export class SigFoxCallbackDto { + @IsNumber() time: number; + @IsString() deviceTypeId: string; + @IsString() deviceId: string; + @IsString() data: string; + @IsNumber() seqNumber: number; // If true, then the device expects a downlink ack: boolean; // Only included in BIDIR + @IsOptional() longPolling?: boolean; // these are not available for all contracts "Condition: for devices with contract option NETWORK METADATA" // https://support.sigfox.com/docs/bidir // We cannot assume they'll exists + @IsOptional() + @IsNumber() snr?: number; + @IsOptional() + @IsNumber() rssi?: number; + @IsOptional() + @IsString() station?: string; } diff --git a/src/entities/dto/test-payload-decoder.dto.ts b/src/entities/dto/test-payload-decoder.dto.ts index bc8a6873..61e28ce3 100644 --- a/src/entities/dto/test-payload-decoder.dto.ts +++ b/src/entities/dto/test-payload-decoder.dto.ts @@ -1,5 +1,10 @@ +import { IsString } from "class-validator"; + export class TestPayloadDecoderDto { + @IsString() code: string; + @IsString() iotDeviceJsonString: string; + @IsString() rawPayloadJsonString: string; } diff --git a/src/entities/dto/user-management/create-organization.dto.ts b/src/entities/dto/user-management/create-organization.dto.ts index b40641aa..c1dd3c47 100644 --- a/src/entities/dto/user-management/create-organization.dto.ts +++ b/src/entities/dto/user-management/create-organization.dto.ts @@ -1,3 +1,6 @@ +import { IsString } from "class-validator"; + export class CreateOrganizationDto { + @IsString() name: string; } diff --git a/src/helpers/string-to-number-validator.ts b/src/helpers/string-to-number-validator.ts new file mode 100644 index 00000000..6ff41cbf --- /dev/null +++ b/src/helpers/string-to-number-validator.ts @@ -0,0 +1,12 @@ +import { Type } from "class-transformer"; +import { IsNumber } from "class-validator"; + +/** + * Checks if a value can be converted to a number + */ +export const StringToNumber = (): PropertyDecorator => { + return (target: unknown, propertyName: string): void => { + Type(() => Number)(target, propertyName); + IsNumber()(target, propertyName); + }; +}; diff --git a/src/loaders/nestjs.ts b/src/loaders/nestjs.ts index 243cb7b0..c32500b2 100644 --- a/src/loaders/nestjs.ts +++ b/src/loaders/nestjs.ts @@ -28,8 +28,12 @@ export async function setupNestJs( app.useGlobalPipes( new ValidationPipe({ exceptionFactory: errors => { + // Throw exception if any controller validation fails. Will also fail if a property has a type + // but doesn't have the proper decorator (like @IsNumber() for a number property) return new BadRequestException(errors); }, + // Fix CVE-2019-18413. Issue: https://github.com/typestack/class-validator/issues/438 + forbidUnknownValues: true, }) ); app.enableCors(); From 24300b600bb2e1c4a8a90779a56c8873ae105f00 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 27 Jan 2022 15:51:57 +0100 Subject: [PATCH 2/2] Fix request 400 on get applications by permission --- .../list-all-iot-devices-minimal-response.dto.ts | 8 +++----- src/entities/dto/list-all-paginated.dto.ts | 6 +++--- src/helpers/optional-validator.ts | 14 ++++++++++++++ src/helpers/string-to-number-validator.ts | 8 +++++--- 4 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/helpers/optional-validator.ts diff --git a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts index d3720f2f..ce01c041 100644 --- a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts +++ b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; export class ListAllIoTDevicesMinimalResponseDto { @ApiProperty() @@ -35,11 +35,9 @@ export class IoTDeviceMinimalRaw { } export class PayloadDecoderIoDeviceMinimalQuery { - @ApiPropertyOptional() - @IsOptional() + @IsSwaggerOptional() limit? = 20; - @ApiPropertyOptional() - @IsOptional() + @IsSwaggerOptional() offset? = 0; } diff --git a/src/entities/dto/list-all-paginated.dto.ts b/src/entities/dto/list-all-paginated.dto.ts index 24665158..cd22d6b3 100644 --- a/src/entities/dto/list-all-paginated.dto.ts +++ b/src/entities/dto/list-all-paginated.dto.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; export class ListAllPaginated { - @ApiProperty({ type: Number, required: false }) + @IsSwaggerOptional({ type: Number }) limit? = 100; - @ApiProperty({ type: Number, required: false }) + @IsSwaggerOptional({ type: Number }) offset? = 0; } diff --git a/src/helpers/optional-validator.ts b/src/helpers/optional-validator.ts new file mode 100644 index 00000000..fc8153e0 --- /dev/null +++ b/src/helpers/optional-validator.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional, ApiPropertyOptions } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; + +/** + * Sets a property as optional on the swagger and controller level + */ +export const IsSwaggerOptional = (swaggerOptions?: ApiPropertyOptions): PropertyDecorator => { + return (propertyValue: unknown, propertyName: string): void => { + // Set as optional in the swagger document + ApiPropertyOptional(swaggerOptions)(propertyValue, propertyName); + // If no value is passed, then ignore all validators + IsOptional()(propertyValue, propertyName); + }; +}; diff --git a/src/helpers/string-to-number-validator.ts b/src/helpers/string-to-number-validator.ts index 6ff41cbf..721e7298 100644 --- a/src/helpers/string-to-number-validator.ts +++ b/src/helpers/string-to-number-validator.ts @@ -5,8 +5,10 @@ import { IsNumber } from "class-validator"; * Checks if a value can be converted to a number */ export const StringToNumber = (): PropertyDecorator => { - return (target: unknown, propertyName: string): void => { - Type(() => Number)(target, propertyName); - IsNumber()(target, propertyName); + return (propertyValue: unknown, propertyName: string): void => { + // Cast the value to a number + Type(() => Number)(propertyValue, propertyName); + // Validate whether the value is a number + IsNumber()(propertyValue, propertyName); }; };