Files
picpeak/backend/src/utils/safeExec.js
T
Paul Nothaft 2b25d81144 security: comprehensive hardening across frontend, backend, and infrastructure
- Disable production source maps and hide nginx version
- Reduce JSON body limit from 10gb to 50mb (uploads use multer, not JSON)
- Strip database info and error details from health endpoint
- Mask reCAPTCHA secret key in admin settings API responses
- Whitelist sort/order query parameters in events and photos endpoints
- Stop reflecting arbitrary origins in static file CORS headers
- Align nginx security headers with backend Helmet CSP, remove deprecated X-XSS-Protection
- Strip EXIF metadata from generated thumbnails and hero images
- Bind postgres/redis dev ports to localhost in docker-compose configs
- Add safeExec utility (spawn with shell:false) to prevent command injection
- Convert all exec/execAsync calls in backup, restore, and database backup
  services to use safe spawn-based helpers
2026-02-16 22:33:20 +01:00

114 lines
3.2 KiB
JavaScript

const { spawn } = require('child_process');
/**
* Safe command execution utilities using spawn (shell: false).
* These prevent command injection by never invoking a shell interpreter.
*/
/**
* Run a command with arguments, returning { stdout, stderr }.
* Equivalent to execAsync(cmd) but safe from injection.
*/
function spawnAsync(cmd, args = [], options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, {
shell: false,
...options,
stdio: ['ignore', 'pipe', 'pipe']
});
const stdoutChunks = [];
const stderrChunks = [];
child.stdout.on('data', chunk => stdoutChunks.push(chunk));
child.stderr.on('data', chunk => stderrChunks.push(chunk));
child.on('error', reject);
child.on('close', (code) => {
const stdout = Buffer.concat(stdoutChunks).toString();
const stderr = Buffer.concat(stderrChunks).toString();
if (code !== 0) {
const err = new Error(`${cmd} exited with code ${code}: ${stderr}`);
err.code = code;
err.stdout = stdout;
err.stderr = stderr;
return reject(err);
}
resolve({ stdout, stderr });
});
});
}
/**
* Run a command and redirect stdout to a file (replaces shell `> file`).
*/
function spawnToFile(cmd, args, outputPath, options = {}) {
const fs = require('fs');
return new Promise((resolve, reject) => {
const outStream = fs.createWriteStream(outputPath);
const child = spawn(cmd, args, {
shell: false,
...options,
stdio: ['ignore', outStream, 'pipe']
});
const stderrChunks = [];
child.stderr.on('data', chunk => stderrChunks.push(chunk));
child.on('error', (err) => {
outStream.destroy();
reject(err);
});
child.on('close', (code) => {
outStream.end();
const stderr = Buffer.concat(stderrChunks).toString();
if (code !== 0) {
const err = new Error(`${cmd} exited with code ${code}: ${stderr}`);
err.code = code;
err.stderr = stderr;
return reject(err);
}
resolve({ stderr });
});
});
}
/**
* Run a command and pipe a file into stdin (replaces shell `< file`).
*/
function spawnFromFile(cmd, args, inputPath, options = {}) {
const fs = require('fs');
return new Promise((resolve, reject) => {
const inStream = fs.createReadStream(inputPath);
const child = spawn(cmd, args, {
shell: false,
...options,
stdio: [inStream, 'pipe', 'pipe']
});
const stdoutChunks = [];
const stderrChunks = [];
child.stdout.on('data', chunk => stdoutChunks.push(chunk));
child.stderr.on('data', chunk => stderrChunks.push(chunk));
child.on('error', (err) => {
inStream.destroy();
reject(err);
});
child.on('close', (code) => {
const stdout = Buffer.concat(stdoutChunks).toString();
const stderr = Buffer.concat(stderrChunks).toString();
if (code !== 0) {
const err = new Error(`${cmd} exited with code ${code}: ${stderr}`);
err.code = code;
err.stdout = stdout;
err.stderr = stderr;
return reject(err);
}
resolve({ stdout, stderr });
});
});
}
module.exports = { spawnAsync, spawnToFile, spawnFromFile };