* docs: rebrand from image-only to multi-modality across docs and metadata SnapOtter expanded from image-only to 157 tools across 5 modalities (image, video, audio, document/PDF, data). Update all product-level copy, metadata, and i18n that still framed it as an image-only tool. - README, package.json, root llms.txt: multi-modality framing, 157 tools - OpenAPI info + tags, generated /llms.txt tagline (docs.ts) - VitePress docs site: hero, getting-started, architecture, security, deployment, configuration, developer, supported-formats - i18n: 10 product keys across all 21 locales (hero, app description, privacy notes, AI features, progress messages, getting-started) - web/demo/landing meta + privacy copy, COMMUNITY_GUIDE, .env.example Stale tool counts (53/50+/52/70+/35) corrected to 157 throughout. Database/container deployment claims left unchanged (out of scope). * docs: fix stale post-rebrand test assertions and README language list - tests/e2e-docs/homepage.spec.ts: assert the current docs homepage (file toolkit, 157 tools, 5 modalities) instead of the old image-only strings - tests/unit/api/docs-route.test.ts: sync the reproduced llms.txt tagline with docs.ts - README.md: 21 languages with the correct list (add Swedish and Chinese Traditional, drop Czech which is not supported) * docs: correct 2.0 architecture references (Postgres 17 + Redis 8, 3-container stack) The docs and metadata still described the 1.x stack (SQLite, single container, p-queue). Update them to the current 2.0 reality. - README: replace the broken single-container `docker run` quick-start with the real Docker Compose stack (app + Postgres 17 + Redis 8); fix the "no Redis, no Postgres" feature bullet - package.json: description no longer claims a single container - apps/docs: rewrite database.md for Postgres; configuration.md DB_PATH -> DATABASE_URL + REDIS_URL; architecture.md SQLite/p-queue/better-sqlite3 -> Postgres/BullMQ/pg and add media-engine + doc-engine; developer/security/deployment/docker-tags/getting-started/contributing compose examples now include postgres + redis; index.md + api/ai.md AI count 16 -> 19 - SECURITY.md: Drizzle (SQLite) -> (PostgreSQL) - landing: enterprise/FeatureHighlights single-container wording; TrustSignals/ToolGrid 150+ -> 157 (dynamic); Pricing/FAQ 15 -> 19 AI tools * docs(api): document all video, audio, document, and data tool endpoints in OpenAPI The spec covered only image tools; the Scalar UI and the generated /llms.txt and /llms-full.txt inherited that gap. Add the 104 missing tool endpoints so the API docs match the code. - Video: 29 endpoints (most long/async; auto-subtitles is AI) - Audio: 17 (transcribe-audio is AI) - Document/PDF: 36 (ocr-pdf is AI; conversions are long/async) - Data: 10 - Image: 12 newer tools (background-replace, blur-background AI; histogram/lqip-placeholder/sprite-sheet custom responses; barcode-generate uses a JSON body) Each schema is derived from the tool's Zod validator and executionHint (fast -> 200, long -> 202+SSE, AI adds 501 FeatureNotInstalledError, multi-file inputs as arrays), referencing the existing shared schemas. Tool path entries: 64 -> 168. Spec parses as valid YAML with no duplicate paths and only known $refs.
9.2 KiB
Security Policy
Supported Versions
Only the latest release of SnapOtter receives security updates. We recommend always running the most recent version.
| Version | Supported |
|---|---|
| Latest release | Yes |
| Previous releases | No |
Self-hosted deployments should subscribe to GitHub release notifications and upgrade promptly when security patches are published.
Reporting a Vulnerability
Do not open a public GitHub issue or pull request for security vulnerabilities.
To report a vulnerability, email contact@snapotter.com with:
- A description of the vulnerability and its potential impact
- Steps to reproduce or a proof-of-concept
- The affected version(s)
- Any suggested fix, if available
Response Timeline
| Stage | Timeline |
|---|---|
| Acknowledgment | Within 48 hours |
| Critical severity patch | Within 7 days |
| Non-critical severity patch | Within 30 days |
After acknowledging your report, we will keep you informed of our progress toward a fix. Once a patch is released, we will credit you in the release notes unless you prefer to remain anonymous.
Severity Classification
| Severity | Definition |
|---|---|
| Critical | Remote code execution, authentication bypass, data exfiltration without authentication |
| High | Privilege escalation, stored XSS, SQL injection, SSRF with internal network access |
| Medium | CSRF, information disclosure of non-sensitive data, denial of service |
| Low | Missing security headers on non-sensitive endpoints, verbose error messages |
Security Architecture
Authentication and Access Control
- Password hashing: scrypt with 32-byte random salt and 64-byte derived key
- Timing-safe comparison: All credential verification uses
crypto.timingSafeEqualto prevent timing attacks - Password policy: Minimum 8 characters with uppercase, lowercase, and numeric requirements
- Session management: Cryptographically random UUIDs, configurable expiration (
SESSION_DURATION_HOURS), automatic cleanup of expired sessions - Credential rotation: Password changes invalidate all other sessions and revoke all API keys for the affected user
- Brute-force protection: Per-endpoint rate limiting on the login route (
LOGIN_ATTEMPT_LIMIT) - API keys: Hashed with scrypt (same parameters as passwords), SHA-256 prefix index for O(1) lookup, optional expiration, scoped permissions
- Role-based access control: Hierarchical roles (admin > editor > user) with granular permissions. Escalation prevention blocks creating or promoting users above your own role. Last-admin and self-demote protections prevent lockout
Input Validation
- Image uploads: Magic-byte verification against a known format table, null-byte buffer detection, configurable megapixel limit (
MAX_MEGAPIXELS), configurable upload size limit (MAX_UPLOAD_SIZE_MB) - SVG sanitization: Strips DOCTYPE declarations (XXE prevention), removes
<script>tags,<foreignObject>elements, event handlers, and blocks dangerous URI schemes (javascript:,data:text/html,file:, external URLs inhref/xlink:href) - API validation: Zod schemas on tool routes and environment config; manual validation on auth routes
- Database queries: Parameterized via Drizzle ORM (PostgreSQL) — no raw string concatenation
HTTP Security
The following headers are set on all responses:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
X-XSS-Protection |
0 (modern best practice) |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Strict-Transport-Security |
max-age=31536000; includeSubDomains (production only) |
Content-Security-Policy |
Restrictive policy with default-src 'self' (production only) |
Rate Limiting
- Global rate limiting via
@fastify/rate-limit(configurable withRATE_LIMIT_PER_MIN) - Stricter per-route limits on authentication endpoints
- Static assets excluded from rate limiting
Container Security
- Non-root execution: Dedicated
snapotteruser and group created at build time. The entrypoint starts as root only to fix volume permissions, then drops privileges viagosu - Root prevention: PUID/PGID of 0 are explicitly rejected with a warning
- PID 1:
tinihandles zombie reaping and signal forwarding - Multi-stage build: Production image contains only runtime dependencies
- No baked credentials: Auth defaults (
AUTH_ENABLED,DEFAULT_USERNAME,DEFAULT_PASSWORD) are set at container runtime, never in image layers - Health check: Built-in
HEALTHCHECKinstruction with 30-second intervals - PUID/PGID support: Bind mount permission conflicts are resolved by remapping the runtime user to match host UID/GID
Audit Logging
Security-relevant events are dual-written to structured stdout (for log aggregators) and to the PostgreSQL database:
LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, PASSWORD_CHANGED, PASSWORD_RESET, USER_CREATED, USER_UPDATED, USER_DELETED, API_KEY_CREATED, API_KEY_DELETED, ROLE_CREATED, ROLE_UPDATED, ROLE_DELETED, SETTINGS_UPDATED, FILE_UPLOADED, FILE_DELETED
Error Handling
- Stack traces are suppressed in production (
NODE_ENV=production) - Internal server errors return a generic message to clients
- Optional Sentry integration for error tracking
Shared Responsibility Model
SnapOtter is a self-hosted application. Security is a shared responsibility between the SnapOtter maintainers and the deployer.
| Area | SnapOtter maintainers | Deployer |
|---|---|---|
| Application code | Patch vulnerabilities, follow secure coding practices | Keep SnapOtter updated to the latest release |
| Docker image | Publish hardened images with non-root user, minimal attack surface | Pull updates regularly, scan images with your own tooling |
| Dependencies | Monitor and update npm/pip dependencies | N/A |
| Authentication | Provide secure auth implementation (scrypt, RBAC, brute-force protection) | Change default credentials before production use, enforce strong passwords |
| TLS/HTTPS | Support TRUST_PROXY for termination at a reverse proxy |
Configure and maintain TLS certificates and reverse proxy |
| Network security | Bind to 0.0.0.0 for container flexibility |
Restrict network exposure with firewalls, do not expose port 1349 directly to the internet |
| Host OS | N/A | Patch and harden the host operating system |
| Secrets management | Never bake credentials into image layers | Manage env vars securely (Docker secrets, Vault, etc.), rotate the default admin password |
| Data backups | Store data in /data for easy volume mounting |
Implement backup and disaster recovery for the /data volume |
| Monitoring | Emit structured audit logs and health check endpoints | Collect logs, set up alerting, monitor /api/v1/health |
Hardening Checklist
The following configurations are recommended for production deployments:
Required
- Change the default admin password immediately after first login (enforced by
mustChangePasswordunlessSKIP_MUST_CHANGE_PASSWORD=true) - Place SnapOtter behind a TLS-terminating reverse proxy (nginx, Caddy, Traefik)
- Set
CORS_ORIGINto your specific domain(s) if cross-origin access is needed (default in production is same-origin only)
Strongly Recommended
- Set
RATE_LIMIT_PER_MINto an appropriate value for your workload (e.g.,60) - Set
MAX_UPLOAD_SIZE_MBto limit upload sizes (e.g.,50) - Set
MAX_MEGAPIXELSto prevent memory exhaustion from oversized images (e.g.,100) - Set
MAX_USERSto limit account creation - Set
SESSION_DURATION_HOURSto a value appropriate for your environment (default:168/ 7 days) - Set
LOGIN_ATTEMPT_LIMITto a low value (default:10) - Use named Docker volumes instead of bind mounts for the
/datadirectory - Run with explicit
PUID/PGIDmatching your host user
Additional Hardening
- Set
MAX_BATCH_SIZEto limit batch processing resource consumption - Set
MAX_PIPELINE_STEPSto limit pipeline complexity - Set
PROCESSING_TIMEOUT_Sto prevent long-running operations from monopolizing resources - Set
MAX_SVG_SIZE_MBto limit SVG upload sizes - Set
MAX_PDF_PAGESto limit PDF processing scope - Forward structured logs to a centralized log aggregator (audit events emit at
infolevel — do not setLOG_LEVELaboveinfoor audit stdout output will be suppressed) - Monitor the
/api/v1/healthendpoint with your infrastructure monitoring - Restrict Docker socket access if running alongside other containers
Dependency Management
- npm dependencies are locked via
pnpm-lock.yamlwith--frozen-lockfilein CI and Docker builds - Python dependencies are pinned to exact versions in the Dockerfile
- GitHub Dependabot or similar tooling is recommended for automated dependency update PRs
Disclosure Policy
We follow coordinated disclosure. After a fix is released:
- The vulnerability is documented in the GitHub release notes
- A CVE is requested for critical and high severity issues
- The reporter is credited unless they request anonymity