chore(security): resolve CodeQL findings — unbiased voucher RNG, linear email check, rate-limit query filter (v0.17.2-beta)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3fe8341b71
commit
b3ba2b98dc
@@ -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
|
||||||
@@ -33,6 +33,8 @@ jobs:
|
|||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# CodeQL queries to run — security-extended adds more security checks
|
# CodeQL queries to run — security-extended adds more security checks
|
||||||
queries: security-extended
|
queries: security-extended
|
||||||
|
# Filters out non-actionable rules (see file for rationale)
|
||||||
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v4
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-backend",
|
"name": "mikrotik-manager-backend",
|
||||||
"version": "0.17.1-beta",
|
"version": "0.17.2-beta",
|
||||||
"description": "Mikrotik Network Management Platform - Backend API",
|
"description": "Mikrotik Network Management Platform - Backend API",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ import { reportService, computeNextRun } from '../services/ReportService';
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
router.use(requireAuth);
|
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) ─────────────
|
// ─── API tokens (admin only — a token can never mint more tokens) ─────────────
|
||||||
|
|
||||||
router.get('/tokens', requireAdmin, async (_req: Request, res: Response) => {
|
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 (!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' });
|
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);
|
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' });
|
return res.status(400).json({ error: 'recipients must be comma-separated email addresses' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
// Human-friendly voucher codes: no ambiguous chars (0/O, 1/I/L)
|
||||||
const CODE_CHARS = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
const CODE_CHARS = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
||||||
function generateCode(len = 8): string {
|
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 = '';
|
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)}`;
|
return `${out.slice(0, 4)}-${out.slice(4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-frontend",
|
"name": "mikrotik-manager-frontend",
|
||||||
"version": "0.17.1-beta",
|
"version": "0.17.2-beta",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = 'v0.17.1 Beta';
|
export const APP_VERSION = 'v0.17.2 Beta';
|
||||||
|
|||||||
Reference in New Issue
Block a user