From 04be97e48cf0a129c86242224a57dc9116551b7f Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sat, 6 Jun 2026 18:29:48 +0200 Subject: [PATCH] add unit tests for ssrf guard, version compare, and settings validation --- apps/ignis-server/server/routes/proxy.js | 1 + .../ignis-server/server/routes/proxy.test.mjs | 62 +++++++++++++++++++ apps/ignis-server/server/routes/settings.js | 1 + .../server/routes/settings.test.mjs | 47 ++++++++++++++ packages/bridge/src/util/version.test.mjs | 31 ++++++++++ 5 files changed, 142 insertions(+) create mode 100644 apps/ignis-server/server/routes/proxy.test.mjs create mode 100644 apps/ignis-server/server/routes/settings.test.mjs create mode 100644 packages/bridge/src/util/version.test.mjs diff --git a/apps/ignis-server/server/routes/proxy.js b/apps/ignis-server/server/routes/proxy.js index 530f59c..4b5fb97 100644 --- a/apps/ignis-server/server/routes/proxy.js +++ b/apps/ignis-server/server/routes/proxy.js @@ -183,3 +183,4 @@ router.post("/", async (req, res) => { }); module.exports = router; +module.exports.isPrivateIp = isPrivateIp; diff --git a/apps/ignis-server/server/routes/proxy.test.mjs b/apps/ignis-server/server/routes/proxy.test.mjs new file mode 100644 index 0000000..6ddd3d2 --- /dev/null +++ b/apps/ignis-server/server/routes/proxy.test.mjs @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { isPrivateIp } = require("./proxy.js"); + +describe("isPrivateIp", () => { + it("flags private and link-local IPv4", () => { + for (const ip of [ + "0.0.0.0", + "10.0.0.1", + "127.0.0.1", + "169.254.1.1", + "172.16.0.1", + "172.31.255.255", + "192.168.1.1", + "100.64.0.1", + "100.127.255.255", + ]) { + expect(isPrivateIp(ip), ip).toBe(true); + } + }); + + it("allows public IPv4, including range boundaries", () => { + for (const ip of [ + "8.8.8.8", + "1.1.1.1", + "172.15.255.255", + "172.32.0.0", + "100.63.255.255", + "100.128.0.0", + "169.253.0.0", + "169.255.0.0", + "11.0.0.1", + "192.169.0.1", + ]) { + expect(isPrivateIp(ip), ip).toBe(false); + } + }); + + it("flags private and link-local IPv6", () => { + for (const ip of ["::1", "::", "fc00::1", "fd12::1", "fe80::1", "feaf::1"]) { + expect(isPrivateIp(ip), ip).toBe(true); + } + }); + + it("allows public IPv6", () => { + for (const ip of ["2606:4700:4700::1111", "2001:4860:4860::8888"]) { + expect(isPrivateIp(ip), ip).toBe(false); + } + }); + + it("classifies IPv4-mapped IPv6 by the embedded address", () => { + expect(isPrivateIp("::ffff:127.0.0.1")).toBe(true); + expect(isPrivateIp("::ffff:8.8.8.8")).toBe(false); + }); + + it("returns false for non-IP input", () => { + expect(isPrivateIp("not-an-ip")).toBe(false); + expect(isPrivateIp("")).toBe(false); + }); +}); diff --git a/apps/ignis-server/server/routes/settings.js b/apps/ignis-server/server/routes/settings.js index 56b6c0b..1171660 100644 --- a/apps/ignis-server/server/routes/settings.js +++ b/apps/ignis-server/server/routes/settings.js @@ -94,3 +94,4 @@ router.post("/", (req, res) => { }); module.exports = router; +module.exports.validate = validate; diff --git a/apps/ignis-server/server/routes/settings.test.mjs b/apps/ignis-server/server/routes/settings.test.mjs new file mode 100644 index 0000000..5afa6dc --- /dev/null +++ b/apps/ignis-server/server/routes/settings.test.mjs @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { validate } = require("./settings.js"); +const settings = require("../settings.js"); + +describe("settings validate", () => { + it("rejects an unknown proxy mode", () => { + expect(() => validate({ proxyMode: "bogus" })).toThrow(); + }); + + it("rejects negative or non-integer numbers", () => { + expect(() => validate({ contentCacheBytes: -1 })).toThrow(); + expect(() => validate({ contentCacheBytes: 1.5 })).toThrow(); + expect(() => validate({ contentCacheBytes: "5" })).toThrow(); + }); + + it("enforces maxBodyBytes bounds", () => { + expect(() => validate({ maxBodyBytes: 0 })).toThrow(); + expect(() => + validate({ maxBodyBytes: settings.MAX_BODY_BACKSTOP + 1 }), + ).toThrow(); + expect(validate({ maxBodyBytes: 1048576 })).toEqual({ + maxBodyBytes: 1048576, + }); + }); + + it("trims a valid proxy allowlist", () => { + expect( + validate({ proxyAllowlist: [" api.example.com ", "github.com"] }), + ).toEqual({ proxyAllowlist: ["api.example.com", "github.com"] }); + }); + + it("rejects a non-array allowlist or an empty entry", () => { + expect(() => validate({ proxyAllowlist: "x" })).toThrow(); + expect(() => validate({ proxyAllowlist: ["ok", " "] })).toThrow(); + }); + + it("ignores wsOrigins, which is env-only", () => { + expect(validate({ wsOrigins: ["https://evil.example.com"] })).toEqual({}); + }); + + it("ignores unknown keys", () => { + expect(validate({ bogusKey: 1 })).toEqual({}); + }); +}); diff --git a/packages/bridge/src/util/version.test.mjs b/packages/bridge/src/util/version.test.mjs new file mode 100644 index 0000000..9472995 --- /dev/null +++ b/packages/bridge/src/util/version.test.mjs @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { stripBuildMetadata, isNewer } = require("./version.js"); + +describe("isNewer", () => { + it("is true when latest is strictly newer", () => { + expect(isNewer("0.8.4", "0.8.3")).toBe(true); + expect(isNewer("1.0.0", "0.9.9")).toBe(true); + expect(isNewer("0.9.0", "0.8.9")).toBe(true); + }); + + it("is false for older or equal, so no downgrade is prompted", () => { + expect(isNewer("0.8.3", "0.8.4")).toBe(false); + expect(isNewer("0.8.4", "0.8.4")).toBe(false); + expect(isNewer("0.9.9", "1.0.0")).toBe(false); + }); + + it("is false for malformed versions", () => { + expect(isNewer("x", "0.8.4")).toBe(false); + expect(isNewer("0.8", "0.8.4")).toBe(false); + expect(isNewer("1.x.0", "0.8.4")).toBe(false); + }); + + it("ignores build metadata, so an equal version with a build tag is not newer", () => { + expect( + isNewer(stripBuildMetadata("0.8.4"), stripBuildMetadata("0.8.4+q2fmfox")), + ).toBe(false); + }); +});