improve security against injection (#2669)

* improve security against injection

* fix shell injection through ipv4, ipv6 address

Co-authored-by: 7megaumka7 <116706490+7megaumka7@users.noreply.github.com>

---------

Co-authored-by: 7megaumka7 <116706490+7megaumka7@users.noreply.github.com>
This commit is contained in:
Bernd Storath
2026-06-16 16:00:25 +02:00
committed by GitHub
co-authored by 7megaumka7
parent 5f54fa3e58
commit 032347648d
6 changed files with 54 additions and 16 deletions
+3 -1
View File
@@ -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",
+1 -5
View File
@@ -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(
@@ -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<typeof client>;
@@ -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'),
@@ -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({
@@ -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,
});
+34 -4
View File
@@ -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<T>(
schema: ZodSchema<T>,
schema: ZodType<T>,
event: H3Event<EventHandlerRequest>
) {
return async (data: unknown) => {
@@ -203,6 +217,22 @@ export function validateZod<T>(
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', [