Skip to content

Security: ImChustTesting/ContentForge

Security

SECURITY.md

Security

ContentForge is self-hosted by design. The threat model and the controls follow.

Threat model

The dominant risk is leakage of the user's Anthropic API key. The key is worth real money to an attacker — both as spend on the user's account and as exfiltration of capability.

Out of scope:

  • An attacker with root on the user's box. (Game over for any self-hosted tool.)
  • Side-channel attacks against the Postgres host process.
  • Anthropic-side abuse of the key. ContentForge does not — and cannot — enforce policies on api.anthropic.com.

In scope:

  1. An attacker who obtains a Postgres dump or a backup of the data volume must not be able to recover the API key.
  2. The key must never appear in container logs, exception traces, or HTTP responses.
  3. Default deployment should bind only to localhost; remote exposure is the operator's call and risk.

Controls

API key encryption (at rest)

  • Algorithm: AES-256-GCM, 96-bit IV, 128-bit auth tag.
  • Key derivation: PBKDF2-HMAC-SHA256, 100,000 iterations, salt is contentforge-v{key_version}.
  • Master key: ENCRYPTION_KEY env var, 64 hex characters (32 bytes), generated by the operator with openssl rand -hex 32.
  • Storage layout: BYTEA column = IV (12) || ciphertext || tag (16).
  • Decryption: only happens in worker process memory at the moment of an Anthropic API call. Never logged, never returned in HTTP responses, never re-emitted.

The plaintext is held in memory for the lifetime of one outbound call. There is no persistence of the decrypted key beyond the heap of the calling Node process.

Master key rotation

# In your .env
ENCRYPTION_KEY_OLD=<your current 64-hex value>
ENCRYPTION_KEY=<your new 64-hex value>

docker compose run --rm app node src/scripts/rotate-key.js

The script re-encrypts every user_secrets row from old to new and bumps key_version. After it succeeds, remove ENCRYPTION_KEY_OLD from .env. The script is idempotent — safe to re-run if the previous run was interrupted.

Admin password

  • Algorithm: bcrypt, cost 12.
  • Reset: there is no email reset path. Run:
    docker compose run --rm app node src/scripts/reset-password.js <new-password>

Sessions

  • httpOnly cookies; sameSite=lax; secure only when BEHIND_TLS=true.
  • Server-side session storage in Postgres (connect-pg-simple), so a docker compose down does not log everyone out.
  • Default cookie name: cf.sid. Default lifetime: 7 days.

File uploads

  • Streamed to disk via busboy. Hard cap 2 GB.
  • Filename sanitization: extension whitelist (.mp4, .mov, .mkv, .webm); on-disk filename is server-controlled (source<ext>), not user-controlled.
  • Source files live under /data/uploads/<jobId>/; the directory is per-job and the job id is a server-generated UUID.

Outbound network

The only host ContentForge calls is api.anthropic.com. There is no telemetry. There is no phone-home. There is no analytics endpoint. If you put the deployment in an egress-controlled environment, allow only that host.

Inbound network

  • The dashboard binds to 0.0.0.0:3000 inside the container, mapped to 127.0.0.1:${APP_PORT} on the host by the default docker-compose.yml.
  • Exposure on a LAN is the operator's choice. Use a reverse proxy with TLS and access control if you do this. Set BEHIND_TLS=true so cookies are marked Secure.

SQL

All queries are parameterized via pg. There are no string-interpolated query bodies. Schema is version-controlled in db/migrations/ and applied at app boot.

Containers

  • Both app and worker run as the non-root node user.
  • The worker container additionally applies nice -n 19 ionice -c2 -n7 via entrypoint.sh to avoid starving other processes on shared boxes.

Audit invitation

Outside readers are explicitly invited to audit the encryption surface. The relevant files:

  • app/src/lib/encryption.js — KDF + AEAD primitives.
  • worker/src/lib/encryption.js — same logic, copy-pasted because the surface is small.
  • app/src/routes/auth.js — wizard "test connection" + storage path.
  • worker/src/lib/repo.jsgetDecryptedAnthropicKey decryption call site.
  • app/test/encryption.test.js — round-trip + tamper tests.

If you find a problem, please open a private security advisory on GitHub rather than a public issue.

Known weaknesses (documented honestly)

  • No password reset by email. The reset-password.js script requires shell access to the box.
  • No 2FA for the dashboard. Single-user, self-hosted; out of scope for v1.
  • The ENCRYPTION_KEY lives in the same .env as POSTGRES_PASSWORD. An attacker who reads .env reads both. Hardening that means a separate KMS, which is v0.2 territory at best.
  • Cookies are sameSite=lax, not strict. We accept the small CSRF surface in exchange for working "click a link" flows.

Reporting

Open a GitHub security advisory or email the maintainer. Please don't open public issues for security problems until a fix is available.

There aren't any published security advisories