chore: clear the dependency backlog, and fix a log-injection finding (v0.23.32-beta)

Two pieces of housekeeping that had gone quiet.

**A code-scanning alert nobody had looked at.** CodeQL flagged js/log-injection
in the preflight handler: a request value interpolated straight into a log line.
Log entries are newline-delimited, so a value containing a line break forges
additional entries — and an audit trail its own subject can write into is worse
than none, because it is believed.

The interesting part is that the risk had already been recognised. Two sites in
the clients routes strip carriage returns inline, ad hoc, and the third was
simply missed. That is what a shared helper is for, so there is now a `logSafe`
utility with tests, used at all three. It replaces control characters with a
visible marker rather than stripping them, so a tampered value reads as tampered
instead of being quietly tidied, and truncates absurd input so one field cannot
flood the log.

**Twelve Dependabot pull requests, the oldest six weeks old.** Nine had passing
checks, but those runs were from July against a main that has moved a long way,
so rather than merging stale branches serially the eight safe ones are applied
here as one change and verified together: bullmq, pg, helmet, @types/supertest,
tailwind-merge, vite, actions/setup-node and github/codeql-action.

Deliberately excluded, and left open:

  * bcryptjs 2.4.3 to 3.0.3 (#79) — a major on the password-hashing path.
    Dependabot groups it with routine bumps; it is not one, and it deserves
    its own verification rather than riding along in a batch.
  * typescript 5.9 to 7.0 (#81), tailwindcss 3.4 to 4.3 (#76) and
    eslint-plugin-react-hooks (#78) — all three already fail their own checks.

Verified beyond the gates: the application was rebuilt and exercised on the new
dependencies — authentication, the BullMQ-backed poller, Postgres reads and
helmet's headers all behave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-09-05 16:17:35 -07:00
co-authored by Claude Opus 5
parent 1ce57a25ec
commit 33ca7cc651
12 changed files with 282 additions and 302 deletions
+51 -62
View File
@@ -1,30 +1,30 @@
{
"name": "mikrotik-manager-backend",
"version": "0.18.1-beta",
"version": "0.23.31-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mikrotik-manager-backend",
"version": "0.18.1-beta",
"version": "0.23.31-beta",
"dependencies": {
"@influxdata/influxdb-client": "^1.33.2",
"@influxdata/influxdb-client-apis": "^1.35.0",
"bcryptjs": "^2.4.3",
"bullmq": "^5.1.1",
"bullmq": "^5.81.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^17.4.2",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"helmet": "^8.1.0",
"helmet": "^8.3.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"nodemailer": "^9.0.3",
"openid-client": "^5.7.1",
"otpauth": "^9.5.1",
"pg": "^8.21.0",
"pg": "^8.22.0",
"qrcode": "^1.5.4",
"socket.io": "^4.6.1",
"ssh2": "^1.14.0"
@@ -43,7 +43,7 @@
"@types/pg": "^8.10.9",
"@types/qrcode": "^1.5.6",
"@types/ssh2": "^1.11.17",
"@types/supertest": "^7.2.0",
"@types/supertest": "^7.2.1",
"eslint": "^10.7.0",
"eslint-plugin-security": "^4.0.1",
"jest": "^30.4.2",
@@ -900,9 +900,9 @@
}
},
"node_modules/@ioredis/commands": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
"integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
@@ -2076,9 +2076,9 @@
}
},
"node_modules/@types/supertest": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz",
"integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==",
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz",
"integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3200,16 +3200,16 @@
}
},
"node_modules/bullmq": {
"version": "5.78.1",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.78.1.tgz",
"integrity": "sha512-zD5IT+qMqbMgPFPdL9FwnZka1bz6nckM+5lXj4N0vsXqdzoVO6wizmXpwsg/4GnHmXJsL7XOKeWA64tYUdPrOA==",
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.81.2.tgz",
"integrity": "sha512-Hi9GaVCC6HE9bQP65j/FNv1aL1fcEukTF99ezS5pl1Ud+joCFpNWiPCV45mWdkEmpuacS0XJdMMFGKPIHCwoPg==",
"license": "MIT",
"dependencies": {
"cron-parser": "4.9.0",
"ioredis": "5.10.1",
"msgpackr": "2.0.2",
"ioredis": "5.11.1",
"msgpackr": "2.0.4",
"node-abort-controller": "3.1.1",
"semver": "7.8.1",
"semver": "7.8.5",
"tslib": "2.8.1"
},
"engines": {
@@ -4821,12 +4821,15 @@
}
},
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
},
"node_modules/html-escaper": {
@@ -4944,20 +4947,18 @@
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
"version": "5.11.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz",
"integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.5.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
"@ioredis/commands": "1.10.0",
"cluster-key-slot": "1.1.1",
"debug": "4.4.3",
"denque": "2.1.0",
"redis-errors": "1.2.0",
"redis-parser": "3.0.0",
"standard-as-callback": "2.1.0"
},
"engines": {
"node": ">=12.22.0"
@@ -6056,24 +6057,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@@ -6318,9 +6307,9 @@
"license": "MIT"
},
"node_modules/msgpackr": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.2.tgz",
"integrity": "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz",
"integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.4"
@@ -6815,14 +6804,14 @@
"license": "MIT"
},
"node_modules/pg": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.13.0",
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.14.0",
"pg-protocol": "^1.15.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
@@ -6849,9 +6838,9 @@
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-int8": {
@@ -6873,9 +6862,9 @@
}
},
"node_modules/pg-protocol": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
"license": "MIT"
},
"node_modules/pg-types": {
@@ -7445,9 +7434,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-backend",
"version": "0.23.31-beta",
"version": "0.23.32-beta",
"description": "Mikrotik Network Management Platform - Backend API",
"main": "dist/index.js",
"scripts": {
@@ -16,20 +16,20 @@
"@influxdata/influxdb-client": "^1.33.2",
"@influxdata/influxdb-client-apis": "^1.35.0",
"bcryptjs": "^2.4.3",
"bullmq": "^5.1.1",
"bullmq": "^5.81.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^17.4.2",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"helmet": "^8.1.0",
"helmet": "^8.3.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"nodemailer": "^9.0.3",
"openid-client": "^5.7.1",
"otpauth": "^9.5.1",
"pg": "^8.21.0",
"pg": "^8.22.0",
"qrcode": "^1.5.4",
"socket.io": "^4.6.1",
"ssh2": "^1.14.0"
@@ -48,7 +48,7 @@
"@types/pg": "^8.10.9",
"@types/qrcode": "^1.5.6",
"@types/ssh2": "^1.11.17",
"@types/supertest": "^7.2.0",
"@types/supertest": "^7.2.1",
"eslint": "^10.7.0",
"eslint-plugin-security": "^4.0.1",
"jest": "^30.4.2",
+3 -2
View File
@@ -1,6 +1,7 @@
import { Router, Request, Response } from 'express';
import * as dgram from 'dgram';
import { query } from '../config/database';
import { logSafe } from '../utils/logSafe';
import { requireAuth, requireAdmin, requireWrite } from '../middleware/auth';
import { PollerService } from '../services/PollerService';
import { getQueryApi, bucket } from '../config/influxdb';
@@ -326,7 +327,7 @@ router.get('/:mac/traffic', async (req: Request, res: Response) => {
if (time && field && value != null) raw.push({ time, field, value });
});
} catch (err) {
console.error(`[clients/traffic] Flux error for ${mac.replace(/[\r\n]/g, '_')}:`, err);
console.error(`[clients/traffic] Flux error for ${logSafe(mac)}:`, err);
}
// Separate tx and rx series, sort by time, then compute non-negative deltas
@@ -384,7 +385,7 @@ router.get('/:mac/signal', async (req: Request, res: Response) => {
if (time && value != null) points.push({ time, signal_strength: Math.round(value) });
});
} catch (err) {
console.error(`[clients/signal] Flux error for ${mac.replace(/[\r\n]/g, '_')}:`, err);
console.error(`[clients/signal] Flux error for ${logSafe(mac)}:`, err);
}
return res.json(points);
+2 -1
View File
@@ -19,6 +19,7 @@ import { runConfigHealth } from '../services/changeGuard/configHealth';
import { isMultiVlanSpec } from '../utils/vlan';
import { redis } from '../config/redis';
import { enqueueBulkAddJob, getBulkAddJobState } from '../services/DeviceBulkAddWorker';
import { logSafe } from '../utils/logSafe';
import { parseBandList, uplinkAnchor, totalBandwidthMhz, type LteBandInfo } from '../utils/lte';
// Resolve a credential preset id into decrypted credentials. Returns null if
@@ -1017,7 +1018,7 @@ async function withGuardedChange<T>(
return;
}
} catch (err) {
console.warn(`[preflight] analysis skipped for device ${id}: ${(err as Error).message}`);
console.warn(`[preflight] analysis skipped for device ${logSafe(id)}: ${logSafe((err as Error).message)}`);
}
}
@@ -0,0 +1,37 @@
import { logSafe } from '../logSafe';
describe('logSafe', () => {
it('leaves ordinary values alone', () => {
expect(logSafe('ether1')).toBe('ether1');
expect(logSafe(42)).toBe('42');
expect(logSafe('2GT-NW-100G')).toBe('2GT-NW-100G');
});
it('stops a value forging a second log entry', () => {
// The whole point: a log that its subject can write lines into is worse
// than no log, because it is believed.
const forged = logSafe('7\n[preflight] device 999 verified OK');
expect(forged).not.toContain('\n');
expect(forged).toContain('[preflight] device 999 verified OK');
});
it('handles carriage returns and other control characters', () => {
expect(logSafe('a\r\nb')).not.toMatch(/[\r\n]/);
expect(logSafe('a\u0007b\u001bc')).toBe('a\u2423b\u2423c');
});
it('marks what it replaced rather than hiding it', () => {
expect(logSafe('a\nb')).toBe('a\u2423b');
});
it('truncates absurd input so one value cannot flood the log', () => {
const out = logSafe('x'.repeat(5000));
expect(out.length).toBeLessThanOrEqual(201);
expect(out.endsWith('…')).toBe(true);
});
it('handles null and undefined without printing them', () => {
expect(logSafe(null)).toBe('');
expect(logSafe(undefined)).toBe('');
});
});
+26
View File
@@ -0,0 +1,26 @@
/**
* Make an untrusted value safe to put in a log line.
*
* Log entries are newline-delimited, so a value containing a line break can
* forge additional entries — an audit trail that can be written into by the
* thing it is auditing is worse than no audit trail, because it is believed.
*
* Two sources matter and neither is under our control: values from HTTP
* requests, and strings read off devices. A device identity is attacker-settable
* on a compromised switch, and it appears in log lines throughout the collector.
*/
/** Longest a single interpolated value may be before it is truncated. */
const MAX_LEN = 200;
export function logSafe(value: unknown): string {
if (value == null) return '';
const text = typeof value === 'string' ? value : String(value);
// CR, LF, other C0 controls and DEL. Replaced rather than stripped so that a
// tampered value reads as tampered instead of being silently tidied up.
// eslint-disable-next-line no-control-regex
const flattened = text.replace(/[\u0000-\u001F\u007F]/g, '\u2423');
return flattened.length > MAX_LEN ? `${flattened.slice(0, MAX_LEN)}…` : flattened;
}