Compare commits

..
5 Commits
Author SHA1 Message Date
Zaid Humaimidi 944a690d96 1.2.15 2026-03-26 14:12:26 -07:00
Zaid Humaimidi e7facc3bba Changelog for v1.2.15 2026-03-26 14:10:18 -07:00
Zaid Humaimidi aa83b62064 Update dependencies 2026-03-26 13:50:37 -07:00
Zaid Humaimidi f9ee39dfd0 Fix device ID parsing regression in Mithril 2.3.8
Device IDs containing literal % characters (e.g., "202BC1-CODA%2D5519-000000")
are double-encoded in URLs. Mithril 2.3.8 incorrectly double-decodes route
parameters, corrupting device IDs with special characters.

Workaround: Extract device ID directly from window.location.hash and decode
once to get the correct value.

See: https://github.com/MithrilJS/mithril.js/issues/3064
2026-03-26 13:20:19 -07:00
Zaid Humaimidi 087b95388f Fix RCE vulnerability in /api/ping endpoint
This addresses a follow-up to CVE-2021-46704 where the host validation
could be bypassed via domainToASCII stripping path components.

Changes:
- Normalize host with domainToASCII before validation
- Add round-trip check with domainToUnicode to catch path injection
- Switch from exec to execFile to prevent shell injection
- Add -- separator to ping arguments

Credit: JuHwiSang for reporting this vulnerability
2026-03-25 15:00:00 -07:00
5 changed files with 84 additions and 37 deletions

No files matched your search

