# LinkLog Security Audit **Assessment date:** 2026-08-26 **Scope:** Current LinkLog backend, web frontend, Firefox extension, SQLite persistence, SMTP and Mastodon integrations, Docker/Traefik deployment files, and automated tests. **Assessment type:** Source-code review. This is not a penetration test, dependency scan, container scan, formal threat-model sign-off, or production configuration certification. ## Executive Summary The current worktree contains strong security improvements: salted scrypt password hashing with legacy upgrade support, bearer-header authentication, hashed and expiring tokens, refresh-token rotation with device binding and family revocation, OTP recovery codes, encrypted newly written secrets, Mastodon SSRF controls, image decoding and re-encoding, reduced extension permissions, proxy-only production Compose, and append-only audit events. The following issues remain before an Internet-facing production release: 1. Logout still accepts a bearer token in a JSON body rather than using the standard `Authorization` header. 2. SMTP, Mastodon, setup, and some user-service errors return raw exception details to clients. 3. First-run setup is intentionally unauthenticated and lacks a bootstrap secret and application-level request-size controls. 4. Audit event details are serialized without defensive sanitization or size limits at the audit-service boundary. 5. The development secret fallback is not rejected at application startup in production. 6. Rate limiting is single-instance SQLite state, is not atomic under concurrency, and reset-mail issuance is not independently throttled. 7. Runtime verification, security headers, centralized audit export, retention, alerting, and dependency/container/security scanning remain incomplete. The application should remain behind the production reverse proxy, with real DNS/TLS, protected secrets, and restricted network access until these items are addressed. ## Verified Controls - Passwords use salted scrypt hashes; valid legacy SHA-256 hashes are upgraded on login. - Bearer authentication is centralized through `get_current_user` and `require_admin`. - Query-string authentication is not accepted by protected session endpoints. - Access tokens are short-lived by default; refresh tokens are hashed, separately expiring, device-bound, rotated, and family-revoked on reuse. - Logout, password changes, and password resets revoke session material according to the token lifecycle. - OTP enrollment provides ten one-time recovery codes; only hashes are stored. - Users can recover OTP with password plus a recovery code, and administrators can disable OTP for another user. - Newly written SMTP, Mastodon, OAuth, and OTP secrets are encrypted with an external Fernet key. - Mastodon instances are restricted to HTTPS public hostnames, unsafe resolved addresses are rejected, and redirects are blocked. - Avatar uploads are size-limited, decoded with Pillow, pixel-limited, fully loaded, and re-encoded as server-generated PNG. - Production Compose does not publish the application port and uses the external Traefik network; local direct access is separate. - The Firefox extension uses `activeTab`, session-scoped credentials, exact configured backend permissions, and a self-only extension-page CSP. - SQLite queries are parameterized and foreign-key enforcement is enabled. - An append-only `security_audit_events` table records actor, action, target, outcome, and details for major administrative and destructive operations. - The current automated backend suite passes 49 tests. ## Findings ### SA-001: Logout uses non-standard token transport **Severity:** High **Evidence:** `POST /api/auth/logout` accepts `{"token": ...}` in the JSON request body, and the web frontend sends the access token this way. **Impact:** Request bodies may be captured by debugging middleware, application logs, or monitoring systems. The endpoint also diverges from the bearer-header contract used elsewhere, increasing the chance of inconsistent token handling. **Recommendation:** Make logout use `Authorization: Bearer ` and revoke the authenticated token or its family server-side. If a compatibility period is required, support both forms temporarily, prefer the header, and remove the body form after client migration. Add tests proving body-only tokens are rejected once compatibility is removed. **Priority:** High. ### SA-002: Raw infrastructure errors are returned to clients **Severity:** High **Evidence:** SMTP and Mastodon routes interpolate exception text into `503`/`502` responses. Setup and email-address routes also expose mail-delivery exception text. **Impact:** Error responses can disclose SMTP hostnames, ports, TLS/library details, upstream response bodies, internal network information, or sensitive URL fragments. **Recommendation:** Log technical details server-side with a request/correlation ID and return a stable public message with a short reference ID. Redact credentials, authorization headers, reset tokens, OTP data, and secret-bearing URLs. Add tests asserting that representative exception text is absent from HTTP responses. **Priority:** High. ### SA-003: First-run setup is unauthenticated and lacks application-level body limits **Severity:** Medium/High for exposed fresh deployments **Evidence:** `/api/setup/configuration`, `/api/setup/test-mail`, `/api/setup/complete`, and `/api/setup/status` are available before an administrator exists. No global request-size middleware or bootstrap secret is enforced in the application. **Impact:** Anyone who can reach a fresh instance can overwrite pending setup values, attempt SMTP delivery, consume test-mail quota, and submit oversized request bodies. The setup design is necessary for provisioning but is unsafe when directly exposed. **Recommendation:** Require a one-time bootstrap secret supplied through the environment or console, or restrict setup to localhost/private management networking. Add bounded request models and a global body-size limit. Keep strict setup/test-mail throttling, audit setup actions, expire pending setup data, and disable setup routes after provisioning. **Priority:** High for Internet-facing fresh installations. ### SA-004: Audit details are not sanitized at the audit-service boundary **Severity:** Medium **Evidence:** `record_audit_event()` serializes caller-supplied `details` directly to SQLite. Current callers generally avoid secrets, but the service does not enforce that contract or bound nested values and event size. **Impact:** A future caller could persist passwords, tokens, OTP codes, SMTP credentials, sensitive URLs, or oversized data in the audit database. Audit records are durable and are not a suitable place for arbitrary request payloads. **Recommendation:** Use an allow-list of permitted detail fields per action, or recursively redact sensitive key names and URL query values. Bound string lengths and serialized event size. Add direct service tests with nested `password`, `token`, `secret`, and URL values and assert that they are redacted or rejected. **Priority:** Medium. ### SA-005: Production secret fallback is not fail-closed **Severity:** Medium **Evidence:** `Settings.secret_key` defaults to `dev-secret-key-change-me`, and `LINKLOG_DATA_ENCRYPTION_KEY` is validated when encryption is used rather than fully validated during startup. **Impact:** A deployment that omits required configuration can start with a known development secret or fail only when a protected feature is exercised. This creates dangerous configuration drift and complicates incident response. **Recommendation:** During startup, reject a missing or known development `LINKLOG_SECRET_KEY` when `APP_ENV=production`; validate minimum length and entropy. Require and validate the encryption key before startup when encrypted data exists or production requires it. Add configuration tests for fail-closed production behavior. **Priority:** Medium. ### SA-006: Login and reset-mail throttling are not distributed or atomic **Severity:** Medium/High in multi-instance deployments **Evidence:** Login failure state is stored in SQLite and keyed by a client-IP/email hash. The check and increment occur as separate operations. Failed login handling can also issue a password-reset email for a known verified account without an independent reset-mail cooldown. **Impact:** Concurrent attempts can overwrite counters, multiple application instances do not share reliable rate state, and reset-mail issuance can be abused to spam a user or consume SMTP resources. **Recommendation:** Use an atomic shared limiter such as Redis for multi-instance deployments, with both account and IP buckets. Add an independent per-account/IP reset-mail cooldown and monitoring. Treat trusted proxy headers explicitly when deriving client IPs. Add concurrency, proxy, OTP-failure, and reset-mail abuse tests. **Priority:** Medium/High for scaled or public deployments. ### SA-007: Security headers and global request policy are incomplete **Severity:** Medium **Evidence:** The application does not consistently install or test CSP, HSTS, `X-Content-Type-Options`, frame protections, `Referrer-Policy`, trusted hosts, or a global request-size limit. The extension CSP does not cover the web application. **Impact:** Browser defense-in-depth and resource exhaustion protections depend on external proxy configuration. A proxy configuration mistake can leave HTML, API, or media responses weaker than intended. **Recommendation:** Add a documented application or guaranteed-proxy policy and test headers on HTML, API, and media responses. Use `TrustedHostMiddleware` with explicit production hosts, `nosniff`, restrictive framing/referrer rules, HSTS only on HTTPS, and bounded request bodies. **Priority:** Medium. ### SA-008: Audit operations lack request correlation, retention, export, and alerting **Severity:** Medium **Evidence:** Audit events contain actor/action/target/outcome/details/time but no request ID, source context, retention policy, protected export, or alerting pipeline. **Impact:** Operators can inspect database events but cannot reliably correlate them with request logs, detect attacks promptly, or guarantee retention and tamper-resistant access controls. **Recommendation:** Add request IDs at middleware entry, export redacted events to protected logs or a security monitoring system, define retention and access controls, and alert on privilege changes, OTP resets, password resets, credential changes, refresh-token reuse, and destructive actions. **Priority:** Medium. ### SA-009: Dependency, container, secret, and runtime security verification is incomplete **Severity:** Medium **Evidence:** The repository runs functional tests and static syntax checks, but no dependency vulnerability scan, container scan, secret scan, authenticated dynamic test, or live Firefox extension workflow is part of the verified release path. **Impact:** Known vulnerable dependencies, image issues, accidental secret commits, proxy misconfiguration, and browser-runtime permission failures can reach release despite passing unit tests. **Recommendation:** Add CI jobs for Python dependency and license policy, container scanning, secret scanning, Compose rendering, authenticated dynamic API checks, and a Firefox smoke test covering permission grant, login, refresh, logout, and active-tab capture. **Priority:** Medium before public release. ## Residual Operational Requirements - Replace documentation hostnames with real DNS names and enforce HTTPS/TLS. - Keep production Compose proxy-only and verify the actual Traefik network and middleware in deployment. - Rotate legacy plaintext secrets and previously issued sessions after upgrades. - Protect and encrypt database/avatar backups; test restoration and token/session revocation. - Monitor failed logins, reset-mail volume, refresh-token reuse, OTP recovery, privilege changes, and destructive actions. - Review the Firefox extension against Mozilla Add-ons policy before signing. ## Verification Performed - `PYTHONPATH=. pytest -q`: **49 passed** at the start of this audit. - Static source review of backend APIs/services, frontend assets, extension manifest/scripts, Compose files, configuration, and tests. - Targeted searches for authentication, token, secret, upload, outbound-request, error, and audit-log paths. Functional tests demonstrate regression coverage only; they do not certify production security.