From 080ee8a203a54a7e0f32a6f0fd08dce9120dec19 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Fri, 23 Jan 2026 21:09:27 -0800 Subject: [PATCH] validate firewall IPs --- src/app/pages/admin/interface.vue | 2 +- src/app/stores/global.ts | 3 +- src/i18n/locales/en.json | 3 +- .../database/repositories/client/types.ts | 2 +- src/server/utils/types.ts | 36 +++++++++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/app/pages/admin/interface.vue b/src/app/pages/admin/interface.vue index 91ddbfff..0df031df 100644 --- a/src/app/pages/admin/interface.vue +++ b/src/app/pages/admin/interface.vue @@ -185,7 +185,7 @@ const _submit = useSubmit( await revert(); if (success) { // Refresh global store information after successful save - await refreshNuxtData('/api/information'); + await globalStore.refreshInformation(); } }, } diff --git a/src/app/stores/global.ts b/src/app/stores/global.ts index aee45d54..f0a12de4 100644 --- a/src/app/stores/global.ts +++ b/src/app/stores/global.ts @@ -1,5 +1,5 @@ export const useGlobalStore = defineStore('Global', () => { - const { data: information } = useFetch('/api/information', { + const { data: information, refresh: refreshInformation } = useFetch('/api/information', { method: 'get', }); @@ -22,6 +22,7 @@ export const useGlobalStore = defineStore('Global', () => { return { sortClient, information, + refreshInformation, uiShowCharts, toggleCharts, uiChartType, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a5611456..13182c55 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -202,7 +202,8 @@ "address4": "IPv4 Address", "address6": "IPv6 Address", "serverAllowedIps": "Server Allowed IPs", - "firewallIps": "Firewall Allowed IPs" + "firewallIps": "Firewall Allowed IPs", + "firewallIpsInvalid": "Invalid firewall IP entry. Must be: IP (10.0.0.1), CIDR (10.0.0.0/24), IP:port (10.0.0.1:443), or IP:port/protocol (10.0.0.1:443/tcp)" }, "user": { "username": "Username", diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index 393ada7c..b0aa7fa3 100644 --- a/src/server/database/repositories/client/types.ts +++ b/src/server/database/repositories/client/types.ts @@ -71,7 +71,7 @@ export const ClientUpdateSchema = schemaForType()( postDown: HookSchema, allowedIps: AllowedIpsSchema.nullable(), serverAllowedIps: serverAllowedIps, - firewallIps: AllowedIpsSchema.nullable(), + firewallIps: FirewallIpsSchema.nullable(), mtu: MtuSchema, jC: JcSchema, jMin: JminSchema, diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index 6fed3d1f..2846bde9 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -1,6 +1,8 @@ import type { ZodSchema } from 'zod'; import z from 'zod'; import type { H3Event, EventHandlerRequest } from 'h3'; +import { isIP } from 'is-ip'; +import { default as isCidr } from 'is-cidr'; export type ID = number; @@ -91,6 +93,40 @@ export const AllowedIpsSchema = z .array(AddressSchema, { message: t('zod.allowedIps') }) .min(1, { message: t('zod.allowedIps') }); +// Validation for firewall IP entries +const FirewallIpEntrySchema = z + .string({ message: t('zod.client.firewallIps') }) + .min(1, { message: t('zod.client.firewallIps') }) + .refine( + (entry) => { + // Remove protocol suffix if present (/tcp, /udp) + const entryWithoutProto = entry.replace(/\/(tcp|udp)$/i, ''); + + // Check if it's IP:port format + const portMatch = entryWithoutProto.match(/^(.+):(\d+)$/); + if (portMatch) { + const [, ipPart, portPart] = portMatch; + const port = parseInt(portPart, 10); + + // Remove IPv6 brackets if present + const cleanIp = ipPart.replace(/^\[|\]$/g, ''); + + // Validate IP and port + return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535; + } + + // Check if it's just IP or CIDR + return isIP(entryWithoutProto) || isCidr(entryWithoutProto); + }, + { + message: t('zod.client.firewallIpsInvalid'), + } + ); + +export const FirewallIpsSchema = z.array(FirewallIpEntrySchema, { + message: t('zod.client.firewallIps'), +}); + export const FileSchema = z.object({ file: z.string({ message: t('zod.file') }), });