Files
whatshooked/security_issues.md
Hein a4eb2a175c
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled
CI / Test (1.23) (push) Has been cancelled
feat(api): add server-side pagination and sorting to event logs API
- 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
2026-02-20 21:51:09 +02:00

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