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.
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
| 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 | 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 |
- 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
- 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 (SQLite) — no raw string concatenation
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) |
- 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
- 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
Security-relevant events are dual-written to structured stdout (for log aggregators) and to the SQLite 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
- Stack traces are suppressed in production (
NODE_ENV=production) - Internal server errors return a generic message to clients
- Optional Sentry integration for error tracking
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 |
The following configurations are recommended for production deployments:
- 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)
- 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
- 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
- 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
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