mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(security): comprehensive security audit and hardening
Auth: login rate limit 30/min (was 500), global rate limit 1000/min (was unlimited), password/username max lengths on all Zod schemas, session invalidation on role change, API key legacy scan bounded to 100 keys. SVG: hardened regex sanitizer with CDATA stripping, XML entity decoding, set/animate/iframe/embed blocking, comprehensive data: URI blocking, use element external href blocking. 11 attack payload fixtures added. SSRF: fixed DNS rebinding TOCTOU by pinning resolved IPs via custom HTTP/HTTPS agents. Added 6to4 and NAT64 to blocked IPv6 ranges. Docker: capability dropping (cap_drop ALL + minimal cap_add), resource limits (4g/8g mem, 512/1024 pids), healthcheck timeout, password removed from startup banner, default password warning comments. Network: CSP and HSTS applied in all environments (not just production), stack traces removed from all error responses, internal paths stripped from error details, per-route rate limits on uploads (60/min) and URL fetches (200/hour). Files: exclusive temp file creation (O_EXCL), disk space circuit breaker, per-user storage quotas, settings payload 64KB size guard. Python sidecar: script name allowlist in dispatcher, minimal environment for subprocess spawns. Dependencies: fixed 6 production CVEs (drizzle-orm, fastify, fast-uri, @fastify/static, next, archiver/lodash). Pinned all GitHub Actions to SHA hashes. 114 security tests added. Full OWASP Top 10 penetration test matrix verified against production Docker container (30/30 pass after hardening).
This commit is contained in:
@@ -6,6 +6,40 @@ import { fileURLToPath } from "node:url";
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PYTHON_DIR = resolve(__dirname, "../python");
|
||||
|
||||
/**
|
||||
* Build a minimal environment for spawned Python processes.
|
||||
* Only passes through variables needed for venv, CUDA, model cache,
|
||||
* package resolution, and locale -- avoids leaking secrets or
|
||||
* application config from the parent process.
|
||||
*/
|
||||
function buildMinimalEnv(): Record<string, string> {
|
||||
const env: Record<string, string> = {
|
||||
PYTHONUNBUFFERED: "1",
|
||||
LANG: process.env.LANG || "C.UTF-8",
|
||||
};
|
||||
const passthrough = [
|
||||
"PATH",
|
||||
"HOME",
|
||||
"VIRTUAL_ENV",
|
||||
"PYTHONPATH",
|
||||
"CUDA_VISIBLE_DEVICES",
|
||||
"LD_LIBRARY_PATH",
|
||||
// Application-specific vars the sidecar scripts depend on
|
||||
"DATA_DIR",
|
||||
"MODELS_DIR",
|
||||
"U2NET_HOME",
|
||||
"PROCESSING_TIMEOUT_S",
|
||||
"DISPATCHER_MAX_REQUESTS",
|
||||
"PYTHON_VENV_PATH",
|
||||
];
|
||||
for (const key of passthrough) {
|
||||
if (process.env[key] !== undefined) {
|
||||
env[key] = process.env[key] as string;
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/** Try venv first, then system python. */
|
||||
function getPythonPath(): string {
|
||||
const venvPath = process.env.PYTHON_VENV_PATH || resolve(__dirname, "../../../.venv");
|
||||
@@ -115,6 +149,7 @@ function startDispatcher(): ChildProcess | null {
|
||||
try {
|
||||
const child = spawn(getPythonPath(), [resolve(PYTHON_DIR, "dispatcher.py")], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: buildMinimalEnv(),
|
||||
});
|
||||
|
||||
let stderrBuffer = "";
|
||||
@@ -394,6 +429,7 @@ function runPythonPerRequest(
|
||||
const trySpawn = (pythonBin: string, isFallback: boolean) => {
|
||||
const child = spawn(pythonBin, [scriptPath, ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: buildMinimalEnv(),
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
|
||||
Reference in New Issue
Block a user