+8
View File
@@ -1,5 +1,13 @@
# Change Log
## 1.2.15 (2026-03-26)
- Fix critical RCE vulnerability in /api/ping endpoint (follow-up to
CVE-2021-46704). Credit: JuHwiSang.
- Fix device IDs containing special characters (e.g., %) not being handled
correctly in the UI due to a regression in Mithril 2.3.8.
## 1.2.14 (2026-03-12)
- Prevent UI crash when a malformed URL is sent to the server.
+16 -7
View File
@@ -1,6 +1,6 @@
import { platform } from "node:os";
import { exec } from "node:child_process";
import { domainToASCII } from "node:url";
import { execFile } from "node:child_process";
import { domainToASCII, domainToUnicode } from "node:url";
export interface PingResult {
packetsTransmitted: number;
@@ -16,9 +16,16 @@ function isValidHost(host: string): boolean {
// Valid chars in IPv4, IPv6, domain names
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
const lowercase = host.toLowerCase();
const ascii = domainToASCII(lowercase);
// Check if input is an IDN convert to Punycode
// Can't merge with above because domainToASCII doesn't accept IP addresses
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
if (!/^[a-zA-Z0-9\-.:[\]-]+$/.test(ascii)) return false;
// Round-trip to ensure the original host matches its normalized form.
// This prevents bypasses where domainToASCII strips paths (e.g. "example.com/;cmd").
return lowercase === domainToUnicode(ascii);
}
export function parsePing(osPlatform: string, stdout: string): PingResult {
@@ -73,24 +80,26 @@ export function ping(
): void {
// Validate input to prevent possible remote code execution
// Credit to Alex Hordijk for reporting this vulnerability
// Credit to JuHwiSang for reporting a follow-up vulnerability
if (!isValidHost(host)) return callback(new Error("Invalid host"));
host = host.replace("[", "").replace("]", "");
let cmd: string;
let args: string[];
switch (platform()) {
case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
args = ["-w", "1", "-i", "0.2", "-c", "3", "--", host];
break;
case "freebsd":
// Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`;
args = ["-t", "1", "-c", "3", "--", host];
break;
default:
return callback(new Error("Platform not supported"));
}
exec(cmd, (err, stdout) => {
// Use execFile instead of exec to avoid shell interpretation of the host
execFile("ping", args, (err, stdout) => {
if (err) return callback(err);
const parsed: PingResult = parsePing(platform(), stdout);
return callback(err, parsed, stdout);
+27 -24
View File
@@ -1,12 +1,12 @@
{
"name": "genieacs",
"version": "1.2.14",
"version": "1.2.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "genieacs",
"version": "1.2.14",
"version": "1.2.15",
"license": "AGPL-3.0",
"dependencies": {
"@breejs/later": "^4.2.0",
@@ -18,7 +18,7 @@
"ipaddr.js": "^2.3.0",
"jsonwebtoken": "^9.0.3",
"koa": "^2.16.4",
"koa-compress": "^5.2.0",
"koa-compress": "^5.2.1",
"koa-jwt": "^4.0.3",
"koa-send": "^5.0.1",
"mongodb": "^4.16.0",
@@ -36,7 +36,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^2.15.0",
"@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7",
"@types/mithril": "^2.2.8",
"@types/node": "^20.19.37",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
@@ -51,7 +51,7 @@
"sql.js": "^1.14.1",
"svgo": "^3.3.3",
"typescript": "^5.9.3",
"yaml": "^1.10.2"
"yaml": "^1.10.3"
},
"engines": {
"node": ">=12.13.0"
@@ -2080,10 +2080,11 @@
}
},
"node_modules/@types/mithril": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.2.7.tgz",
"integrity": "sha512-uetxoYizBMHPELl6DSZUfO6Q/aOm+h0NUCv9bVAX2iAxfrdBSOvU9KKFl+McTtxR13F+BReYLY814pJsZvnSxg==",
"dev": true
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.2.8.tgz",
"integrity": "sha512-FN9Tv1+Nlr0LNPGnIL/xOxLJfu5WW2n8HAFeo4yxF+/O0per/8g080xlXoo+xj8baowAcfsNI3k80DxyLY34gQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
@@ -4016,9 +4017,10 @@
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="
},
"node_modules/koa-compress": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-5.2.0.tgz",
"integrity": "sha512-RsRnI+v+/rs1lYpcAUcxowUzHYssf71qbMr0Mpdq1wktbtXDZmxBIgxJHtaEsBjSe4jiWYELpGFbASa2AemmOg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-5.2.1.tgz",
"integrity": "sha512-k8VJMZI7vvSq1/G/t6Jggeg4h7fec1NJxcYaDWC4/1PK6mJFPs0OYqj44vx/soUcDVEJR7ysR5cc6pszKpnzXA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"compressible": "^2.0.18",
@@ -5282,10 +5284,11 @@
"dev": true
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
@@ -6824,9 +6827,9 @@
}
},
"@types/mithril": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.2.7.tgz",
"integrity": "sha512-uetxoYizBMHPELl6DSZUfO6Q/aOm+h0NUCv9bVAX2iAxfrdBSOvU9KKFl+McTtxR13F+BReYLY814pJsZvnSxg==",
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.2.8.tgz",
"integrity": "sha512-FN9Tv1+Nlr0LNPGnIL/xOxLJfu5WW2n8HAFeo4yxF+/O0per/8g080xlXoo+xj8baowAcfsNI3k80DxyLY34gQ==",
"dev": true
},
"@types/ms": {
@@ -8246,9 +8249,9 @@
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="
},
"koa-compress": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-5.2.0.tgz",
"integrity": "sha512-RsRnI+v+/rs1lYpcAUcxowUzHYssf71qbMr0Mpdq1wktbtXDZmxBIgxJHtaEsBjSe4jiWYELpGFbASa2AemmOg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-5.2.1.tgz",
"integrity": "sha512-k8VJMZI7vvSq1/G/t6Jggeg4h7fec1NJxcYaDWC4/1PK6mJFPs0OYqj44vx/soUcDVEJR7ysR5cc6pszKpnzXA==",
"requires": {
"bytes": "^3.1.2",
"compressible": "^2.0.18",
@@ -9111,9 +9114,9 @@
"dev": true
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"dev": true
},
"ylru": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "1.2.14",
"version": "1.2.15",
"description": "A TR-069 Auto Configuration Server (ACS)",
"repository": {
"type": "git",
@@ -34,7 +34,7 @@
"ipaddr.js": "^2.3.0",
"jsonwebtoken": "^9.0.3",
"koa": "^2.16.4",
"koa-compress": "^5.2.0",
"koa-compress": "^5.2.1",
"koa-jwt": "^4.0.3",
"koa-send": "^5.0.1",
"mongodb": "^4.16.0",
@@ -46,7 +46,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^2.15.0",
"@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7",
"@types/mithril": "^2.2.8",
"@types/node": "^20.19.37",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
@@ -61,7 +61,7 @@
"sql.js": "^1.14.1",
"svgo": "^3.3.3",
"typescript": "^5.9.3",
"yaml": "^1.10.2"
"yaml": "^1.10.3"
},
"engines": {
"node": ">=12.13.0"
+29 -2
View File
@@ -3,6 +3,29 @@ import { m } from "./components.ts";
import config from "./config.ts";
import * as store from "./store.ts";
// Workaround for Mithril 2.3.8 regression
// https://github.com/MithrilJS/mithril.js/issues/3064
function getPathParamFromHash(hash: string, prefix: string): string | null {
if (!hash.startsWith(prefix)) return null;
const pathWithQuery = hash.slice(prefix.length);
// Stop at query string or hash fragment (mirrors Mithril's parsePathname)
const queryIdx = pathWithQuery.indexOf("?");
const hashIdx = pathWithQuery.indexOf("#");
let pathEnd = pathWithQuery.length;
if (queryIdx >= 0) pathEnd = Math.min(pathEnd, queryIdx);
if (hashIdx >= 0) pathEnd = Math.min(pathEnd, hashIdx);
const rawPath = pathWithQuery.slice(0, pathEnd);
// Normalize: collapse slashes, remove trailing slash (matches Mithril)
const normalizedPath = rawPath.replace(/\/{2,}/g, "/").replace(/\/$/, "");
// Decode once (Mithril already decoded with decodeURIComponentSafe)
return decodeURIComponent(normalizedPath);
}
export function init(
args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
@@ -12,9 +35,13 @@ export function init(
);
}
const deviceId =
getPathParamFromHash(window.location.hash, "#!/devices/") ??
(args.id as string);
return Promise.resolve({
deviceId: args.id,
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id],
deviceId: deviceId,
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], deviceId],
});
}