Merge branch 'security/hardening-audit' into chore/consolidate-v2.0.0

This commit is contained in:
SnapOtter
2026-06-21 02:08:50 +08:00
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 { captureException, initAnalytics, shutdownAnalytics } from "./lib/analytics.js";
import { shouldRunStartupCleanup } from "./lib/cleanup.js"; import { shouldRunStartupCleanup } from "./lib/cleanup.js";
import { buildCsp } from "./lib/csp.js"; import { buildCsp } from "./lib/csp.js";
import { stripInternalPaths } from "./lib/errors.js";
import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.js"; import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.js";
import { logger } from "./lib/logger.js"; import { logger } from "./lib/logger.js";
import { requestDuration } from "./lib/metrics.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"); request.log.warn({ err: error, url: request.url, method: request.method }, "Request error");
} }
reply.status(statusCode).send({ reply.status(statusCode).send({
error: statusCode >= 500 ? "Internal server error" : error.message, error: statusCode >= 500 ? "Internal server error" : stripInternalPaths(error.message),
...(statusCode < 500 && { details: 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), MAX_BATCH_SIZE: z.coerce.number().default(0),
CONCURRENT_JOBS: z.coerce.number().default(0), CONCURRENT_JOBS: z.coerce.number().default(0),
MAX_MEGAPIXELS: 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"), DATABASE_URL: z.string().default("postgres://snapotter:snapotter@localhost:5432/snapotter"),
SQLITE_MIGRATE_PATH: z.string().default(""), SQLITE_MIGRATE_PATH: z.string().default(""),
FILES_STORAGE_PATH: z.string().default("./data/files"), FILES_STORAGE_PATH: z.string().default("./data/files"),
@@ -58,7 +58,7 @@ const envSchema = z
MAX_VIDEO_BITRATE_KBPS: z.coerce.number().default(0), MAX_VIDEO_BITRATE_KBPS: z.coerce.number().default(0),
LIBREOFFICE_TIMEOUT_S: z.coerce.number().default(120), LIBREOFFICE_TIMEOUT_S: z.coerce.number().default(120),
SESSION_DURATION_HOURS: z.coerce.number().default(168), 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"), TRUST_PROXY: z.string().default("false"),
OIDC_ENABLED: z OIDC_ENABLED: z
.enum(["true", "false"]) .enum(["true", "false"])
+11 -4
View File
@@ -33,12 +33,12 @@ services:
- MAX_WORKER_THREADS=${MAX_WORKER_THREADS:-0} - MAX_WORKER_THREADS=${MAX_WORKER_THREADS:-0}
- PROCESSING_TIMEOUT_S=${PROCESSING_TIMEOUT_S:-0} - PROCESSING_TIMEOUT_S=${PROCESSING_TIMEOUT_S:-0}
- MAX_PIPELINE_STEPS=${MAX_PIPELINE_STEPS:-20} - MAX_PIPELINE_STEPS=${MAX_PIPELINE_STEPS:-20}
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-1000} - RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-300}
- MAX_USERS=${MAX_USERS:-0} - MAX_USERS=${MAX_USERS:-0}
- SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168} - SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168}
- TRUST_PROXY=${TRUST_PROXY:-true} - TRUST_PROXY=${TRUST_PROXY:-true}
- DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter} - DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter}
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://:${REDIS_PASSWORD:-snapotter}@redis:6379
# 1.x upgrade: uncomment to import the old SQLite database on first boot; # 1.x upgrade: uncomment to import the old SQLite database on first boot;
# re-comment after the migration succeeds. # re-comment after the migration succeeds.
# - SQLITE_MIGRATE_PATH=/data/snapotter.db # - SQLITE_MIGRATE_PATH=/data/snapotter.db
@@ -121,6 +121,7 @@ services:
volumes: volumes:
- SnapOtter-pgdata:/var/lib/postgresql/data - SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped restart: unless-stopped
mem_limit: 1g
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter}"]
interval: 10s interval: 10s
@@ -131,12 +132,18 @@ services:
redis: redis:
image: redis:8-alpine image: redis:8-alpine
container_name: SnapOtter-redis container_name: SnapOtter-redis
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"] command: >-
redis-server
--maxmemory-policy noeviction
--maxmemory 512mb
--appendonly yes
--requirepass ${REDIS_PASSWORD:-snapotter}
volumes: volumes:
- SnapOtter-redisdata:/data - SnapOtter-redisdata:/data
restart: unless-stopped restart: unless-stopped
mem_limit: 1g
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-snapotter}", "--no-auth-warning", "ping"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
+11 -4
View File
@@ -32,12 +32,12 @@ services:
- MAX_WORKER_THREADS=${MAX_WORKER_THREADS:-0} - MAX_WORKER_THREADS=${MAX_WORKER_THREADS:-0}
- PROCESSING_TIMEOUT_S=${PROCESSING_TIMEOUT_S:-0} - PROCESSING_TIMEOUT_S=${PROCESSING_TIMEOUT_S:-0}
- MAX_PIPELINE_STEPS=${MAX_PIPELINE_STEPS:-20} - MAX_PIPELINE_STEPS=${MAX_PIPELINE_STEPS:-20}
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-1000} - RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-300}
- MAX_USERS=${MAX_USERS:-0} - MAX_USERS=${MAX_USERS:-0}
- SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168} - SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168}
- TRUST_PROXY=${TRUST_PROXY:-true} - TRUST_PROXY=${TRUST_PROXY:-true}
- DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter} - DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter}
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://:${REDIS_PASSWORD:-snapotter}@redis:6379
# 1.x upgrade: uncomment to import the old SQLite database on first boot; # 1.x upgrade: uncomment to import the old SQLite database on first boot;
# re-comment after the migration succeeds. # re-comment after the migration succeeds.
# - SQLITE_MIGRATE_PATH=/data/snapotter.db # - SQLITE_MIGRATE_PATH=/data/snapotter.db
@@ -113,6 +113,7 @@ services:
volumes: volumes:
- SnapOtter-pgdata:/var/lib/postgresql/data - SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped restart: unless-stopped
mem_limit: 1g
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter}"]
interval: 10s interval: 10s
@@ -123,12 +124,18 @@ services:
redis: redis:
image: redis:8-alpine image: redis:8-alpine
container_name: SnapOtter-redis container_name: SnapOtter-redis
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"] command: >-
redis-server
--maxmemory-policy noeviction
--maxmemory 512mb
--appendonly yes
--requirepass ${REDIS_PASSWORD:-snapotter}
volumes: volumes:
- SnapOtter-redisdata:/data - SnapOtter-redisdata:/data
restart: unless-stopped restart: unless-stopped
mem_limit: 1g
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-snapotter}", "--no-auth-warning", "ping"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
+13
View File
@@ -104,6 +104,18 @@ if [ -n "${DATABASE_URL:-}" ]; then
echo "Postgres is reachable." echo "Postgres is reachable."
fi fi
print_security_warnings() {
if [ "${DEFAULT_PASSWORD}" = "admin" ]; then
printf ' \033[33mWARNING:%b Default admin password is still "admin". Change it for any non-local deployment.\n' '\033[0m' >&2
fi
if echo "${DATABASE_URL:-}" | grep -q "snapotter:snapotter@"; then
printf ' \033[33mWARNING:%b Default Postgres credentials in use. Set POSTGRES_PASSWORD for production.\n' '\033[0m' >&2
fi
if echo "${REDIS_URL:-}" | grep -q ":snapotter@"; then
printf ' \033[33mWARNING:%b Default Redis password in use. Set REDIS_PASSWORD for production.\n' '\033[0m' >&2
fi
}
print_banner() { print_banner() {
RST='\033[0m' RST='\033[0m'
printf '\n' printf '\n'
@@ -114,6 +126,7 @@ print_banner() {
printf ' \033[33m➜%b Login \033[1m%s%b / \033[1m[CHANGE ON FIRST LOGIN]%b\n' "$RST" "${DEFAULT_USERNAME}" "$RST" "$RST" printf ' \033[33m➜%b Login \033[1m%s%b / \033[1m[CHANGE ON FIRST LOGIN]%b\n' "$RST" "${DEFAULT_USERNAME}" "$RST" "$RST"
printf ' \033[36m➜%b Docs \033[2mhttps://docs.snapotter.com%b\n' "$RST" "$RST" printf ' \033[36m➜%b Docs \033[2mhttps://docs.snapotter.com%b\n' "$RST" "$RST"
printf '\n' printf '\n'
print_security_warnings
} }
# Fix ownership of mounted volumes so the non-root snapotter user can write. # Fix ownership of mounted volumes so the non-root snapotter user can write.
+2 -2
View File
@@ -26,9 +26,9 @@ import {
// ── Env defaults ───────────────────────────────────────────────────────────── // ── Env defaults ─────────────────────────────────────────────────────────────
describe("Security: env defaults", () => { describe("Security: env defaults", () => {
it("LOGIN_ATTEMPT_LIMIT defaults to 30", () => { it("LOGIN_ATTEMPT_LIMIT defaults to 10", () => {
const env = loadEnv(); const env = loadEnv();
expect(env.LOGIN_ATTEMPT_LIMIT).toBe(30); expect(env.LOGIN_ATTEMPT_LIMIT).toBe(10);
}); });
it("RATE_LIMIT_PER_MIN is parsed correctly (test env overrides to 10000)", () => { it("RATE_LIMIT_PER_MIN is parsed correctly (test env overrides to 10000)", () => {