Compare commits

...
Author SHA1 Message Date
Zaid Humaimidi 38e9dfd0e6 1.2.16 2026-03-29 13:59:06 -07:00
Zaid Humaimidi b6102bddbc Changelog for v1.2.16 2026-03-29 13:58:40 -07:00
Zaid Humaimidi e22b0c499e Update npm-shrinkwrap.json 2026-03-29 13:35:03 -07:00
Zaid Humaimidi 969dfaf943 Revert "Fix device ID parsing regression in Mithril 2.3.8"
This reverts commit f9ee39dfd0.
2026-03-29 12:58:36 -07:00
Zaid Humaimidi fb7e973d8b Revert Mithril to v2.2.2
Revert Mithril from v2.3.8 back to v2.2.2 (used in v1.2.13) to fix
regression bugs in the latest version that have not been resolved.
2026-03-29 12:57:58 -07:00
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
Zaid Humaimidi cc723c0b50 1.2.14 2026-03-12 14:26:22 -07:00
Zaid Humaimidi a2a6fd82f3 Changelog for v1.2.14 2026-03-12 14:25:25 -07:00
Zaid Humaimidi 729beba0ed Update dependencies 2026-03-12 12:38:03 -07:00
Zaid Humaimidi 1b14387aa2 Fix covers() for false and contradictions 2026-03-12 10:21:03 -07:00
Zaid Humaimidi 49cdd39617 Fix (de)serialization bug in VersionedMap 2025-02-12 19:05:08 -08:00
15 changed files with 3073 additions and 2723 deletions

No files matched your search

