fix: security & privacy (rate limiter fix, csrf session cookie, logout on server, drafts stale token passw, login timing, password over 72b, JWT_SECRET complexity, SMTP TLS, CSV export neutralised, fonts bundled, upload metadata, hourly cleanup job, privacy.md report)
This commit is contained in:
1 parent
e2812ee0b9
commit
ea98c5ec05
118 files changed
+3828
-130
No files matched your search
@@ -0,0 +1,84 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user