From b3ba2b98dc51d4096f7167e67faccbb3453a92ba Mon Sep 17 00:00:00 2001 From: 2GT-Rich <107219997+2GT-Rich@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:56:18 -0700 Subject: [PATCH] =?UTF-8?q?chore(security):=20resolve=20CodeQL=20findings?= =?UTF-8?q?=20=E2=80=94=20unbiased=20voucher=20RNG,=20linear=20email=20che?= =?UTF-8?q?ck,=20rate-limit=20query=20filter=20(v0.17.2-beta)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the open CodeQL code-scanning alerts: - guestWifi voucher codes now use rejection sampling so the character distribution is uniform (fixes js/biased-cryptographic-random). - Scheduled-report recipient validation replaced the backtracking email regex with a linear split-based check (fixes js/polynomial-redos). - Add .github/codeql/codeql-config.yml excluding js/missing-rate-limiting: rate limiting is applied globally via app.use('/api', rateLimitRedis) plus per-route limiters, which CodeQL can't attribute to individual handlers, so that rule fired ~38 false positives. Remaining alerts are dismissed as accepted/false-positive (RouterOS MD5 API login and self-signed cert acceptance are protocol-mandated; log sites already strip CRLF). Co-Authored-By: Claude Opus 4.8 --- .github/codeql/codeql-config.yml | 10 ++++++++++ .github/workflows/codeql.yml | 2 ++ README.md | 2 +- backend/package.json | 2 +- backend/src/routes/automation.ts | 12 +++++++++++- backend/src/routes/guestWifi.ts | 14 ++++++++++++-- frontend/package.json | 2 +- frontend/src/version.ts | 2 +- 8 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 .github/codeql/codeql-config.yml diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..4c09409 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,10 @@ +name: "MikroTik Manager CodeQL config" + +# Rate limiting is enforced globally in Express — `app.use('/api', rateLimitRedis(...))` +# in backend/src/index.ts, plus dedicated limiters on auth/login/TOTP/OIDC routes. +# CodeQL's data flow can't attribute a global app-level middleware to each individual +# route handler, so `js/missing-rate-limiting` fires on ~every DB/fs handler as a false +# positive. Exclude it rather than carry dozens of non-actionable "high" alerts. +query-filters: + - exclude: + id: js/missing-rate-limiting diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0753c72..2dfd89e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -33,6 +33,8 @@ jobs: languages: ${{ matrix.language }} # CodeQL queries to run — security-extended adds more security checks queries: security-extended + # Filters out non-actionable rules (see file for rationale) + config-file: ./.github/codeql/codeql-config.yml - name: Autobuild uses: github/codeql-action/autobuild@v4 diff --git a/README.md b/README.md index 0f61740..c69fc45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface. -![Version](https://img.shields.io/badge/version-0.17.1_Beta-blue) +![Version](https://img.shields.io/badge/version-0.17.2_Beta-blue) ![License](https://img.shields.io/badge/license-AGPLv3-blue) ![Docker](https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white) diff --git a/backend/package.json b/backend/package.json index fc76a86..1048882 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.17.1-beta", + "version": "0.17.2-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/backend/src/routes/automation.ts b/backend/src/routes/automation.ts index a21d392..085b920 100644 --- a/backend/src/routes/automation.ts +++ b/backend/src/routes/automation.ts @@ -8,6 +8,16 @@ import { reportService, computeNextRun } from '../services/ReportService'; const router = Router(); router.use(requireAuth); +// Linear email check — no backtracking regex (avoids ReDoS on user-supplied lists). +function isValidEmail(addr: string): boolean { + if (/\s/.test(addr)) return false; + const at = addr.indexOf('@'); + if (at <= 0 || at !== addr.lastIndexOf('@')) return false; + const domain = addr.slice(at + 1); + const dot = domain.lastIndexOf('.'); + return dot > 0 && dot < domain.length - 1; +} + // ─── API tokens (admin only — a token can never mint more tokens) ───────────── router.get('/tokens', requireAdmin, async (_req: Request, res: Response) => { @@ -117,7 +127,7 @@ router.post('/reports', requireWrite, async (req: Request, res: Response) => { if (!name || !name.trim()) return res.status(400).json({ error: 'name is required' }); if (!['daily', 'weekly', 'monthly'].includes(frequency ?? '')) return res.status(400).json({ error: 'frequency must be daily, weekly or monthly' }); const rcpts = (recipients ?? '').split(',').map(r => r.trim()).filter(Boolean); - if (rcpts.length === 0 || rcpts.some(r => !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(r))) { + if (rcpts.length === 0 || rcpts.some(r => !isValidEmail(r))) { return res.status(400).json({ error: 'recipients must be comma-separated email addresses' }); } diff --git a/backend/src/routes/guestWifi.ts b/backend/src/routes/guestWifi.ts index 7715073..28befdb 100644 --- a/backend/src/routes/guestWifi.ts +++ b/backend/src/routes/guestWifi.ts @@ -87,9 +87,19 @@ router.post('/setup', requireWrite, async (req: Request, res: Response) => { // Human-friendly voucher codes: no ambiguous chars (0/O, 1/I/L) const CODE_CHARS = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; function generateCode(len = 8): string { - const bytes = randomBytes(len); + const n = CODE_CHARS.length; + // Rejection sampling: discard bytes at or above the largest multiple of n so + // the modulo is uniform (avoids the slight bias of a raw byte % n). + const limit = Math.floor(256 / n) * n; let out = ''; - for (let i = 0; i < len; i++) out += CODE_CHARS[bytes[i] % CODE_CHARS.length]; + while (out.length < len) { + for (const b of randomBytes(len)) { + if (b < limit) { + out += CODE_CHARS[b % n]; + if (out.length === len) break; + } + } + } return `${out.slice(0, 4)}-${out.slice(4)}`; } diff --git a/frontend/package.json b/frontend/package.json index 1b53d37..6962123 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.17.1-beta", + "version": "0.17.2-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 2e02954..682b463 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.17.1 Beta'; +export const APP_VERSION = 'v0.17.2 Beta';