7.3 KiB
Security Audit: User Preferences API
Summary
Overall Assessment: PASS
The User Preferences API feature demonstrates a well-structured, security-conscious implementation. No critical or high severity findings were identified. The code follows established hexagonal architecture patterns, uses parameterized SQL queries, validates all input at the domain layer, and enforces authentication and authorization boundaries correctly. A few low-severity observations are noted below for consideration.
Static Analysis Results
go vet
Status: Clean - no warnings or errors detected.
$ go vet ./...
(no output - all clear)
golangci-lint
Status: Not installed in environment. Manual review performed in lieu of automated linting.
Tests
Status: All 13 tests pass across 2 test suites (handler and service layers).
OWASP Assessment
| Category | Status | Notes |
|---|---|---|
| A01 - Injection | PASS | All SQL queries use parameterized placeholders ($1, $2). JSONB data is marshaled via Go's encoding/json and passed as parameters, not interpolated into SQL strings. No command execution or template rendering present. |
| A02 - Broken Authentication | PASS | JWT authentication enforced via auth.Middleware() on all preference endpoints. JWT secret sourced from environment variable (JWT_SECRET), not hardcoded. Token validation uses the shared auth package with proper expiry checking. |
| A03 - Sensitive Data Exposure | PASS | No secrets in code. JWT secret read from environment. Preferences contain only non-sensitive data (theme, language, notification toggle). Error messages do not leak internal details. No PII is logged beyond user_id. |
| A04 - XXE / Insecure Deserialization | PASS | No XML parsing. JSON unmarshaling uses Go's standard encoding/json with typed struct binding (app.BindAndValidate). Domain validation rejects unknown keys and invalid value types before persistence. |
| A05 - Broken Access Control | PASS | Every endpoint checks resource ownership via checkOwnership() which compares the authenticated user ID (from JWT context) against the {user_id} path parameter. Mismatch returns 403 Forbidden. Both GET and PUT perform this check before any data access. |
| A06 - Security Misconfiguration | PASS (with note) | Auth is conditionally enabled via AUTH_ENABLED env var (see Medium finding M1). No debug modes or permissive CORS in this service. Health endpoint is correctly excluded from auth. |
| A07 - XSS | N/A | Backend API only - no HTML rendering. JSON responses are safely serialized by Go's standard library. |
| A08 - Insecure Components | PASS | Dependencies are standard Go ecosystem packages (chi, google/uuid). No known vulnerable dependencies identified. |
| A09 - Insufficient Logging | PASS | Request logging via middleware (RequestLogger). Successful preference updates are logged with user_id. Auth failures logged by auth middleware. Recovery middleware catches panics. |
| A10 - SSRF | PASS | No user-controlled URLs or outbound HTTP requests. The service only accepts inbound requests and queries PostgreSQL. |
Critical Findings
None.
High Findings
None.
Medium Findings
M1: Auth Toggle Could Bypass Authentication in Production
Severity: Medium
Location: internal/api/routes.go:32, internal/config/config.go:31
The AuthEnabled config flag (controlled by AUTH_ENABLED env var) allows completely disabling authentication. If this is accidentally set to false or omitted in production, all preference endpoints become unauthenticated. Additionally, auth.MustGetUser() in the handler's checkOwnership() would panic when no user is in context, relying on the recovery middleware to return a 500 error.
Mitigation: This is a common pattern in this codebase for local development convenience. Ensure production deployment configurations explicitly set AUTH_ENABLED=true and that infrastructure safeguards (e.g., deployment manifests, CI checks) validate this setting.
Low Findings
L1: No Request Body Size Limit
Severity: Low
Location: internal/api/handlers/preferences.go:76
The PUT endpoint does not explicitly limit request body size. While the domain layer restricts preferences to 3 known keys, a malicious client could send a very large JSON body before validation occurs. The app.BindAndValidate() function may rely on framework defaults for body size limits.
Recommendation: Verify that the app package or middleware enforces a reasonable body size limit (e.g., 1MB). If not, consider adding http.MaxBytesReader at the handler or middleware level.
L2: No Rate Limiting on Preference Updates
Severity: Low
Location: internal/api/routes.go:42-43
There is no rate limiting on the PUT endpoint. A compromised or malicious client with a valid JWT could rapidly update preferences, generating unnecessary database writes.
Recommendation: This is acknowledged as out of scope in the spec. Rate limiting should be handled at the infrastructure level (API gateway/ingress) rather than per-service.
L3: User ID Logged on Update Without Sanitization Context
Severity: Low
Location: internal/service/preferences.go:55
The user_id is logged on successful update. While UUIDs are safe to log, the logging statement doesn't include the request ID for correlation. The middleware-level request logger handles this via context, but the service-level log entry may lack request tracing context depending on the logging configuration.
Recommendation: Ensure structured logging propagates request IDs through context automatically (verify logging.Logger behavior).
Recommendations
- Ensure
AUTH_ENABLED=truein production - Add a CI/deployment check that validates auth is enabled in production configurations. - Verify request body size limits - Confirm the
app.BindAndValidate()or middleware stack enforces a maximum request body size. - Add infrastructure-level rate limiting - Configure rate limits at the API gateway for the preferences endpoints.
- Consider domain-level unit tests - The domain validation logic (
ValidatePreferences,ValidatePreferenceKey,ValidatePreferenceValue) has no dedicated unit tests. While covered transitively through service tests, direct domain tests would improve coverage and serve as documentation.
Files Reviewed
| File | Purpose |
|---|---|
cmd/server/main.go |
Service entry point and wiring |
internal/config/config.go |
Configuration loading |
internal/domain/preferences.go |
Domain entity and validation |
internal/domain/errors.go |
Domain error definitions |
internal/port/preferences.go |
Repository interface |
internal/adapter/postgres/preferences.go |
PostgreSQL repository |
internal/service/preferences.go |
Business logic |
internal/service/preferences_test.go |
Service unit tests |
internal/api/handlers/preferences.go |
HTTP handlers |
internal/api/handlers/preferences_test.go |
Handler integration tests |
internal/api/handlers/health.go |
Health check handler |
internal/api/routes.go |
Route registration |
internal/api/spec.go |
OpenAPI specification |
migrations/001_create_user_preferences.sql |
Database migration |
migrations/migrations.go |
Embedded migration FS |