refactor: replace express-ipfilter with lightweight custom middleware (#3917)

This fixes security issue
[CVE-2023-42282](https://github.com/advisories/GHSA-78xj-cgh5-2h22),
which is not very likely to be exploitable in MagicMirror² setups, but
still should be fixed.

The [express-ipfilter](https://www.npmjs.com/package/express-ipfilter)
package depends on the obviously unmaintained
[ip](https://github.com/indutny/node-ip) package, which has known
security vulnerabilities. Since no fix is available, this commit
replaces both dependencies with a custom middleware using the better
maintained [ipaddr.js](https://www.npmjs.com/package/ipaddr.js) library.

Changes:
- Add new `js/ip_access_control.js` with lightweight middleware
- Remove `express-ipfilter` dependency, add `ipaddr.js`
- Update `js/server.js` to use new middleware
- In addition, I have formulated the descriptions of the corresponding
tests a little more clearly.
This commit is contained in:
Kristjan ESPERANTO
2025-10-18 19:56:55 +02:00
committed by GitHub
parent 9ff716f4ab
commit 37d1a3ae8f
6 changed files with 93 additions and 88 deletions

View File

@@ -21,6 +21,7 @@ planned for 2026-01-01
- feat: add ESlint rule `no-sparse-arrays` for config check to fix #3910 (#3911)
- fixed eslint warnings shown in #3911 and updated npm publish docs (#3913)
- [core] refactor: replace `express-ipfilter` with lightweight custom middleware (#3917) - This fixes security issue [CVE-2023-42282](https://github.com/advisories/GHSA-78xj-cgh5-2h22), which is not very likely to be exploitable in MagicMirror² setups, but still should be fixed.
### Updated

63
js/ip_access_control.js Normal file
View File

@@ -0,0 +1,63 @@
const ipaddr = require("ipaddr.js");
const Log = require("logger");
/**
* Checks if a client IP matches any entry in the whitelist
* @param {string} clientIp - The IP address to check
* @param {string[]} whitelist - Array of IP addresses or CIDR ranges
* @returns {boolean} True if IP is allowed
*/
function isAllowed (clientIp, whitelist) {
try {
const addr = ipaddr.process(clientIp);
return whitelist.some((entry) => {
try {
// CIDR notation
if (entry.includes("/")) {
const [rangeAddr, prefixLen] = ipaddr.parseCIDR(entry);
return addr.match(rangeAddr, prefixLen);
}
// Single IP address - let ipaddr.process normalize both
const allowedAddr = ipaddr.process(entry);
return addr.toString() === allowedAddr.toString();
} catch (err) {
Log.warn(`Invalid whitelist entry: ${entry}`);
return false;
}
});
} catch (err) {
Log.warn(`Failed to parse client IP: ${clientIp}`);
return false;
}
}
/**
* Creates an Express middleware for IP whitelisting
* @param {string[]} whitelist - Array of allowed IP addresses or CIDR ranges
* @returns {import("express").RequestHandler} Express middleware function
*/
function ipAccessControl (whitelist) {
// Empty whitelist means allow all
if (!Array.isArray(whitelist) || whitelist.length === 0) {
return function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
next();
};
}
return function (req, res, next) {
const clientIp = req.ip || req.socket.remoteAddress;
if (isAllowed(clientIp, whitelist)) {
res.header("Access-Control-Allow-Origin", "*");
next();
} else {
Log.log(`IP ${clientIp} is not allowed to access the mirror`);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
}
};
}
module.exports = { ipAccessControl };

View File

@@ -3,12 +3,13 @@ const http = require("node:http");
const https = require("node:https");
const path = require("node:path");
const express = require("express");
const ipfilter = require("express-ipfilter").IpFilter;
const helmet = require("helmet");
const socketio = require("socket.io");
const Log = require("logger");
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("#server_functions");
const { ipAccessControl } = require(`${__dirname}/ip_access_control`);
const vendor = require(`${__dirname}/vendor`);
/**
@@ -84,17 +85,7 @@ function Server (config) {
Log.warn("You're using a full whitelist configuration to allow for all IPs");
}
app.use(function (req, res, next) {
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
if (err === undefined) {
res.header("Access-Control-Allow-Origin", "*");
return next();
}
Log.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
});
});
app.use(ipAccessControl(config.ipWhitelist));
app.use(helmet(config.httpHeaders));
app.use("/js", express.static(__dirname));

92
package-lock.json generated
View File

@@ -20,11 +20,11 @@
"envsub": "^4.1.0",
"eslint": "^9.37.0",
"express": "^5.1.0",
"express-ipfilter": "^1.3.2",
"feedme": "^2.0.2",
"helmet": "^8.1.0",
"html-to-text": "^9.0.5",
"iconv-lite": "^0.7.0",
"ipaddr.js": "^2.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"node-ical": "^0.21.0",
@@ -156,7 +156,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -886,8 +885,7 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz",
"integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@cspell/dict-dart": {
"version": "2.3.1",
@@ -1027,16 +1025,14 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz",
"integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@cspell/dict-html-symbol-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz",
"integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@cspell/dict-java": {
"version": "5.0.12",
@@ -1234,8 +1230,7 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz",
"integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@cspell/dict-vue": {
"version": "3.0.5",
@@ -1376,7 +1371,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1423,7 +1417,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3249,7 +3242,6 @@
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.46.1",
@@ -3586,7 +3578,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4139,7 +4130,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -4436,7 +4426,6 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -6070,7 +6059,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -6571,21 +6559,6 @@
"basic-auth": "^2.0.1"
}
},
"node_modules/express-ipfilter": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/express-ipfilter/-/express-ipfilter-1.3.2.tgz",
"integrity": "sha512-yMzCWGuVMnR8CFlsIC2spHWoQYp9vtyZXUgS/JdV5GOJgrz6zmKOEZsA4eF1XrxkOIVzaVk6yzTBk65pBhliNw==",
"license": "MIT",
"dependencies": {
"ip": "^2.0.1",
"lodash": "^4.17.11",
"proxy-addr": "^2.0.7",
"range_check": "^2.0.4"
},
"engines": {
"node": ">=8.9.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -7897,12 +7870,6 @@
"node": ">= 0.4"
}
},
"node_modules/ip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"license": "MIT"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
@@ -7912,22 +7879,13 @@
"node": ">= 12"
}
},
"node_modules/ip6": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/ip6/-/ip6-0.2.11.tgz",
"integrity": "sha512-OmTP7FyIp+ZoNvZ7Xr97bWrCgypa3BeuYuRFNTOPT8Y11cxMW1pW1VC70kHZP1onSHHMotADcjdg5QyECiIMUw==",
"license": "MIT",
"bin": {
"ip6": "ip6-cli.js"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
"node": ">= 10"
}
},
"node_modules/is-alphabetical": {
@@ -8544,7 +8502,6 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -9274,6 +9231,7 @@
"integrity": "sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"acorn": "^8.5.0",
"eslint-visitor-keys": "^3.0.0",
@@ -9293,6 +9251,7 @@
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -9306,6 +9265,7 @@
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
@@ -9755,7 +9715,6 @@
"integrity": "sha512-/4Osri9QFGCZOCTkfA8qJF+XGjKYERSHkXzxSyS1hd3ZERJGjvsUao2h4wdnvpHp6Tu2Jh/bPHM0FE9JJza6ng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"globby": "14.1.0",
"js-yaml": "4.1.0",
@@ -11726,7 +11685,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11776,7 +11734,6 @@
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -11807,7 +11764,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -11901,6 +11857,15 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-addr/node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/proxy-agent": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
@@ -12044,19 +12009,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range_check": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/range_check/-/range_check-2.0.4.tgz",
"integrity": "sha512-aed0ocXXj+SIiNNN9b+mZWA3Ow2GXHtftOGk2xQwshK5GbEZAvUcPWNQBLTx/lPcdFRIUFlFCRtHTQNIFMqynQ==",
"license": "BSD-2-Clause",
"dependencies": {
"ip6": "^0.2.0",
"ipaddr.js": "^1.9.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -13517,7 +13469,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4",
@@ -14382,7 +14333,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},

View File

@@ -79,11 +79,11 @@
"envsub": "^4.1.0",
"eslint": "^9.37.0",
"express": "^5.1.0",
"express-ipfilter": "^1.3.2",
"feedme": "^2.0.2",
"helmet": "^8.1.0",
"html-to-text": "^9.0.5",
"iconv-lite": "^0.7.0",
"ipaddr.js": "^2.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"node-ical": "^0.21.0",

View File

@@ -1,7 +1,7 @@
const helpers = require("./helpers/global-setup");
describe("ipWhitelist directive configuration", () => {
describe("Set ipWhitelist without access", () => {
describe("When IP is not in whitelist", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/noIpWhiteList.js");
});
@@ -9,13 +9,13 @@ describe("ipWhitelist directive configuration", () => {
await helpers.stopApplication();
});
it("should return 403", async () => {
it("should reject request with 403 (Forbidden)", async () => {
const res = await fetch("http://localhost:8181");
expect(res.status).toBe(403);
});
});
describe("Set ipWhitelist []", () => {
describe("When whitelist is empty (allow all IPs)", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/empty_ipWhiteList.js");
});
@@ -23,7 +23,7 @@ describe("ipWhitelist directive configuration", () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
it("should allow request with 200 (OK)", async () => {
const res = await fetch("http://localhost:8282");
expect(res.status).toBe(200);
});