Harden admin API routes and shell usage

- Require admin auth on interface-local-ip, network-interfaces,
  next-listen-port and public-ip (interface-local-ip and public-ip
  allowed unauthenticated command injection as root)
- Validate interface names and call ip/curl via execFileSync
- Write host/client WireGuard configs with fs instead of shell
  heredocs, validating IPs, AllowedIPs, endpoint and listen port
- Replace rm -rf/cat/mkdir/chmod/test shell calls with fs APIs
- Refuse private media requested through the public media route

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 21:09:27 +01:00
co-authored by Claude Opus 5.5
parent 178f98104c
commit cd707465f4
19 changed files with 326 additions and 106 deletions
@@ -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<APIResponseObject | undefined> {
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;
}
@@ -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<BUN_SQLITE_WGUI_CLIENTS, TableType>({
@@ -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({
@@ -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<string> {
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(
@@ -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`);
}
/**
@@ -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)) {
@@ -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 }
@@ -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(