fix(security): harden rate limits, Redis auth, resource caps, and error sanitization

- Lower LOGIN_ATTEMPT_LIMIT default from 30 to 10 (brute-force protection)
- Lower RATE_LIMIT_PER_MIN default from 1000 to 300
- Add Redis authentication (requirepass) with REDIS_PASSWORD env var
- Add Redis maxmemory 512mb cap to prevent unbounded growth
- Add mem_limit: 1g to Postgres and Redis containers
- Strip internal file paths from all error responses (defense-in-depth)
- Add startup warnings for default admin/Postgres/Redis credentials
- Update security test expectations for new defaults
This commit is contained in:
SnapOtter
2026-06-20 00:53:11 +08:00
parent b847dcc2ab
commit d61226496b
6 changed files with 42 additions and 14 deletions
+3 -2
View File
@@ -20,6 +20,7 @@ import { closeWorkers, startWorkers } from "./jobs/worker.js";
import { captureException, initAnalytics, shutdownAnalytics } from "./lib/analytics.js";
import { shouldRunStartupCleanup } from "./lib/cleanup.js";
import { buildCsp } from "./lib/csp.js";
import { stripInternalPaths } from "./lib/errors.js";
import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.js";
import { logger } from "./lib/logger.js";
import { requestDuration } from "./lib/metrics.js";
@@ -229,8 +230,8 @@ app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) =>
request.log.warn({ err: error, url: request.url, method: request.method }, "Request error");
}
reply.status(statusCode).send({
error: statusCode >= 500 ? "Internal server error" : error.message,
...(statusCode < 500 && { details: error.message }),
error: statusCode >= 500 ? "Internal server error" : stripInternalPaths(error.message),
...(statusCode < 500 && { details: stripInternalPaths(error.message) }),
});
});
+2 -2
View File
@@ -32,7 +32,7 @@ const envSchema = z
MAX_BATCH_SIZE: z.coerce.number().default(0),
CONCURRENT_JOBS: z.coerce.number().default(0),
MAX_MEGAPIXELS: z.coerce.number().default(0),
RATE_LIMIT_PER_MIN: z.coerce.number().default(1000),
RATE_LIMIT_PER_MIN: z.coerce.number().default(300),
DATABASE_URL: z.string().default("postgres://snapotter:snapotter@localhost:5432/snapotter"),
SQLITE_MIGRATE_PATH: z.string().default(""),
FILES_STORAGE_PATH: z.string().default("./data/files"),
@@ -57,7 +57,7 @@ const envSchema = z
MAX_VIDEO_BITRATE_KBPS: z.coerce.number().default(0),
LIBREOFFICE_TIMEOUT_S: z.coerce.number().default(120),
SESSION_DURATION_HOURS: z.coerce.number().default(168),
LOGIN_ATTEMPT_LIMIT: z.coerce.number().default(30),
LOGIN_ATTEMPT_LIMIT: z.coerce.number().default(10),
TRUST_PROXY: z.string().default("false"),
OIDC_ENABLED: z
.enum(["true", "false"])