- adminMilestones was reached only because adminConfig is mounted on /api/admin ahead of it and guards the whole path. adminMiddleware checks req.user.role and nothing else, so it failed closed (403) rather than open — but on mount order, not intent. It now states the requirement, with a test covering all four admin routers. - TODO.md records the whole audit: what was verified working (live transcription round trip, voice mode wiring), what was fixed, the two advisories that are unreachable and why, and the CI/CD and Kubernetes work worth doing before scaling out. Security review found nothing else exploitable: parameterised SQL throughout (the one interpolated table name is allowlisted), CORS refuses to start open in production, JWT_SECRET refuses to start unset in production, rate limits on /api and each auth route, a real CSP, and no secrets in the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
83 lines
3.9 KiB
JavaScript
83 lines
3.9 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8');
|
|
}
|
|
|
|
test('redactor removes PHI and common secret patterns from logs', () => {
|
|
const { redact } = require('../src/utils/redact');
|
|
const input = 'Authorization: Bearer abc.def.ghi password=secret123 token=tok_1234567890 email test@example.com MRN 123456789';
|
|
const output = redact(input);
|
|
|
|
assert.doesNotMatch(output, /abc\.def\.ghi/);
|
|
assert.doesNotMatch(output, /secret123/);
|
|
assert.doesNotMatch(output, /tok_1234567890/);
|
|
assert.doesNotMatch(output, /test@example\.com/);
|
|
assert.doesNotMatch(output, /123456789/);
|
|
assert.match(output, /\[REDACTED\]|\[JWT\]/);
|
|
});
|
|
|
|
test('URL safety helper blocks private network targets', () => {
|
|
const { isPrivateIp } = require('../src/utils/urlSafety');
|
|
|
|
assert.equal(isPrivateIp('127.0.0.1'), true);
|
|
assert.equal(isPrivateIp('10.0.0.5'), true);
|
|
assert.equal(isPrivateIp('172.16.0.1'), true);
|
|
assert.equal(isPrivateIp('192.168.1.10'), true);
|
|
assert.equal(isPrivateIp('169.254.169.254'), true);
|
|
assert.equal(isPrivateIp('100.64.0.1'), true);
|
|
assert.equal(isPrivateIp('::1'), true);
|
|
assert.equal(isPrivateIp('fe80::1'), true);
|
|
assert.equal(isPrivateIp('8.8.8.8'), false);
|
|
assert.equal(isPrivateIp('2606:4700:4700::1111'), false);
|
|
});
|
|
|
|
test('Nextcloud/WebDAV routes enforce SSRF guard and redirect blocking', () => {
|
|
const nextcloud = read('src/routes/nextcloud.js');
|
|
const learningAI = read('src/routes/learningAI.js');
|
|
|
|
assert.match(nextcloud, /assertSafeHttpsUrl\(cleanUrl, 'Nextcloud URL'\)/);
|
|
assert.match(nextcloud, /assertSafeHttpsUrl\(user\.nextcloud_url, 'Nextcloud URL'\)/);
|
|
assert.match(nextcloud, /maxRedirects: 0/);
|
|
assert.match(nextcloud, /encodeURIComponent\(username\)/);
|
|
assert.match(learningAI, /assertSafeHttpsUrl\(user\.nextcloud_url, 'Nextcloud URL'\)/);
|
|
assert.match(learningAI, /maxRedirects: 0/);
|
|
assert.match(learningAI, /encodeURIComponent\(user\.nextcloud_user\)/);
|
|
});
|
|
|
|
test('logs and audits avoid unbounded limits and PHI-prone details', () => {
|
|
const logs = read('src/routes/logs.js');
|
|
const encounters = read('src/routes/encounters.js');
|
|
const documents = read('src/routes/documents.js');
|
|
const learningAI = read('src/routes/learningAI.js');
|
|
|
|
assert.match(logs, /function clampLimit/);
|
|
assert.match(logs, /clampLimit\(req\.query\.limit, 50, 200\)/);
|
|
assert.match(logs, /logger\.warn\('client_error'/);
|
|
assert.match(logs, /redact\(trimField\(e\.stack, 1200\)\)/);
|
|
assert.doesNotMatch(logs, /console\.error\('\[CLIENT ERROR\]'/);
|
|
|
|
assert.doesNotMatch(encounters, /Saved encounter: ' \+ \(label/);
|
|
assert.doesNotMatch(encounters, /Loaded encounter: ' \+ \(row\.label/);
|
|
assert.doesNotMatch(documents, /Uploaded: ' \+ \(req\.file/);
|
|
assert.doesNotMatch(learningAI, /Char context/);
|
|
});
|
|
|
|
// adminMiddleware only checks req.user.role; without authMiddleware having run,
|
|
// req.user is undefined. These routes were protected only because adminConfig
|
|
// happens to be mounted on /api/admin first and guards the whole path.
|
|
test('admin routers state their own authentication, not mount order', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const root = path.join(__dirname, '..');
|
|
for (const file of ['adminMilestones.js', 'admin.js', 'adminConfig.js', 'adminDocs.js']) {
|
|
const src = fs.readFileSync(path.join(root, 'src/routes', file), 'utf8');
|
|
assert.match(src, /router\.use\(authMiddleware\);/, file + ' authenticates every route it serves');
|
|
}
|
|
const auth = fs.readFileSync(path.join(root, 'src/middleware/auth.js'), 'utf8');
|
|
assert.match(auth, /async function adminMiddleware\(req, res, next\) \{\s*\n\s*if \(!req\.user \|\| req\.user\.role !== 'admin'\)/,
|
|
'and the role check stays a role check, so it fails closed on its own');
|
|
});
|