fix(audit): critical security hardening and ops reliability fixes

This commit is contained in:
Zac Gaetano 2026-06-03 04:14:31 +00:00
parent 30328e2871
commit b766f4cdfb
4 changed files with 57 additions and 13 deletions

View file

@ -1,4 +1,4 @@
import { randomBytes, createHash } from 'node:crypto';
import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
const PREFIX = 'dfl_';
@ -10,6 +10,14 @@ export function hashToken(token) {
return createHash('sha256').update(token).digest('hex');
}
export function compareTokens(tokenA, tokenB) {
if (!tokenA || !tokenB) return false;
const a = Buffer.from(tokenA);
const b = Buffer.from(tokenB);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
export function parseBearer(authorizationHeader) {
if (!authorizationHeader || typeof authorizationHeader !== 'string') return null;
const m = authorizationHeader.match(/^Bearer\s+(\S+)$/i);

View file

@ -40,7 +40,7 @@ async function requireRecorderEdit(req, res, next) {
const SIDECAR_PORT_BASE = 7438;
// Docker API helper function
function dockerApi(method, path, body = null) {
function dockerApi(method, path, body = null, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const options = {
socketPath: '/var/run/docker.sock',
@ -60,9 +60,9 @@ function dockerApi(method, path, body = null) {
});
});
req.on('error', reject);
// Add 10-second timeout to prevent indefinite hangs if Docker daemon is unresponsive
req.setTimeout(10000, () => {
req.destroy(new Error('Docker API timeout after 10s'));
// Use parameterizable timeout to prevent indefinite hangs if Docker daemon is unresponsive
req.setTimeout(timeoutMs, () => {
req.destroy(new Error(`Docker API timeout after ${timeoutMs/1000}s`));
});
if (body) req.write(JSON.stringify(body));
req.end();
@ -796,13 +796,16 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
const containerId = recorder.container_id;
(async () => {
try {
const stopRes = await dockerApi('POST', `/containers/${containerId}/stop?t=180`);
const stopRes = await dockerApi('POST', `/containers/${containerId}/stop?t=180`, null, 185000);
if (stopRes.status !== 404) {
await waitForFinalize(recorder);
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
}
} catch (e) {
console.error('[recorders] failed local background stop:', e.message);
// Attempt finalize and cleanup even if stop call timed out
await waitForFinalize(recorder).catch(() => {});
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
}
})();
}
@ -997,10 +1000,14 @@ router.post('/probe', async (req, res) => {
if (!ALLOWED_PROBE_SCHEMES.has(proto)) {
return res.status(400).json({ error: `Scheme "${proto}" is not permitted for probe (#104)` });
}
// Non-admin users can only probe public hostnames. Admins may probe LAN.
if (!isAdmin(req) && isPrivateOrLoopback(parsed.hostname)) {
return res.status(403).json({ error: 'Probe target must be a public host (#104)' });
}
// Non-admin users can only probe public hostnames. Admins may probe LAN.
if (!isAdmin(req) && isPrivateOrLoopback(parsed.hostname)) {
return res.status(403).json({ error: 'Probe target must be a public host (#104)' });
}
// Probe target should not be mam-api itself.
if (parsed.hostname === 'mam-api' || parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
return res.status(403).json({ error: 'Internal probe target is not permitted' });
}
// Try the capture service first (5s timeout)

View file

@ -57,7 +57,7 @@ async function probeGrowingPath(path) {
// df -P returns POSIX-portable output: "Filesystem 1024-blocks Used Available Capacity Mounted-on"
try {
const { stdout } = await exec(`df -PB1 ${JSON.stringify(path)}`, { timeout: 3000 });
const { stdout } = await exec(`df -PB1 -- ${JSON.stringify(path)}`, { timeout: 3000 });
const lines = stdout.trim().split('\n');
if (lines.length >= 2) {
const cols = lines[1].split(/\s+/);

View file

@ -73,7 +73,19 @@ server {
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Access-Control-Allow-Origin * always;
# Tighten CORS: no wildcard.
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# Playout HLS preview CasparCG sidecar writes to the media volume under
@ -83,7 +95,19 @@ server {
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Access-Control-Allow-Origin * always;
# Tighten CORS: no wildcard.
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# API proxy - forward to mam-api service
@ -133,6 +157,11 @@ server {
try_files $uri $uri/ /index.html;
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:;" always;
}
# Deny access to dotfiles