diff --git a/src/functions/backend/auth/admin-api-guard.ts b/src/functions/backend/auth/admin-api-guard.ts new file mode 100644 index 0000000..4b89d49 --- /dev/null +++ b/src/functions/backend/auth/admin-api-guard.ts @@ -0,0 +1,37 @@ +import type { APIResponseObject } from "@moduletrace/bunext/types"; +import checkUserAccess from "./check-user-access"; +import userAuth from "./user-auth"; + +type Params = { + req: Request; +}; + +/** + * Guard for admin API routes. Returns an error + * response if the request is not from a logged in + * user with admin access, otherwise `undefined` + */ +export default async function adminApiGuard({ + req, +}: Params): Promise { + const { user, user_types } = await userAuth({ req }); + + if (!user?.logged_in_status) { + return { + success: false, + msg: `Unauthorized`, + logoutUser: true, + }; + } + + const is_super_admin = checkUserAccess({ user_types }); + + if (!is_super_admin.success) { + return { + success: false, + msg: `Unauthorized`, + }; + } + + return undefined; +} diff --git a/src/functions/backend/setup/delete-wireguard-host.ts b/src/functions/backend/setup/delete-wireguard-host.ts index 365d1ed..5dfb4f9 100644 --- a/src/functions/backend/setup/delete-wireguard-host.ts +++ b/src/functions/backend/setup/delete-wireguard-host.ts @@ -11,7 +11,7 @@ import grabHostDirnames from "./grab-host-dir-names"; import grabClientDirnames from "./grab-client-dir-names"; import removeSubnetFirewallRules from "../firewall/remove-subnet-firewall-rules"; import { AppData } from "@/src/data/app-data"; -import { execSync } from "node:child_process"; +import { rmSync } from "node:fs"; type Params = { host_id: number; @@ -56,7 +56,7 @@ export default async function deleteWireguardHost({ }); try { - execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" }); + rmSync(CLIENT_DIR, { recursive: true, force: true }); } catch {} await BunSQLite.delete({ @@ -112,7 +112,7 @@ export default async function deleteWireguardHost({ } try { - execSync(`rm -rf ${HOST_CONFIG_DIR}`, { encoding: "utf-8" }); + rmSync(HOST_CONFIG_DIR, { recursive: true, force: true }); } catch {} const del_host = await BunSQLite.sql({ diff --git a/src/functions/backend/setup/get-interface-local-ip.ts b/src/functions/backend/setup/get-interface-local-ip.ts index a76282f..65d9985 100644 --- a/src/functions/backend/setup/get-interface-local-ip.ts +++ b/src/functions/backend/setup/get-interface-local-ip.ts @@ -1,4 +1,5 @@ -import { execSync } from "node:child_process"; +import validateInterfaceName from "@/src/utils/validate-interface-name"; +import { execFileSync } from "node:child_process"; /** * Get the local IPv4 address of a specific network interface @@ -8,10 +9,11 @@ import { execSync } from "node:child_process"; export default async function getInterfaceLocalIP( interface_name: string | null, ): Promise { - if (!interface_name) return ``; + if (!validateInterfaceName({ name: interface_name })) return ``; try { - const output = execSync( - `ip -4 addr show ${interface_name}`, + const output = execFileSync( + "ip", + ["-4", "addr", "show", interface_name as string], { encoding: "utf-8" }, ).trim(); const match = output.match( diff --git a/src/functions/backend/setup/grab-host-network-interfaces.ts b/src/functions/backend/setup/grab-host-network-interfaces.ts index 7c02161..9d141e4 100644 --- a/src/functions/backend/setup/grab-host-network-interfaces.ts +++ b/src/functions/backend/setup/grab-host-network-interfaces.ts @@ -1,4 +1,5 @@ import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; import os from "node:os"; const VIRTUAL_INTERFACE_PATTERNS = [ @@ -25,15 +26,7 @@ function isVirtualInterface(name: string): boolean { } function isPhysicalInterface(name: string): boolean { - try { - const devicePath = `/sys/class/net/${name}/device`; - const result = execSync(`test -e ${devicePath} && echo yes || echo no`, { - encoding: "utf-8", - }).trim(); - return result === "yes"; - } catch { - return true; - } + return existsSync(`/sys/class/net/${name}/device`); } /** diff --git a/src/functions/backend/setup/grab-host-public-ip-address.ts b/src/functions/backend/setup/grab-host-public-ip-address.ts index 4888254..68c3bdc 100644 --- a/src/functions/backend/setup/grab-host-public-ip-address.ts +++ b/src/functions/backend/setup/grab-host-public-ip-address.ts @@ -1,4 +1,5 @@ -import { execSync } from "node:child_process"; +import validateInterfaceName from "@/src/utils/validate-interface-name"; +import { execFileSync, execSync } from "node:child_process"; /** * Function to grab the host's public @@ -9,12 +10,20 @@ export default async function grabHostPublicIPAddress( interface_name?: string | null, ) { try { - const curl_cmd = interface_name - ? `curl -sS --max-time 15 --interface ${interface_name} https://api.ipify.org` - : `curl -sS --max-time 15 https://api.ipify.org`; - const public_ip = execSync(curl_cmd, { - encoding: "utf-8", - }) + const interface_args = validateInterfaceName({ name: interface_name }) + ? ["--interface", interface_name as string] + : []; + const public_ip = execFileSync( + "curl", + [ + "-sS", + "--max-time", + "15", + ...interface_args, + "https://api.ipify.org", + ], + { encoding: "utf-8" }, + ) .trim(); if (/^\d{1,3}(\.\d{1,3}){3}$/.test(public_ip)) { diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts index 0b5c85a..5b86efd 100644 --- a/src/functions/backend/setup/setup-wireguard-client.ts +++ b/src/functions/backend/setup/setup-wireguard-client.ts @@ -5,7 +5,11 @@ import type { import { AppData } from "@/src/data/app-data"; import grabDirNames from "@/src/utils/grab-dir-names"; import { execSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; +import validateIpv4 from "@/src/utils/validate-ipv4"; +import validateAllowedIps from "@/src/utils/validate-allowed-ips"; +import validateEndpointHost from "@/src/utils/validate-endpoint-host"; import grabHostPublicIPAddress from "./grab-host-public-ip-address"; import setupWireguardHost from "./setup-wireguard-host"; import type { APIResponseObject } from "@moduletrace/bunext/types"; @@ -72,6 +76,24 @@ export default async function setupWireguardClient({ }; } + if (!validateIpv4({ ip: CLIENT_WG_IP })) { + return { + success: false, + msg: `Invalid Client Private IP address: ${CLIENT_WG_IP}`, + }; + } + + if ( + client?.allow_all_ips != 1 && + client?.allowed_ips && + !validateAllowedIps({ value: client.allowed_ips }) + ) { + return { + success: false, + msg: `Invalid Allowed IPs: ${client.allowed_ips}`, + }; + } + const HOST_WG_IP = resolved_host?.wg_ip_address; if (!HOST_WG_IP) { @@ -104,53 +126,57 @@ export default async function setupWireguardClient({ client?.public_ip_address || HOST_WG_IP; - const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); + if (!validateEndpointHost({ value: PUBLIC_IP_ADDRESS })) { + return { + success: false, + msg: `Invalid Host public IP address: ${PUBLIC_IP_ADDRESS}`, + }; + } - const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); + const LISTEN_PORT = Number( + resolved_host?.listen_port || AppData["DefaultWGListenPort"], + ); - const HOST_PUBLIC_KEY = execSync( - `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`, - { encoding: "utf-8" }, + if (!Number.isInteger(LISTEN_PORT)) { + return { + success: false, + msg: `Invalid Host listen port: ${resolved_host?.listen_port}`, + }; + } + + const CLIENT_PRIVATE_KEY = readFileSync( + CLIENT_PRIVATE_KEY_FILE, + "utf-8", ).trim(); - let sh = ``; + const HOST_PUBLIC_KEY = readFileSync( + path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME), + "utf-8", + ).trim(); - sh += `cd ${CLIENT_DIR}\n`; - sh += `cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF\n`; - sh += `\n`; - sh += `[Interface]\n`; - sh += `Address = ${CLIENT_WG_IP}/32\n`; - sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`; + let CLIENT_CONFIG = ``; + + CLIENT_CONFIG += `\n`; + CLIENT_CONFIG += `[Interface]\n`; + CLIENT_CONFIG += `Address = ${CLIENT_WG_IP}/32\n`; + CLIENT_CONFIG += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`; if (client?.allow_all_ips == 1) { - sh += `DNS = 1.1.1.1\n`; + CLIENT_CONFIG += `DNS = 1.1.1.1\n`; } - sh += `\n`; - sh += `[Peer]\n`; - sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`; - sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:${resolved_host?.listen_port || AppData["DefaultWGListenPort"]}\n`; + CLIENT_CONFIG += `\n`; + CLIENT_CONFIG += `[Peer]\n`; + CLIENT_CONFIG += `PublicKey = ${HOST_PUBLIC_KEY}\n`; + CLIENT_CONFIG += `Endpoint = ${PUBLIC_IP_ADDRESS}:${LISTEN_PORT}\n`; if (client?.allow_all_ips == 1) { - sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`; + CLIENT_CONFIG += `AllowedIPs = 0.0.0.0/0, ::/0\n`; } else if (client?.allowed_ips) { - sh += `AllowedIPs = ${client.allowed_ips}\n`; + CLIENT_CONFIG += `AllowedIPs = ${client.allowed_ips}\n`; } - sh += `PersistentKeepalive = 25\n`; + CLIENT_CONFIG += `PersistentKeepalive = 25\n`; - sh += `EOF\n`; - sh += `\n`; - - const exec = execSync(sh, { - encoding: "utf-8", - }); - - const CLIENT_CONFIG = execSync(`cat ${CLIENT_CONFIG_FILE}`, { - encoding: "utf-8", - }); + writeFileSync(CLIENT_CONFIG_FILE, CLIENT_CONFIG, { mode: 0o600 }); const host_setup_res = skip_host_setup ? { success: true } diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts index ff74ef0..fa0abde 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -5,6 +5,8 @@ import type { } from "@/db/types/db"; import grabDirNames from "@/src/utils/grab-dir-names"; import { execSync } from "node:child_process"; +import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import validateIpv4 from "@/src/utils/validate-ipv4"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { TableType, User } from "@/src/types"; @@ -125,6 +127,24 @@ export default async function setupWireguardHost({ }; } + if (!validateIpv4({ ip: HOST_WG_IP })) { + return { + success: false, + msg: `Invalid Host Private IP address: ${HOST_WG_IP}`, + }; + } + + const LISTEN_PORT = Number( + host?.listen_port || AppData["DefaultWGListenPort"], + ); + + if (!Number.isInteger(LISTEN_PORT)) { + return { + success: false, + msg: `Invalid Host listen port: ${host?.listen_port}`, + }; + } + const is_ip_available = is_update_after_client_setup ? { success: true } : host?.wg_ip_address @@ -163,13 +183,12 @@ export default async function setupWireguardHost({ } try { - const HOST_PUBLIC_KEY = execSync(`cat ${HOST_PUBLIC_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); + const HOST_PUBLIC_KEY = readFileSync(HOST_PUBLIC_KEY_FILE, "utf-8").trim(); - const HOST_PRIVATE_KEY = execSync(`cat ${HOST_PRIVATE_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); + const HOST_PRIVATE_KEY = readFileSync( + HOST_PRIVATE_KEY_FILE, + "utf-8", + ).trim(); if (!is_update_after_client_setup) { const update_host = await BunSQLite.insert< @@ -189,12 +208,6 @@ export default async function setupWireguardHost({ }); } - let sh = ``; - - sh += `set -e\n`; - - sh += `cd ${HOST_CONFIG_DIR}\n`; - const HOST_SUBNET = `${HOST_WG_IP.split(".").slice(0, 3).join(".")}.0/24`; const iptables_scripts = buildHostIptablesScripts({ @@ -224,53 +237,53 @@ export default async function setupWireguardHost({ }; } - execSync(`mkdir -p ${HOST_IPTABLES_DIR}`, { encoding: "utf-8" }); + mkdirSync(HOST_IPTABLES_DIR, { recursive: true }); await Bun.write(POST_UP_PATH, iptables_scripts.post_up); await Bun.write(POST_DOWN_PATH, iptables_scripts.post_down); - execSync(`chmod +x ${POST_UP_PATH} ${POST_DOWN_PATH}`, { - encoding: "utf-8", - }); + chmodSync(POST_UP_PATH, 0o755); + chmodSync(POST_DOWN_PATH, 0o755); - sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`; - sh += `[Interface]\n`; - sh += `Address = ${HOST_WG_IP}/24\n`; - sh += `ListenPort = ${host?.listen_port || AppData["DefaultWGListenPort"]}\n`; - sh += `PrivateKey = ${HOST_PRIVATE_KEY}\n`; - sh += `PostUp = ${POST_UP_PATH}\n`; - sh += `PostDown = ${POST_DOWN_PATH}\n`; - sh += `\n`; + let HOST_CONFIG = ``; + + HOST_CONFIG += `[Interface]\n`; + HOST_CONFIG += `Address = ${HOST_WG_IP}/24\n`; + HOST_CONFIG += `ListenPort = ${LISTEN_PORT}\n`; + HOST_CONFIG += `PrivateKey = ${HOST_PRIVATE_KEY}\n`; + HOST_CONFIG += `PostUp = ${POST_UP_PATH}\n`; + HOST_CONFIG += `PostDown = ${POST_DOWN_PATH}\n`; + HOST_CONFIG += `\n`; if (clients[0]) { for (let i = 0; i < clients.length; i++) { const client = clients[i]; if (!client?.id) continue; + if (!validateIpv4({ ip: client.wg_ip_address })) { + return { + success: false, + msg: `Invalid Private IP address for client ${client.id}: ${client.wg_ip_address}`, + }; + } + const { CLIENT_PUBLIC_KEY_FILE } = grabClientDirnames({ client, host_id: HOST_ID, }); - const CLIENT_PUBLIC_KEY = execSync( - `cat ${CLIENT_PUBLIC_KEY_FILE}`, - { - encoding: "utf-8", - }, + const CLIENT_PUBLIC_KEY = readFileSync( + CLIENT_PUBLIC_KEY_FILE, + "utf-8", ).trim(); - sh += `[Peer]\n`; - sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`; - sh += `AllowedIPs = ${client.wg_ip_address}/32\n`; - sh += `\n`; + HOST_CONFIG += `[Peer]\n`; + HOST_CONFIG += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`; + HOST_CONFIG += `AllowedIPs = ${client.wg_ip_address}/32\n`; + HOST_CONFIG += `\n`; } } - sh += `EOF\n`; - - sh += `chmod 600 ${INTERFACE_NAME}.conf\n`; - - sh += `\n`; - - const exec = execSync(sh, { encoding: "utf-8" }); + writeFileSync(HOST_CONFIG_FILE, HOST_CONFIG, { mode: 0o600 }); + chmodSync(HOST_CONFIG_FILE, 0o600); if (!is_update_after_client_setup) { execSync( @@ -293,7 +306,7 @@ export default async function setupWireguardHost({ const firewall_res = await allowSubnetFirewallRules({ subnet: HOST_SUBNET, - listen_port: host?.listen_port || AppData["DefaultWGListenPort"], + listen_port: LISTEN_PORT, }); if (!firewall_res.success) { @@ -304,7 +317,7 @@ export default async function setupWireguardHost({ return { success: true, - msg: [exec.trim(), manage_res.msg, firewall_res.msg].join("\n\n"), + msg: [manage_res.msg, firewall_res.msg].join("\n\n"), }; } catch (error: any) { console.log( diff --git a/src/functions/server/media/handle-media-server.ts b/src/functions/server/media/handle-media-server.ts index ef283aa..0c3f3f3 100644 --- a/src/functions/server/media/handle-media-server.ts +++ b/src/functions/server/media/handle-media-server.ts @@ -65,6 +65,10 @@ export default async function handleMediaServer({ throw new Error(`Media record not found!`); } + if (privacy != "private" && media.is_private == 1) { + throw new Error(`Media record not found!`); + } + const media_location = thumbnail ? media.media_thumbnail_write_path : media.media_write_path; diff --git a/src/pages/api/admin/crud/(tables)/clients/del.ts b/src/pages/api/admin/crud/(tables)/clients/del.ts index 7b981d4..b96dc54 100644 --- a/src/pages/api/admin/crud/(tables)/clients/del.ts +++ b/src/pages/api/admin/crud/(tables)/clients/del.ts @@ -1,5 +1,5 @@ import _ from "lodash"; -import { execSync } from "node:child_process"; +import { rmSync } from "node:fs"; import path from "node:path"; import type { AdminCrudAPIParams, TableType } from "@/src/types"; import type { APIResponseObject } from "@moduletrace/bunext/types"; @@ -14,7 +14,7 @@ import type { BUN_SQLITE_WGUI_HOSTS, } from "@/db/types/db"; import BunSQLite from "@moduletrace/bun-sqlite"; -import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types"; +import type { ServerQueryParam } from "@moduletrace/bun-sqlite/types"; import grabClientDirnames from "@/src/functions/backend/setup/grab-client-dir-names"; const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_LIB_HOST_CLIENTS_DIR_NAME } = @@ -72,7 +72,7 @@ export default async function ( }); try { - execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" }); + rmSync(CLIENT_DIR, { recursive: true, force: true }); } catch (error) {} affected_host_ids.add(host_id); diff --git a/src/pages/api/admin/interface-local-ip.ts b/src/pages/api/admin/interface-local-ip.ts index e98f1f1..d7cee2a 100644 --- a/src/pages/api/admin/interface-local-ip.ts +++ b/src/pages/api/admin/interface-local-ip.ts @@ -1,5 +1,5 @@ import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip"; -import type { ApiReqParams } from "@/src/types"; +import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard"; import type { APIResponseObject, BunextAPIRouteHandler, @@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler = async ( if (params.req.method !== "GET") { return { success: false }; } + + const unauthorized_res = await adminApiGuard({ req: params.req }); + + if (unauthorized_res) { + return unauthorized_res; + } + const url = new URL(params.req.url || ""); const iface = url.searchParams.get("interface") || undefined; try { diff --git a/src/pages/api/admin/network-interfaces.ts b/src/pages/api/admin/network-interfaces.ts index 4495a26..014fa30 100644 --- a/src/pages/api/admin/network-interfaces.ts +++ b/src/pages/api/admin/network-interfaces.ts @@ -1,5 +1,5 @@ import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces"; -import type { ApiReqParams } from "@/src/types"; +import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard"; import type { APIResponseObject, BunextAPIRouteHandler, @@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler = async ( if (params.req.method !== "GET") { return { success: false }; } + + const unauthorized_res = await adminApiGuard({ req: params.req }); + + if (unauthorized_res) { + return unauthorized_res; + } + try { const interfaces = await grabHostNetworkInterfaces(); return { success: true, stringRes: JSON.stringify(interfaces) }; diff --git a/src/pages/api/admin/next-listen-port.ts b/src/pages/api/admin/next-listen-port.ts index 83c42e2..7d57f4d 100644 --- a/src/pages/api/admin/next-listen-port.ts +++ b/src/pages/api/admin/next-listen-port.ts @@ -1,5 +1,5 @@ import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port"; -import type { ApiReqParams } from "@/src/types"; +import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard"; import type { APIResponseObject, BunextAPIRouteHandler, @@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler = async ( if (params.req.method !== "GET") { return { success: false }; } + + const unauthorized_res = await adminApiGuard({ req: params.req }); + + if (unauthorized_res) { + return unauthorized_res; + } + try { const port = await getNextAvailableListenPort(); return { success: true, numberRes: port }; diff --git a/src/pages/api/admin/public-ip.ts b/src/pages/api/admin/public-ip.ts index edb4605..6ae8486 100644 --- a/src/pages/api/admin/public-ip.ts +++ b/src/pages/api/admin/public-ip.ts @@ -1,5 +1,5 @@ import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address"; -import type { ApiReqParams } from "@/src/types"; +import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard"; import type { APIResponseObject, BunextAPIRouteHandler, @@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler = async ( if (params.req.method !== "GET") { return { success: false }; } + + const unauthorized_res = await adminApiGuard({ req: params.req }); + + if (unauthorized_res) { + return unauthorized_res; + } + const url = new URL(params.req.url || ""); const iface = url.searchParams.get("interface") || undefined; try { diff --git a/src/utils/validate-allowed-ips.test.ts b/src/utils/validate-allowed-ips.test.ts new file mode 100644 index 0000000..b7c6372 --- /dev/null +++ b/src/utils/validate-allowed-ips.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test"; +import validateAllowedIps from "./validate-allowed-ips"; + +describe("validateAllowedIps", () => { + test("accepts single and comma-separated CIDRs", () => { + expect(validateAllowedIps({ value: "10.0.0.0/24" })).toBe(true); + expect(validateAllowedIps({ value: "10.0.0.0/24, 192.168.1.5/32" })).toBe(true); + }); + + test("rejects empty and unsafe values", () => { + expect(validateAllowedIps({ value: "" })).toBe(false); + expect(validateAllowedIps({ value: "$(id)" })).toBe(false); + expect(validateAllowedIps({ value: "10.0.0.0/24\nPostUp = id" })).toBe(false); + expect(validateAllowedIps({ value: "10.0.0.0/24," })).toBe(false); + }); +}); diff --git a/src/utils/validate-allowed-ips.ts b/src/utils/validate-allowed-ips.ts new file mode 100644 index 0000000..e099628 --- /dev/null +++ b/src/utils/validate-allowed-ips.ts @@ -0,0 +1,19 @@ +import validateIpv4Cidr from "./validate-ipv4-cidr"; + +type Params = { + value?: string | null; +}; + +/** + * Validate a comma-separated list of IPv4 + * addresses/CIDRs (WireGuard `AllowedIPs`) + */ +export default function validateAllowedIps({ value }: Params) { + if (!value) { + return false; + } + + return value + .split(",") + .every((entry) => validateIpv4Cidr({ value: entry.trim() })); +} diff --git a/src/utils/validate-endpoint-host.test.ts b/src/utils/validate-endpoint-host.test.ts new file mode 100644 index 0000000..ecad9bc --- /dev/null +++ b/src/utils/validate-endpoint-host.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test"; +import validateEndpointHost from "./validate-endpoint-host"; + +describe("validateEndpointHost", () => { + test("accepts IPv4 addresses and hostnames", () => { + expect(validateEndpointHost({ value: "203.0.113.4" })).toBe(true); + expect(validateEndpointHost({ value: "vpn.example.com" })).toBe(true); + }); + + test("rejects empty and unsafe values", () => { + expect(validateEndpointHost({ value: "" })).toBe(false); + expect(validateEndpointHost({ value: "1.2.3.4\nPostUp = id" })).toBe(false); + expect(validateEndpointHost({ value: "$(id)" })).toBe(false); + }); +}); diff --git a/src/utils/validate-endpoint-host.ts b/src/utils/validate-endpoint-host.ts new file mode 100644 index 0000000..bdc1f9a --- /dev/null +++ b/src/utils/validate-endpoint-host.ts @@ -0,0 +1,23 @@ +import validateIpv4 from "./validate-ipv4"; + +type Params = { + value?: string | null; +}; + +/** + * Validate a WireGuard `Endpoint` host: an IPv4 + * address or a DNS hostname + */ +export default function validateEndpointHost({ value }: Params) { + if (!value) { + return false; + } + + if (validateIpv4({ ip: value })) { + return true; + } + + return /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test( + value, + ); +} diff --git a/src/utils/validate-interface-name.test.ts b/src/utils/validate-interface-name.test.ts new file mode 100644 index 0000000..db69198 --- /dev/null +++ b/src/utils/validate-interface-name.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test"; +import validateInterfaceName from "./validate-interface-name"; + +describe("validateInterfaceName", () => { + test("accepts common interface names", () => { + expect(validateInterfaceName({ name: "eth0" })).toBe(true); + expect(validateInterfaceName({ name: "enp2s0" })).toBe(true); + expect(validateInterfaceName({ name: "wgui1" })).toBe(true); + expect(validateInterfaceName({ name: "eth0.100" })).toBe(true); + }); + + test("rejects empty, long, and shell-unsafe names", () => { + expect(validateInterfaceName({ name: "" })).toBe(false); + expect(validateInterfaceName({ name: null })).toBe(false); + expect(validateInterfaceName({ name: "a".repeat(16) })).toBe(false); + expect(validateInterfaceName({ name: "lo;id" })).toBe(false); + expect(validateInterfaceName({ name: "lo $(id)" })).toBe(false); + expect(validateInterfaceName({ name: "../x" })).toBe(false); + }); +}); diff --git a/src/utils/validate-interface-name.ts b/src/utils/validate-interface-name.ts new file mode 100644 index 0000000..1ae0790 --- /dev/null +++ b/src/utils/validate-interface-name.ts @@ -0,0 +1,15 @@ +type Params = { + name?: string | null; +}; + +/** + * Validate a Linux network interface name + * (max 15 chars, no whitespace or shell characters) + */ +export default function validateInterfaceName({ name }: Params) { + if (!name) { + return false; + } + + return /^[A-Za-z0-9_.:-]{1,15}$/.test(name); +}