Files
RenewCMS/features/security_privacy_hardening.qa.md
T

85 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QA: security and privacy hardening
Everything below runs in `task test` (package `test`, `-tags nofrontend`) unless marked manual.
## Cross-origin protection and cookie flags
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
3. `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`.
4. `TestSessionRevocationUseCase`: an already-expired session is not stored; an empty or 65-byte id
is `session.ErrInvalidSessionID`; 64 bytes is accepted.
5. `TestRevokedSessionRepository` on a migrated SQLite file: revoking twice is harmless,
`DeleteExpired(now)` removes rows whose `expires_at <= now` and reports the count.
6. `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.
7. `test/verified_account_required_http_test.go`: a verified user whose `token_version` moved on
only sees online articles.
## Passwords
8. `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.
9. `test/hasher_test.go`: an unknown username compares against a decoy hash once per attempt and
hashes the decoy once.
## Configuration and mail
10. `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.
11. `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
12. `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`.
13. `TestUploadStoresTheStrippedImage`: the stored bytes carry no marker and `Size`/`SHA256` match
them. `TestUploadRefusesAnImageWhoseContainerIsBroken`: nothing is stored.
## Retention
14. `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
15. `test/security_headers_test.go`: the CSP contains `font-src 'self';` and no Google host.
16. 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.