+21
View File
@@ -1,5 +1,26 @@
# Change Log
## 1.2.16 (2026-03-29)
- Fix regression in v1.2.15 that broke the device search component in the UI by
pinning Mithril dependency to v2.2.2 due to multiple unresolved regressions in
later versions.
## 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.
- Fix potential edge-case bugs in expression evaluation and session
serialization.
## 1.2.13 (2024-06-06)
- Increase connection timeout for UI and NBI from 30 to 120 seconds to avoid
+4 -4
View File
@@ -2,13 +2,13 @@ import { PermissionSet, Expression } from "../types.ts";
import { evaluate, or } from "./expression/util.ts";
export default class Authorizer {
private declare permissionSets: PermissionSet[];
private declare validatorCache: WeakMap<
declare private permissionSets: PermissionSet[];
declare private validatorCache: WeakMap<
any,
(mutationType, mutation, any) => boolean
>;
private declare hasAccessCache: Map<string, boolean>;
private declare getFilterCache: Map<string, Expression>;
declare private hasAccessCache: Map<string, boolean>;
declare private getFilterCache: Map<string, Expression>;
public constructor(permissionSets: PermissionSet[]) {
this.permissionSets = permissionSets;
+3 -3
View File
@@ -9,8 +9,8 @@ const TWO = BI.BigInt(2);
const NEGATIVE_ONE = BI.BigInt(-1);
class Indeterminates {
public declare map: Map<string, number>;
public declare sortedKeys: string[];
declare public map: Map<string, number>;
declare public sortedKeys: string[];
public constructor(str?: string) {
this.map = new Map();
@@ -98,7 +98,7 @@ function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
}
class Polynomial {
public declare terms: Term[];
declare public terms: Term[];
public constructor(terms: Term[]) {
this.terms = terms;
+1 -1
View File
@@ -955,7 +955,7 @@ export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = normalize(expr2);
if (!expr2) return true;
expr1 = normalize(expr1);
if (!Array.isArray(expr1)) return !!expr1;
if (!Array.isArray(expr1) && expr1) return true;
const synt1 = Clause.fromExpression(expr1);
const synt2 = Clause.fromExpression(expr2);
+3 -3
View File
@@ -1,9 +1,9 @@
import Path from "./path.ts";
export default class PathSet {
private declare lengthIndex: Set<Path>[];
private declare fragmentIndex: Map<string, Set<Path>>[];
private declare stringIndex: Map<string, Path>;
declare private lengthIndex: Set<Path>[];
declare private fragmentIndex: Map<string, Set<Path>>[];
declare private stringIndex: Map<string, Path>;
public constructor() {
this.lengthIndex = [];
+5 -5
View File
@@ -30,11 +30,11 @@ function legalChar(c: number): boolean {
}
export default class Path {
public declare readonly segments: Segments;
public declare readonly wildcard: number;
public declare readonly alias: number;
protected declare _string: string;
protected declare _stringIndex: number[];
declare public readonly segments: Segments;
declare public readonly wildcard: number;
declare public readonly alias: number;
declare protected _string: string;
declare protected _stringIndex: number[];
protected static parseAliasValue(
pattern: string,
+1 -1
View File
@@ -3,7 +3,7 @@ interface Instance {
}
export default class InstanceSet {
private declare set: Set<Instance>;
declare private set: Set<Instance>;
public constructor() {
this.set = new Set();
+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);
+9 -5
View File
@@ -7,10 +7,10 @@ interface Revisions<V> {
}
export default class VersionedMap<K, V> {
private declare _sizeDiff: number[];
private declare _revision: number;
private declare map: Map<K, (V | symbol)[]>;
public declare dirty: number;
declare private _sizeDiff: number[];
declare private _revision: number;
declare private map: Map<K, (V | symbol)[]>;
declare public dirty: number;
public constructor() {
this._sizeDiff = [0];
@@ -112,6 +112,8 @@ export default class VersionedMap<K, V> {
prev = v;
}
if (prev === NONEXISTENT && !res.delete) return null;
return res;
}
@@ -122,6 +124,8 @@ export default class VersionedMap<K, V> {
del,
);
if (!mutations) return;
const revisions = [];
let prev: V | symbol = NONEXISTENT;
@@ -149,7 +153,7 @@ export default class VersionedMap<K, V> {
public *diff(): IterableIterator<[K, V, V]> {
for (const [key, revisions] of this.map) {
if (revisions.length === 1) continue;
if (revisions.length <= 1) continue;
let first = revisions[0];
let last = revisions[revisions.length - 1];
if (first === NONEXISTENT && last === NONEXISTENT) continue;
+2942 -2661
View File
File diff suppressed because it is too large. Load diff
+23 -23
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "1.2.13",
"version": "1.2.16",
"description": "A TR-069 Auto Configuration Server (ACS)",
"repository": {
"type": "git",
@@ -26,15 +26,15 @@
},
"dependencies": {
"@breejs/later": "^4.2.0",
"@koa/bodyparser": "^5.1.1",
"@koa/router": "^13.1.0",
"@koa/bodyparser": "^5.1.2",
"@koa/router": "^13.1.1",
"bson": "^4.7.2",
"espresso-iisojs": "^1.0.8",
"iconv-lite": "^0.6.3",
"ipaddr.js": "^2.2.0",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.3",
"koa-compress": "^5.1.0",
"ipaddr.js": "^2.3.0",
"jsonwebtoken": "^9.0.3",
"koa": "^2.16.4",
"koa-compress": "^5.2.1",
"koa-jwt": "^4.0.3",
"koa-send": "^5.0.1",
"mongodb": "^4.16.0",
@@ -42,26 +42,26 @@
"seedrandom": "^3.0.5"
},
"devDependencies": {
"@types/codemirror": "^5.60.15",
"@types/jsonwebtoken": "^9.0.6",
"@types/codemirror": "^5.60.17",
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^2.15.0",
"@types/koa-compress": "^4.0.6",
"@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.6",
"@types/node": "^20.14.2",
"@types/node": "^20.19.37",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"codemirror": "^5.65.16",
"esbuild": "^0.21.4",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"mithril": "^2.2.2",
"prettier": "^3.3.1",
"sql.js": "^1.10.3",
"svgo": "^3.3.2",
"typescript": "^5.4.5",
"yaml": "^1.10.2"
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"codemirror": "^5.65.21",
"esbuild": "^0.27.4",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.2",
"mithril": "2.2.2",
"prettier": "^3.8.1",
"sql.js": "^1.14.1",
"svgo": "^3.3.3",
"typescript": "^5.9.3",
"yaml": "^1.10.3"
},
"engines": {
"node": ">=12.13.0"
+35 -1
View File
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js";
import { minimize, unionDiff } from "../lib/common/expression/synth.ts";
import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts";
import { parse, stringify } from "../lib/common/expression/parser.ts";
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
@@ -115,3 +115,37 @@ void test("unionDiff", async () => {
assert.strictEqual(setsEqual(res4, diffSet), true);
}
});
void test("covers", async () => {
assert.strictEqual(covers(false, false), true);
assert.strictEqual(covers(false, parse("decimal > 5 AND decimal < 3")), true);
assert.strictEqual(covers(parse("true"), parse("decimal > 0")), true);
assert.strictEqual(covers(parse("true"), parse("false")), true);
assert.strictEqual(covers(parse("false"), parse("false")), true);
assert.strictEqual(covers(parse("false"), parse("decimal > 0")), false);
assert.strictEqual(covers(parse("decimal >= 0"), parse("decimal > 0")), true);
assert.strictEqual(
covers(parse("decimal > 0"), parse("decimal >= 0")),
false,
);
const cases = [
["decimal >= 0", "decimal > 0"],
["decimal > 0", "decimal > 5"],
["string IS NOT NULL", "string = 'a'"],
["true", "decimal > 0"],
];
for (const [c1, c2] of cases) {
const res1 = await query(c1);
const res2 = await query(c2);
const coversResult = covers(parse(c1), parse(c2));
const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
assert.strictEqual(
coversResult,
actuallyCovers,
`covers(${c1}, ${c2}) should match actual coverage`,
);
}
});
+2 -1
View File
@@ -11,7 +11,8 @@
"strictFunctionTypes": true,
"isolatedModules": true,
"noEmit": true,
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"skipLibCheck": true
},
"include": [
"./bin/*.ts",
+7 -7
View File
@@ -4,13 +4,13 @@ type AutocompleteCallback = (
) => void;
export default class Autocomplete {
private declare callback: AutocompleteCallback;
private declare element: HTMLInputElement;
private declare hideTimeout: NodeJS.Timeout;
private declare visible: boolean;
private declare default: string;
private declare selection: number;
private declare container: HTMLElement;
declare private callback: AutocompleteCallback;
declare private element: HTMLInputElement;
declare private hideTimeout: NodeJS.Timeout;
declare private visible: boolean;
declare private default: string;
declare private selection: number;
declare private container: HTMLElement;
public constructor(className: string, callback: AutocompleteCallback) {
this.callback = callback;
+1 -1
View File
@@ -79,7 +79,7 @@ export function structureConfig(config: Config[]): any {
if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) {
const pos = Math.floor(idx / MAX_BITS);
if (!indexes[pos]) indexes[pos] = 0;
indexes[pos] |= 1 << idx % MAX_BITS;
indexes[pos] |= 1 << (idx % MAX_BITS);
} else {
indexes = [];
break;