From 06c4381d75363a0b438a61ebd7a9fa8d4a992dae Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 28 Jul 2026 10:42:41 -0600 Subject: [PATCH] fix(pwa): validate config port --- apps/pwa/src/config.mjs | 17 ++++++++-- apps/pwa/test/config-port.test.mjs | 50 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 apps/pwa/test/config-port.test.mjs diff --git a/apps/pwa/src/config.mjs b/apps/pwa/src/config.mjs index 81391bb..3285d06 100644 --- a/apps/pwa/src/config.mjs +++ b/apps/pwa/src/config.mjs @@ -23,13 +23,26 @@ function loadEnv() { } loadEnv(); -const origin = (process.env.PUBLIC_ORIGIN || `http://localhost:${process.env.PORT || 8080}`).replace(/\/+$/, ""); +function readPort(value = process.env.PORT) { + const raw = value === undefined || value === null || String(value).trim() === "" ? "8080" : String(value).trim(); + if (!/^\d+$/.test(raw)) { + throw new Error(`PORT must be a decimal integer, got ${JSON.stringify(value)}`); + } + const port = Number(raw); + if (!Number.isSafeInteger(port) || port < 0 || port > 65535) { + throw new Error(`PORT must be between 0 and 65535, got ${JSON.stringify(value)}`); + } + return port; +} + +const port = readPort(); +const origin = (process.env.PUBLIC_ORIGIN || `http://localhost:${port}`).trim().replace(/\/+$/, ""); const rpID = new URL(origin).hostname; export const config = { root: ROOT, env: process.env.NODE_ENV || "development", - port: Number(process.env.PORT || 8080), + port, origin, // WebAuthn relying party = this host. rpID, diff --git a/apps/pwa/test/config-port.test.mjs b/apps/pwa/test/config-port.test.mjs new file mode 100644 index 0000000..9de1cf3 --- /dev/null +++ b/apps/pwa/test/config-port.test.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; + +const CONFIG = new URL("../src/config.mjs", import.meta.url); + +function loadConfig(env) { + return spawnSync(process.execPath, [ + "--input-type=module", + "-e", + `import(${JSON.stringify(CONFIG.href)}) + .then(({ config }) => { + console.log(JSON.stringify({ port: config.port, origin: config.origin, rpID: config.rpID })); + }) + .catch((err) => { + console.error(err.message); + process.exit(1); + });`, + ], { + env: { + ...process.env, + PUBLIC_ORIGIN: "", + PORT: "", + ...env, + }, + encoding: "utf8", + }); +} + +test("config trims PORT before building the fallback origin", () => { + const res = loadConfig({ PORT: "3000 " }); + assert.equal(res.status, 0, res.stderr); + assert.deepEqual(JSON.parse(res.stdout), { + port: 3000, + origin: "http://localhost:3000", + rpID: "localhost", + }); +}); + +test("config rejects a non-integer PORT before building the fallback origin", () => { + const res = loadConfig({ PORT: "abc" }); + assert.equal(res.status, 1); + assert.match(res.stderr, /PORT must be a decimal integer/); +}); + +test("config rejects a PORT outside the TCP range", () => { + const res = loadConfig({ PORT: "65536" }); + assert.equal(res.status, 1); + assert.match(res.stderr, /PORT must be between 0 and 65535/); +});