- update event logs API to support pagination and sorting via headers - modify event logs page to handle new API response structure - implement debounced search functionality for improved UX - adjust total count display to reflect actual number of logs
5.9 KiB
Security Issues — WhatsHooked
CRITICAL
1. Broken Row-Level Security — All JWT Users Get Admin Access
File: pkg/api/security.go:236-241
GetRowSecurity() checks if userID == 0 → returns empty filter (admin access). But Authenticate() always sets UserID: 0 for JWT auth (line 146). Every JWT-authenticated regular user has unrestricted access to all rows. RBAC is non-functional for JWT sessions.
2. IDOR — No Ownership Checks in /api/v1/query
File: pkg/api/server.go:506-548
handleQueryGet, handleQueryUpdate, and handleQueryDelete operate on records using only the user-supplied id with no user_id ownership check. Any authenticated user can read, update, or delete any other user's hooks, accounts, API keys, or sessions.
db.NewUpdate().Model(model).Where("id = ?", req.ID).Exec(...)
db.NewDelete().Model(model).Where("id = ?", req.ID).Exec(...)
3. user_id Spoofing in Create
File: pkg/api/server.go:440
if _, exists := req.Data["user_id"]; !exists {
req.Data["user_id"] = userID
}
Auto-inject only runs when user_id is absent. A user can supply any user_id in the request body and create resources owned by another user.
4. Hardcoded Default JWT Secret
File: pkg/config/config.go:161
cfg.Server.JWTSecret = "change-me-in-production"
If jwt_secret is omitted from config, this well-known default is used. Attackers can forge valid JWT tokens for any user ID and role including admin.
5. Unauthenticated Media File Serving
File: pkg/api/server.go:200
router.PathPrefix("/api/media/").HandlerFunc(h.ServeMedia)
ServeMedia is not wrapped with h.Auth(). All WhatsApp media (images, documents, audio, video) is publicly accessible without credentials.
HIGH
6. No Token Invalidation on Logout
File: pkg/api/security.go:129-133
Logout() is a no-op. Tokens remain valid for 24 hours after logout. The sessions table exists but is never used to validate or invalidate tokens.
7. Unauthenticated QR Code Access
File: pkg/api/server.go:202
router.PathPrefix("/api/qr/").HandlerFunc(h.ServeQRCode)
Any unauthenticated party who can reach the server can retrieve QR codes for any account_id and link their own device to a WhatsApp account.
8. No Rate Limiting on Login Endpoint
File: pkg/api/server.go:259
/api/v1/auth/login has no rate limiting or account lockout. Brute-force attacks are unrestricted.
9. CORS Misconfiguration
File: pkg/api/server.go:663-667
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
Allow-Origin: * with Allow-Credentials: true is an invalid combination (browsers reject it). The wildcard allows any origin to read API responses. If origin is later restricted, the credentials flag enables CSRF via CORS.
10. No Request Body Size Limit
File: All handlers
All handlers use json.NewDecoder(r.Body).Decode() without http.MaxBytesReader. Arbitrarily large request bodies can cause memory exhaustion.
MEDIUM
11. Internal Database Errors Leaked to Client
File: pkg/api/server.go:382, 499, 529, 543
http.Error(w, fmt.Sprintf("Query failed: %v", err), ...)
http.Error(w, fmt.Sprintf("Create failed: %v", err), ...)
Raw database errors including table names, column names, and constraint violations are returned to clients.
12. Config File Saved World-Readable
File: pkg/config/config.go:229
os.WriteFile(path, data, 0644)
Config contains secrets (JWT secret, DB password, API tokens, MQTT credentials). Permissions should be 0600.
13. Session Path Traversal Potential
File: pkg/api/server.go:461
req.Data["session_path"] = fmt.Sprintf("./sessions/%s", sessionID)
sessionID is derived from user-supplied account_id. A value containing ../ could point the session path outside the sessions directory. account_id is not sanitized.
14. Phase 1 Config Password Stored Plaintext
File: pkg/config/config.go:26-28
username and password in ServerConfig are stored as plaintext strings and compared directly. Config file leakage equals credential leakage.
LOW
15. JWT Stored in localStorage
File: web/src/lib/api.ts:61
localStorage.setItem("auth_token", token);
Accessible to any JavaScript on the page. Any XSS vulnerability results in full token theft. Prefer httpOnly cookies.
16. User Object Deserialized from localStorage Without Validation
File: web/src/lib/api.ts:92-93
return userStr ? JSON.parse(userStr) : null;
Parsed and trusted without server-side revalidation. A tampered value causes the UI to display incorrect role/permissions.
Summary
| # | Severity | Issue | Location |
|---|---|---|---|
| 1 | Critical | JWT users get admin row access | security.go:236 |
| 2 | Critical | IDOR in query update/delete/get | server.go:506-548 |
| 3 | Critical | user_id spoofing in create | server.go:440 |
| 4 | Critical | Default JWT secret in code | config.go:161 |
| 5 | Critical | Media served without auth | server.go:200 |
| 6 | High | No token revocation on logout | security.go:129 |
| 7 | High | Unauthenticated QR code access | server.go:202 |
| 8 | High | No login rate limiting | server.go:259 |
| 9 | High | CORS misconfiguration | server.go:663 |
| 10 | High | No request body size limit | all handlers |
| 11 | Medium | DB errors leaked to client | server.go:382,499,529 |
| 12 | Medium | Config file world-readable (0644) | config.go:229 |
| 13 | Medium | Session path traversal potential | server.go:461 |
| 14 | Medium | Plaintext Phase 1 password in config | config.go:26 |
| 15 | Low | JWT in localStorage | api.ts:61 |
| 16 | Low | Unvalidated localStorage user object | api.ts:92 |