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
This commit is contained in:
193
security_issues.md
Normal file
193
security_issues.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 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.
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```go
|
||||
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`
|
||||
|
||||
```ts
|
||||
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`
|
||||
|
||||
```ts
|
||||
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` |
|
||||
Reference in New Issue
Block a user