diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6a863f7d..a8f3da02 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -204,7 +204,9 @@ "validBoolean": "{0} must be a valid boolean", "validArray": "{0} must be a valid array", "stringMin": "{0} must be at least {1} Character", - "numberMin": "{0} must be at least {1}" + "stringMax": "{0} must be at most {1} Character", + "numberMin": "{0} must be at least {1}", + "numberMax": "{0} must be at most {1}" }, "client": { "id": "Client ID", diff --git a/src/server/api/auth/verify-2fa.post.ts b/src/server/api/auth/verify-2fa.post.ts index 2fa19d86..51bbc718 100644 --- a/src/server/api/auth/verify-2fa.post.ts +++ b/src/server/api/auth/verify-2fa.post.ts @@ -1,8 +1,4 @@ -import { z } from 'zod'; - -const Verify2faSchema = z.object({ - totpCode: z.string().min(6).max(6), -}); +import { Verify2faSchema } from '#db/repositories/user/types'; export default defineEventHandler(async (event) => { const { totpCode } = await readValidatedBody( diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index b0aa7fa3..cffd694d 100644 --- a/src/server/database/repositories/client/types.ts +++ b/src/server/database/repositories/client/types.ts @@ -1,6 +1,7 @@ import type { InferSelectModel } from 'drizzle-orm'; import z from 'zod'; +import { isIPv4, isIPv6 } from 'is-ip'; import type { client } from './schema'; export type ClientType = InferSelectModel; @@ -20,7 +21,8 @@ export type UpdateClientType = Omit< const name = z .string({ message: t('zod.client.name') }) .min(1, t('zod.client.name')) - .pipe(safeStringRefine); + .pipe(safeStringRefine) + .pipe(controlStringRefine); // TODO?: validate iso string const expiresAt = z @@ -32,14 +34,18 @@ const expiresAt = z const address4 = z .string({ message: t('zod.client.address4') }) .min(1, { message: t('zod.client.address4') }) - .pipe(safeStringRefine); + .pipe(safeStringRefine) + .pipe(controlStringRefine) + .refine((v) => isIPv4(v)); const address6 = z .string({ message: t('zod.client.address6') }) .min(1, { message: t('zod.client.address6') }) - .pipe(safeStringRefine); + .pipe(safeStringRefine) + .pipe(controlStringRefine) + .refine((v) => isIPv6(v)); -const filter = z.string().optional(); +const filter = z.string().pipe(safeStringRefine).optional(); const serverAllowedIps = z.array(AddressSchema, { message: t('zod.client.serverAllowedIps'), diff --git a/src/server/database/repositories/general/types.ts b/src/server/database/repositories/general/types.ts index e16bc4c6..3097f645 100644 --- a/src/server/database/repositories/general/types.ts +++ b/src/server/database/repositories/general/types.ts @@ -11,6 +11,7 @@ const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') }); const metricsPassword = z .string({ message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') }) + .pipe(safeStringRefine) .nullable(); export const GeneralUpdateSchema = z.object({ diff --git a/src/server/database/repositories/user/types.ts b/src/server/database/repositories/user/types.ts index 9a175a6a..043d560b 100644 --- a/src/server/database/repositories/user/types.ts +++ b/src/server/database/repositories/user/types.ts @@ -46,9 +46,8 @@ const name = z .pipe(safeStringRefine); const email = z - .string({ message: t('zod.user.email') }) - .min(5, t('zod.user.email')) .email({ message: t('zod.user.emailInvalid') }) + .min(5, t('zod.user.email')) .pipe(safeStringRefine) .nullable(); @@ -80,3 +79,7 @@ export const UserUpdateTotpSchema = z.union([ currentPassword: password, }), ]); + +export const Verify2faSchema = z.object({ + totpCode: totpCode, +}); diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index 526b69b9..b18bf2f0 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -1,4 +1,4 @@ -import type { ZodSchema } from 'zod'; +import type { ZodType } from 'zod'; import z from 'zod'; import type { H3Event, EventHandlerRequest } from 'h3'; import { isIP } from 'is-ip'; @@ -20,6 +20,15 @@ export const safeStringRefine = z { message: t('zod.stringMalformed') } ); +function hasControlChars(str: string) { + // eslint-disable-next-line no-control-regex + return /[\x00-\x1F\x7F]/.test(str); +} + +export const controlStringRefine = z + .string() + .refine((v) => !hasControlChars(v), { message: t('zod.stringMalformed') }); + export const EnabledSchema = z.boolean({ message: t('zod.enabled') }); export const MtuSchema = z @@ -70,7 +79,11 @@ export const HSchema = z }) .nullable(); -export const ISchema = z.string().nullable(); +export const ISchema = z + .string() + .pipe(safeStringRefine) + .pipe(controlStringRefine) + .nullable(); export const PortSchema = z .number({ message: t('zod.port') }) @@ -85,7 +98,8 @@ export const PersistentKeepaliveSchema = z export const AddressSchema = z .string({ message: t('zod.address') }) .min(1, { message: t('zod.address') }) - .pipe(safeStringRefine); + .pipe(safeStringRefine) + .pipe(controlStringRefine); export const DnsSchema = z.array(AddressSchema, { message: t('zod.dns') }); @@ -168,7 +182,7 @@ export const schemaForType = }; export function validateZod( - schema: ZodSchema, + schema: ZodType, event: H3Event ) { return async (data: unknown) => { @@ -203,6 +217,22 @@ export function validateZod( break; } break; + case 'too_big': + switch (v.origin) { + case 'string': + newMessage = t('zod.generic.stringMax', [ + t(v.message), + v.maximum, + ]); + break; + case 'number': + newMessage = t('zod.generic.numberMax', [ + t(v.message), + v.maximum, + ]); + break; + } + break; case 'invalid_type': { if (v.input === null || v.input === undefined) { newMessage = t('zod.generic.required', [