Code Review Checklist
This checklist guides reviewers through the security-relevant surface of the backend. All items below have been implemented and tested.
It has two parts: the production-hardening controls (transport, sessions, uploads, config) and the application layer added afterwards (accounts, profiles, social, messaging), whose risks are mostly authorization and information disclosure rather than infrastructure.
Tip
Each item points at the source location to review and what to confirm. For the narrative behind these controls, read SECURITY.md; for the function-level map, CODE_DOCUMENTATION.md.
Security Features Review
✅ CORS Protection
- Review:
internal/middleware/middleware.go-CORSMiddleware() - Check: Explicit origin allowlist enforced
- Check: Preflight OPTIONS requests handled
- Config:
.env.example-CORS_ALLOWED_ORIGINS
✅ Rate Limiting
- Review:
internal/middleware/middleware.go-RateLimitMiddleware() - Review:
internal/middleware/ratelimit_redis.go- Redis implementation - Check: In-memory eviction working (per-window cleanup)
- Check: Redis Lua script atomicity
- Config:
.env.example- Rate limit specs
✅ Request ID Tracking
- Review:
internal/utils/request_id.go- UUID generation - Review:
internal/middleware/middleware.go-RequestIDMiddleware() - Check: Request ID in all log entries
- Check: X-Request-Id header in responses
✅ Access Logging
- Review:
internal/middleware/middleware.go-AccessLogMiddleware() - Check: Structured logs include method, path, status, latency, IP, request ID
- Check: No duplicate logging (gin.Default() → gin.New())
✅ Security Headers
- Review:
internal/middleware/middleware.go-SecurityHeadersMiddleware() - Check: X-Content-Type-Options: nosniff
- Check: X-Frame-Options: DENY
- Check: Strict-Transport-Security (HSTS) builder
- Check: Referrer-Policy configured
- Check: Permissions-Policy configured
✅ CSRF Protection
- Review:
internal/middleware/middleware.go-CSRFMiddleware() - Review:
internal/api/handlers.go- Token generation inLogin(), removal inLogout() - Check: POST/PUT/DELETE require CSRF token
- Check: GET/HEAD/OPTIONS bypass CSRF
- Check: Bearer auth bypasses CSRF
✅ File Upload Security
- Review:
internal/api/handlers.go-UploadMedia() - Check: Size limit enforced with
http.MaxBytesReader - Check: MIME type allowlist validated
- Check: Magic bytes detected with
http.DetectContentType() - Check: Filenames sanitized (UUID-based storage)
- Check: Failed uploads cleaned up
✅ Session Security
- Review:
internal/service/auth.go-Login()method - Check: Prior sessions deleted in transaction on login
- Check: Session rotation atomic (DeleteAllUserSessions + AddSession)
- Check: Session cookies have HttpOnly, Secure, SameSite flags
- Review: Cookie configuration in
internal/api/handlers.go-sessionCookieConfig()
✅ Graceful Shutdown
- Review:
cmd/main.go- Signal handling and server.Shutdown() - Check: SIGTERM/SIGINT handled
- Check: In-flight requests complete before exit
- Check: Timeout prevents hanging
✅ Health Endpoints
- Review:
internal/api/handlers.go-/healthand/readyendpoints - Check:
/healthalways returns 200 - Check:
/readyreturns 200 when initialized, 503 during startup - Check:
/readydoes DB ping
✅ Error Normalization
- Review:
internal/api/handlers.go- Error mapping - Check: PostgreSQL 23505 (unique) → 400
- Check: PostgreSQL 23503 (FK) → 400
- Check: PostgreSQL 23514 (check) → 400
- Check: No stack traces sent to clients
- Check: User-friendly error messages
✅ Environment Validation
- Review:
cmd/main.go-loadConfig()function - Check: Required vars validated: DATABASE_URL, APP_PORT, MEDIA_STORAGE_DIR, SESSION_COOKIE_SECURE
- Check: Format validation: HSTS_MAX_AGE, TRUSTED_PROXIES CIDR, MIME types
- Check: Logical validation: Production + insecure cookies = fail
- Check: File permissions: MEDIA_STORAGE_DIR writable
- Check: External service validation: RATE_LIMIT_REDIS_URL format
✅ Trusted Proxies
- Review:
internal/api/routes.go-SetTrustedProxies() - Review:
cmd/main.go-validateTrustedProxies() - Check: IP addresses and CIDR blocks accepted
- Check: X-Forwarded-For trusted only from configured proxies
✅ Session Cleanup
- Review:
cmd/main.go-startSessionCleanup()job - Review:
internal/service/auth.go-DeleteExpiredSessions() - Check: Cleanup runs on startup
- Check: Periodic job (configurable interval)
- Check: SQL query in
internal/sql/query.sql
Application Layer Review
✅ Auth Token Handling (password reset, email verification)
- Review:
internal/service/tokens.go-newAuthToken(),hashToken() - Check: Only the SHA-256 digest is stored; the raw token never hits the database
- Check: Tokens are 32 bytes from
crypto/rand - Check: TTLs enforced in the query, not just at issue time (
passwordResetTTL1h,emailVerificationTTL24h) - Check: Issuing a new token invalidates the outstanding one
- Check:
DeleteExpiredAuthTokensis wired into the cleanup sweep incmd/main.go
✅ Account Enumeration Safety
- Review:
internal/api/authflows.go- all four handlers - Check:
/password-reset/requestreturns an identical response for known and unknown emails - Check:
/verify-email/resendreturns identically for unknown, already-verified, suspended, and deleted accounts (ResolveUnverifiedUser) - Check: No timing shortcut leaks existence (the work is the same on both paths)
- Check: Email send failures are logged, never surfaced to the caller
- Check: These routes sit behind the login rate limiter in
routes.go
✅ Session Revocation on Credential Change
- Review:
internal/service/profile.go-ChangePassword() - Review:
internal/service/tokens.go-ConfirmPasswordReset() - Check: Both delete all sessions for the account, in the same transaction as the password write
- Check: Both burn outstanding reset tokens
- Check:
ChangePasswordverifies the current password before doing anything - Check: The caller receives a fresh session so they are not logged out of their own request
✅ Optional Authentication
- Review:
internal/middleware/middleware.go-OptionalAuthMiddleware() - Check: A missing, malformed, expired, or non-active session falls through unauthenticated rather than erroring
- Check: It never grants a role — it only populates viewer context
- Check: No route relies on it for authorization (only
GET /posts/:slugandGET /users/:user, forliked_by_me/following)
✅ Comment Authorization & Moderation
- Review:
internal/service/comments.go - Check:
UpdateCommentis author-only; admins do not get edit rights - Check:
DeleteCommentallows admins to delete any comment, everyone else only their own - Check: Hidden and deleted comments are excluded from
ListPostComments - Check: A missing, hidden, or already-deleted comment returns
ErrCommentNotFoundrather than distinguishing the cases - Check:
ParentIDis validated to belong to the same post (ErrInvalidParentComment) - Check: Body bounded by
ValidateCommentBody(non-blank, ≤ 5000)
✅ Direct Message Authorization
- Review:
internal/api/messages.go,internal/service/messages.go - Check:
MessagesWithonly returns messages where the caller is sender or recipient - Check:
MarkMessagesReadis scoped to the caller as recipient - Check:
MessageResponse.Mineis derived per viewer rather than exposing raw participant IDs - Check: Self-messaging is rejected in the service and the schema
- Check: Body bounded by
ValidateMessageBody(non-blank, ≤ 2000)
✅ Notification Scoping
- Review:
internal/service/notifications.go - Check:
MarkNotificationReadis scoped byuserID, so one user cannot mark another's notification read - Check: Notifications are persisted before
publish, so a dropped live event is never a lost notification - Check: Emitters are fire-and-forget — a notification failure never fails the originating request
- Check:
NotifyFollowersOfNewPostresolves the owner from the post, not the caller (an admin publishing someone else's draft attributes correctly)
✅ SSE Stream (GET /events)
- Review:
internal/api/notifications.go-Events() - Review:
internal/realtime/hub.go - Check: The subscription's
cancelis deferred, so a dropped connection cannot leak a subscriber - Check:
Publishnever blocks — a full buffer drops the event rather than stalling the writer - Check: The stream is behind
RequireAuthMiddlewareand only ever receives events addressed to that user - Check: Write deadline cleared, heartbeat present,
X-Accel-Buffering: noset - Check: The single-instance limitation is understood before scaling out (see IMPLEMENTATION_SUMMARY.md)
✅ Role Model (reader / author / admin)
- Review:
internal/api/routes.go- group nesting - Check: Readers reach social and messaging routes but not the author group (post create/update/delete)
- Check: Media upload is intentionally open to all active accounts (avatars); attaching media to a post still requires the author role
- Check:
POST /registerrejects anyroleother thanauthororreader - Check: Readers cannot self-promote — role is never read from user input after registration
✅ Profile & Avatar Handling
- Review:
internal/api/profile.go-UpdateMe() - Check: Avatar media must be owned by the caller (no pointing at someone else's upload)
- Check:
PublicProfileResponseomits email, phone, and account status - Check: Free-text fields bounded by
ValidateProfileFields - Check:
resolveProfileTargetaccepts a username or UUID without leaking which users exist beyond a 404
✅ Outbound Email
- Review:
internal/mail/mail.go - Check:
Sendlogs the subject but never the body (bodies carry tokens) - Check: STARTTLS for standard ports, implicit TLS for 465
- Check:
nilmailer is safe everywhere (Enabled()false,Senda no-op) - Check: Deployment configures SMTP — without it, readers activate without verifying their address
✅ Search Input
- Review:
internal/api/social.go-SearchPosts(), and theSearchPostsquery - Check: The query text goes through the parameterized full-text query, never string interpolation
- Check: Published posts only — drafts must not be reachable via search
- Check: Results paginated with the shared limit cap
Configuration Review
-
.env.example- All environment variables documented - Check: Required vs optional clearly marked
- Check: Examples provided for each variable
- Check: Defaults documented where applicable
Documentation Review
-
SECURITY.md- Complete security guide- Each feature explained with code locations
- Verification steps provided
- Troubleshooting included
-
DEPLOYMENT.md- Production deployment guide- Prerequisites listed
- Setup instructions clear
- Reverse proxy examples (nginx)
- Rate limiting strategy explained
- Health checks documented
-
IMPLEMENTATION_SUMMARY.md- Implementation overview- Checklist items documented
- Files created/modified listed
- Known limitations stated
- Production checklist provided
-
API_REFERENCE.md- Every route inroutes.gohas an entry, and no entry describes a route that no longer exists -
CODE_DOCUMENTATION.md- New packages and exported symbols itemized -
DEVELOPMENT.md- Local setup still reproduces from a clean checkout
Code Quality Checks
- Go formatting:
go fmt ./...passes - Builds successfully:
go build -o app ./cmd/main.go - Tests pass:
go test -p 1 ./...with a disposableTEST_DATABASE_URL(see DEVELOPMENT.md) - Dependencies:
go mod tidyis clean
Security Audit
- SQL Injection: Parameterized queries via sqlc, including full-text search ✓
- Authentication: Session-based with rotation ✓
- Authorization: Role-based middleware, plus per-row ownership checks on comments, messages, notifications, and avatars ✓
- Input Validation: File uploads, env vars, and every free-text body (comment, message, bio, display name) ✓
- Output Encoding: JSON safe encoding; search excerpts use markdown emphasis rather than HTML ✓
- Cryptography: bcrypt for passwords, secure random for session/CSRF/auth tokens, SHA-256 at rest for auth tokens ✓
- Information Disclosure: Account-flow endpoints are enumeration-safe; public profiles omit email/phone/status ✓
- Error Handling: No stack traces to clients ✓
- Logging: Structured logs with context; email bodies (which carry tokens) are never logged ✓
- CSRF: Cookie-based token validation ✓
- CORS: Explicit origin allowlist ✓
Integration Points
-
Redis: Optional for multi-instance rate limiting
- Configuration validation ✓
- Lua script syntax correct ✓
- Error handling if Redis unavailable ✓
-
PostgreSQL: Database queries
- Session management queries ✓
- sqlc regeneration ✓
- Transaction safety ✓
Deployment Readiness
- Can be built:
go buildsucceeds ✓ - Environment variables comprehensive ✓
- Reverse proxy documentation included ✓
- Health checks documented ✓
- Monitoring guidance provided ✓
- Backup strategy documented (ops responsibility) ✓
- Troubleshooting guide complete ✓
Sign-Off
- Reviewer Name: _____________________
- Date: _____________________
- Approved for Production: [ ] Yes [ ] No
- Comments: _____________________
Notes for Reviewers
- Rate Limiting Strategy: In-memory works for single-instance; Redis needed for multi-instance.
- CSRF Protection: Cookie-based; works with browser clients. Bearer tokens bypass CSRF.
- Session Rotation: Atomic transaction prevents race conditions.
- Error Messages: User-friendly for client, detailed in server logs with request ID.
- Trusted Proxies: Critical for accurate client IP detection and rate limiting.
- File Uploads: 4-layer validation (size, MIME header, magic bytes, cleanup).
- Uniform Responses Are Deliberate: The account-flow endpoints return the same body whether or not an account matched. If a change makes one of them more informative — a different message, status, or latency — that is a regression, not an improvement.
- Idempotent Social Actions: Like/unlike and follow/unfollow return whether anything changed rather than erroring on a repeat. Do not "fix" a double-tap into a 409.
- Realtime Is Best-Effort: Every pushed event exists as a row first. A dropped SSE event is acceptable; a missing notification row is not.
- SMTP Is Not Optional in Production: With it unconfigured, reader accounts activate without proving they own the address.
See SECURITY.md for detailed security documentation.