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:
2GT-Rich
2026-07-18 09:56:18 -07:00
co-authored by Claude Opus 4.8
parent 3fe8341b71
commit b3ba2b98dc
8 changed files with 39 additions and 7 deletions
+11 -1
View File
@@ -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' });
}
+12 -2
View File
@@ -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)}`;
}