Files
RenewCMS/features/security_privacy_hardening.qa.md
T

4.9 KiB
Raw Blame History

QA: security and privacy hardening

Everything below runs in task test (package test, -tags nofrontend) unless marked manual.

  1. test/cross_origin_protection_http_test.go: wrap a counting handler in middleware.CrossOriginProtection("https://cms.example.com").
    • Refused with 403 and apperr.CrossOriginErrorMessage, handler not reached: a cross-site POST, a same-site POST from https://blog.example.com, a cross-site DELETE, and a POST with only Origin: https://evil.example.
    • Allowed: same-origin POST, Sec-Fetch-Site: none, no headers, cross-site GET and OPTIONS, a cross-site POST with Authorization: Bearer rcms_key, and a POST whose Origin is the public URL while Host is 127.0.0.1:8080.
    • With PUBLIC_URL ::not a url, a cross-site POST is still 403.
  2. test/session_revocation_http_test.go drives api.InitRoutes: the login cookie is HttpOnly, Secure and SameSite=Lax, and a cross-site POST /v1/auth/logout is 403 and revokes nothing.

Session revocation

  1. test/authtoken_test.go (TestAuthTokenRevocation, TestAuthTokenDecodeRejections): distinct jti per token; explicit jti kept; a revoked token gives ErrRevoked and is stored until its exp; other tokens stay valid; store read failure gives ErrInvalid; store write failure is returned by Revoke; no jti is a no-op; no exp refuses; tokens without exp or with a non-string jti are refused by Decode.
  2. TestSessionRevocationUseCase: an already-expired session is not stored; an empty or 65-byte id is session.ErrInvalidSessionID; 64 bytes is accepted.
  3. TestRevokedSessionRepository on a migrated SQLite file: revoking twice is harmless, DeleteExpired(now) removes rows whose expires_at <= now and reports the count.
  4. TestLogoutRevokesTheSession through api.InitRoutes: two logins, logout with the first, /me with it is 401 and with the second is 200; logout without a cookie is 200 and stores nothing; a failing write gives 500 with the cookie cleared; a failing read makes /me 401.
  5. test/verified_account_required_http_test.go: a verified user whose token_version moved on only sees online articles.

Passwords

  1. test/password_length_limit_test.go: 72 bytes accepted, 73 bytes and 37 × "é" refused by user.ValidatePassword; register, reset (valid code) and invite accept answer 400 with apperr.PasswordTooLongErrorMessage and change nothing; a wrong reset code still answers 403.
  2. test/hasher_test.go: an unknown username compares against a decoy hash once per attempt and hashes the decoy once.

Configuration and mail

  1. test/runtime_settings_from_env_test.go: empty, short, 31-byte and both example JWT_SECRET values fail with config.ErrWeakJWTSecret and mention openssl rand -hex 32; 32 bytes passes. SMTP_ALLOW_INSECURE reads ""/false as false, true/TRUE as true, and maybe fails naming the variable.
  2. test/smtp_mailer_test.go against the fake server on 127.0.0.2: no STARTTLS and no opt-out fails before MAIL FROM naming SMTP_ALLOW_INSECURE; with the opt-out the message is delivered in clear text; with the opt-out and credentials, nothing is sent.

Upload metadata

  1. test/image_metadata_stripping_test.go builds JPEG, PNG and WebP files carrying a GPS-like marker in EXIF, XMP, IPTC, comments, text chunks, MPF and trailing data. Assert the marker is gone, ICC is kept, pixels decode identically, the orientation-only EXIF appears only when the orientation is not 1 and sits in the documented place, WebP flags and RIFF size are correct, a plain WebP and an SVG come back unchanged, and each malformed container is ErrImageUnreadable.
  2. TestUploadStoresTheStrippedImage: the stored bytes carry no marker and Size/SHA256 match them. TestUploadRefusesAnImageWhoseContainerIsBroken: nothing is stored.

Retention

  1. test/personal_data_retention_test.go: the user and invite rules at their boundaries; one run over a seeded world removes exactly one expired revocation, one abandoned account and two invites; a second run removes nothing; a session store failure touches nothing else; a user or invite deletion failure returns the partial counts; the runner logs counts only when something was removed and logs failures.

Fonts

  1. test/security_headers_test.go: the CSP contains font-src 'self'; and no Google host.
  2. Manual: task build:web, open the back office with the network panel; no request leaves for fonts.googleapis.com or fonts.gstatic.com, and task visual:check -- log-in passes.

Manual verification

  • Behind the real reverse proxy, log in, copy the jwt cookie, log out, then replay the cookie with curl: /v1/auth/me answers 401.
  • With an SMTP relay that lacks STARTTLS on a non-loopback host, sending fails until SMTP_ALLOW_INSECURE=true is set.