6.9 KiB
Security Audit: User Preferences API
Summary
Overall Assessment: PASS
The User Preferences API feature demonstrates solid security practices. No critical or high severity findings were identified. The implementation correctly enforces authentication via JWT middleware, performs authorization checks (own-user or admin), validates all inputs at appropriate layers, and avoids common vulnerability patterns. A few medium/low observations are noted for future hardening.
Static Analysis Results
go vet ./...: Clean — no warnings or errors- All tests passing: 30/30 tests pass across domain, adapter, service, and handler layers
- No
golangci-lintavailable in the environment;go vetwas the sole static analyzer run
OWASP Assessment
| Category | Status | Notes |
|---|---|---|
| A01: Broken Access Control | PASS | Authorization enforced in handler layer via authorizeAccess(). Users can only access own preferences unless they have admin role. Tested with forbidden and admin-access test cases. |
| A02: Cryptographic Failures | PASS | No sensitive data stored. Preferences contain no PII beyond user_id. JWT secret sourced from environment variable, not hardcoded. |
| A03: Injection | PASS | No SQL queries, command execution, or template rendering. All data is in-memory Go maps. JSON input is parsed via standard encoding/json with manual key allowlisting. |
| A04: Insecure Design | PASS | Hexagonal architecture cleanly separates concerns. Domain validation prevents invalid state. Merge semantics use pointer fields to distinguish present vs absent values. |
| A05: Security Misconfiguration | PASS (with note) | Auth is configurable via AUTH_ENABLED env var. When disabled, the authorizeAccess() function allows all access (returns nil when no auth user in context). This is the intended local-dev behavior but warrants documentation. No debug modes exposed. |
| A06: Vulnerable Components | PASS | Uses standard Go stdlib encoding/json, regexp, sync. External deps: chi/v5 (router), google/uuid (UUID parsing) — both well-maintained. |
| A07: Auth Failures | PASS | JWT middleware from pkg/auth handles token extraction and validation. Unauthenticated requests are blocked before reaching handler code (when auth is enabled). |
| A08: Software/Data Integrity | PASS | No deserialization of untrusted types. JSON decoding targets known structs with explicit field tags. Unknown fields in preferences are rejected via manual key allowlisting. |
| A09: Logging & Monitoring Gaps | PASS (with note) | Service layer logs successful upserts with user_id. However, failed authorization attempts are not explicitly logged (they return httperror.Forbidden which is handled by the framework). Failed validation is also not logged at the service level. |
| A10: SSRF | PASS | No outbound HTTP calls, no user-controlled URLs, no network access from this service. |
Critical Findings
None.
High Findings
None.
Medium Findings
M1: No Request Body Size Limit
Severity: Medium
Location: internal/api/handlers/preferences.go:117-123 (Update handler)
The httpresponse.DecodeJSON() call does not enforce a maximum request body size. Neither the handler nor the shared pkg/httpresponse package uses http.MaxBytesReader. An attacker could send an arbitrarily large JSON payload to exhaust server memory.
Risk: Denial-of-service via oversized request bodies.
Remediation: Apply http.MaxBytesReader(w, r.Body, maxBytes) before decoding, either in the handler or as framework middleware. A reasonable limit for preferences would be 64KB.
Note: This is a framework-level concern shared across all services, not specific to this feature.
M2: Auth Bypass When AUTH_ENABLED=false
Severity: Medium
Location: internal/api/routes.go:29, internal/api/handlers/preferences.go:183-192
When AUTH_ENABLED is false (default), the auth middleware is not applied. The authorizeAccess() function handles this by checking if user == nil { return nil } — allowing all requests through without any authorization check. This means any caller can read/write any user's preferences.
Risk: In a deployment where auth is accidentally left disabled, all preferences become world-readable/writable.
Remediation: This is the documented design for local development, but production deployments should enforce AUTH_ENABLED=true via deployment configuration or health-check validation. Consider logging a warning at startup when auth is disabled.
Low Findings
L1: Limited Audit Logging
Severity: Low
Location: internal/service/preferences.go:57
Only successful upserts are logged. Failed authorization, validation failures, and read operations are not logged at the application level (though framework middleware may capture HTTP-level access logs).
Remediation: Add structured logging for authorization denials and validation failures at the handler layer for operational visibility.
L2: Time Format Precision
Severity: Low
Location: internal/api/handlers/preferences.go:82
The UpdatedAt field is formatted with "2006-01-02T15:04:05Z" which drops sub-second precision. This is cosmetic but means two rapid updates in the same second would appear to have the same timestamp.
Remediation: Consider using time.RFC3339Nano or at least millisecond precision if needed for conflict detection in future.
Recommendations
- Add request body size limiting — Apply
http.MaxBytesReaderat the framework level or per-handler (Medium priority) - Log a warning when auth is disabled — Make it obvious in startup logs that the service is running without authentication (Medium priority)
- Add audit logging for authz failures — Log when authorization checks reject a request, including the requesting user ID and target user ID (Low priority)
- Document AUTH_ENABLED behavior — Ensure deployment runbooks require
AUTH_ENABLED=truein production (Low priority)
Files Reviewed
| File | Lines | Reviewed |
|---|---|---|
internal/domain/preferences.go |
113 | Yes |
internal/domain/errors.go |
11 | Yes |
internal/domain/preferences_test.go |
211 | Yes |
internal/port/preferences.go |
17 | Yes |
internal/adapter/memory/preferences.go |
50 | Yes |
internal/adapter/memory/preferences_test.go |
73 | Yes |
internal/service/preferences.go |
59 | Yes |
internal/service/preferences_test.go |
153 | Yes |
internal/api/handlers/preferences.go |
210 | Yes |
internal/api/handlers/preferences_test.go |
303 | Yes |
internal/api/routes.go |
42 | Yes |
internal/api/spec.go |
89 | Yes |
internal/config/config.go |
34 | Yes |
cmd/server/main.go |
39 | Yes |
pkg/auth/middleware.go |
234 | Yes (shared) |
pkg/auth/auth.go |
92 | Yes (shared) |
pkg/httpresponse/response.go |
193 | Yes (shared) |