--- title: 'Security Audit — Tibi-Server' --- # Security Audit — Tibi-Server > **Audit-Datum:** 27. Februar 2026 > **Scope:** Vollständiges Security-Audit des tibi-server Quellcodes > **Methode:** Statische Code-Analyse aller sicherheitsrelevanten Komponenten --- ## Zusammenfassung | Schweregrad | Anzahl | Status | |-------------|--------|--------| | **KRITISCH** | 3 | Offen | | **HOCH** | 6 | Offen | | **MITTEL** | 7 | Offen | | **NIEDRIG** | 5 | Offen / Teilweise gelöst | | **Gelöst** | 1 | ✅ | --- ## Security Architecture ### Authentication Layer | Mechanism | Scope | Header/Cookie | |-----------|-------|---------------| | JWT (HMAC-SHA256) | User authentication | `X-Auth-Token` header | | Refresh Token | Token renewal | `tibi-refresh-token` cookie (HttpOnly, SameSite=Strict) | | Admin Tokens | Server-level CRUD, management & namespace collection access | `X-Admin-Token` header or `Authorization: Bearer` | | Token Permissions | API key access | Configurable header/query parameter | ### Authorization Layers 1. **Role-Based:** Admin (role=0) bypasses all checks 2. **Project-Based:** Users assigned to projects via `project.users[]` 3. **Collection-Based:** Per-collection permission sets with method restrictions 4. **Pre-Filters:** Automatic query filters based on permission context 5. **Projection Restrictions:** Limit visible fields per permission level ### Data Isolation - **Database:** Each project gets its own MongoDB database (`{prefix}_{namespace}`) - **Filesystem:** suexec UID/GID isolation (Linux, requires root) - **Realtime:** Each project has its own Hub instance ### Input Validation - Collection field validation (type, required, allowZero, in, eval) - MongoDB filter operator whitelist (dangerous operators blocked) - File path sanitization (`CleanFilePath`) - User input validation (username alphanumeric, password min 3 chars) ### Bulk Operation Security Bulk operations (`POST` with JSON array body, `PUT`/`DELETE` without `:id`) require **two** permission checks: 1. The standard method permission must be granted (`post`, `put` or `delete` with `allow: true`) 2. The `bulk` flag must be set to `true` in the object form of the permission Method permissions support two formats: - **Boolean shorthand:** `post: true` — allows single-document POST only (no bulk) - **Object form:** `post: {allow: true, bulk: true}` — allows both single and bulk POST This applies equally to PUT and DELETE. Without explicit `bulk: true`, bulk endpoints return **403 Forbidden**. This ensures that existing projects are not automatically exposed to bulk operations when upgrading. --- ## Findings ### CRITICAL #### ~~C1. MD5 Password Hashing~~ ✅ BEHOBEN (2025-02-27) **Location:** `internal/models/login.go`, `internal/models/user.go` **CVSS-Schätzung:** 9.1 (Kritisch) → **behoben** **Description:** User passwords wurden zuvor mit MD5 gehasht, was kryptographisch gebrochen ist. **Fix:** Passwort-Hashing wurde auf bcrypt (`golang.org/x/crypto/bcrypt`, DefaultCost) umgestellt mit abwärtskompatibler MD5-Migration: - `user.go:ValidateData()` — neue Passwörter werden mit bcrypt gehasht - `login.go:LoginCheck()` — Login prüft per `CheckPassword()` sowohl bcrypt als auch legacy-MD5 - Transparente Migration: bei erfolgreichem Login mit MD5-Hash wird automatisch auf bcrypt re-gehasht und in der DB aktualisiert - Neue Hilfsfunktionen: `IsBcryptHash()`, `CheckPassword()` (exportiert für Tests) - Unit-Tests: `TestIsBcryptHash`, `TestCheckPassword` + alle bestehenden Tests grün --- #### C2. No Rate Limiting on Login Endpoint — ✅ BEHOBEN **Location:** `internal/ratelimit/ratelimit.go`, `internal/controller/ratelimit_middleware.go`, `internal/controller/setup.go` **CVSS-Schätzung:** 8.8 (Hoch–Kritisch) **Description:** The `POST /api/v1/login` endpoint has no rate limiting or account lockout mechanism. Combined with MD5 password hashing and a minimum password length of only 3 characters, this makes the system highly vulnerable to brute-force attacks. **Impact:** An attacker can attempt unlimited login attempts without any delay, lockout, or CAPTCHA. With a 3-character minimum password, the search space is trivially small. **Lösung implementiert:** - In-Memory Rate Limiter mit exponentiellem Backoff (1s → 2s → 4s → ... → 60s Cap) - Duales Tracking: per IP und per Username (getrennt) - Automatischer Reset nach 15 Minuten Inaktivität - HTTP 429 + `Retry-After` Header bei Blockierung - Konfigurierbar via `config.yml` (`rateLimit`-Sektion) und Environment-Variablen - Standardmäßig aktiviert, kann via `rateLimit.enabled: false` deaktiviert werden - Hintergrund-Cleanup-Goroutine für abgelaufene Einträge --- #### C3. Admin Token Comparison Vulnerable to Timing Attacks — ✅ BEHOBEN **Location:** `internal/config/admin_token.go`, `internal/controller/admin_reload.go` **CVSS-Schätzung:** 7.5 (Hoch) → **behoben** **Description:** Admin tokens wurden mit Go's `==` Operator verglichen, was nicht constant-time ist und Timing-Angriffe ermöglicht. **Lösung implementiert:** - Alle 3 Token-Vergleichsstellen auf `crypto/subtle.ConstantTimeCompare()` umgestellt: - `config/admin_token.go:CheckAdminToken()` — Server-Admin-Token-Prüfung - `config/admin_token.go:GetAdminTokenInfo()` — Admin-Token-Metadaten-Abfrage - `controller/admin_reload.go:ProjectReload()` — Projekt-Admin-Token-Prüfung - Constant-Time-Vergleich verhindert Rückschlüsse auf Token-Inhalte durch Zeitmessung --- ### HIGH #### H1. Secrets in Plain Text Configuration **Location:** `config.yml`, `internal/config/server.go` **CVSS-Schätzung:** 7.5 **Description:** JWT secrets, SMTP passwords, and LLM API keys are stored in plain text in the configuration file. The shipped `config.yml` contains a hardcoded JWT secret: ```yaml api: jwtSecret: 237uiejakymxeou3zhakN # Hardcoded production secret in repo! ``` **Impact:** Anyone with read access to the repository or config file can forge JWT tokens, access SMTP credentials, and use LLM API keys. **Mitigation:** Environment variable substitution is available (`${ENV_VAR}` syntax). **Recommendation:** - Replace all hardcoded secrets in `config.yml` with env-var references - Warn on startup if `jwtSecret` does not use `${...}` syntax - Add `.gitignore` entry for production configs - Consider adding a vault integration --- #### H2. Token Authentication via Query Parameters — ℹ️ AKZEPTIERTES RISIKO **Location:** `internal/models/permission.go` **CVSS-Schätzung:** 7.0 **Status:** Akzeptiertes Risiko (bewusste Design-Entscheidung) + Timing-Attack-Fix **Description:** Token-based permissions check both headers and query parameters. Query parameters appear in server logs, browser history, referrer headers, and proxy logs, exposing authentication tokens. **Bewertung:** Die Query-Parameter-Unterstützung ist ein bewusstes Feature für Anwendungsfälle, in denen Header nicht gesetzt werden können (z.B. direkte URL-Aufrufe, Webhooks). Das Risiko ist dokumentiert und dem Betreiber bekannt. **Timing-Attack-Fix (2025-02-27):** Token-Vergleiche in `GetTokenPermissionSet()` wurden von `==` auf `subtle.ConstantTimeCompare()` umgestellt, um Timing-Angriffe auf Token-Werte zu verhindern. **Empfehlung für Betreiber:** - Token-Werte über Environment-Variablen konfigurieren (`"x-api-key:${API_KEY}"`) - Proxy/Loadbalancer so konfigurieren, dass Query-Parameter nicht geloggt werden - Wo möglich, Header-basierte Token-Übergabe bevorzugen --- #### H3. Refresh Token Cookie Missing Secure Flag — ✅ BEHOBEN **Location:** `internal/models/login.go`, `internal/config/server.go` **CVSS-Schätzung:** 7.0 → **behoben** **Description:** The refresh token cookie had `Secure: false` hardcoded. **Lösung implementiert:** - Neuer Config-Eintrag `api.secureCookies` (bool, Default: `true`) - Environment-Variable: `API_SECURE_COOKIES` - Beide `SetCookie`-Aufrufe (Login + Logout) verwenden jetzt `config.Server.API.IsSecureCookies()` - Default `true` → Cookies werden nur über HTTPS gesendet - Für lokale Entwicklung (HTTP) kann `secureCookies: false` gesetzt werden --- #### H5. SSRF via http.fetch in Hooks **Location:** `internal/hook/context_http.go` **CVSS-Schätzung:** 6.5 **Description:** The `http.fetch` and `http.fetchStream` functions in hooks allow requests to arbitrary URLs without restriction. A hook can fetch internal network resources (e.g. `http://169.254.169.254/` for cloud metadata, `http://localhost:27017/` for MongoDB). ```go request, err := http.NewRequest(method, url, reqBody) // No URL validation — any scheme, any host, any port ``` **Impact:** Server-Side Request Forgery (SSRF) allowing access to internal services, cloud metadata endpoints, and potentially the MongoDB instance. **Recommendation:** - Implement an optional URL whitelist/blocklist at config level - Block requests to private IP ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `127.0.0.0/8`) by default - Document SSRF risks for project administrators --- #### H6. Unrestricted Shell Command Execution in Hooks **Location:** `internal/hook/context_exec.go` **CVSS-Schätzung:** 6.5 **Description:** The `exec.command` function executes any binary without path or name restrictions. While arguments are passed as separate `args[]` (safe from shell injection), the command name is controlled by hook code: ```go cmd := exec.CommandContext(timeoutCtx, command, cmdArgs...) ``` The `dir` option also allows setting arbitrary working directories: ```go if dir, ok := options["dir"].(string); ok { cmd.Dir = dir } ``` **Mitigation:** Commands execute under project UID via suexec isolation. Default dir is project config directory. **Impact:** Arbitrary code execution on the host with project-level privileges. Without suexec (non-root deployment), commands run as the server process user. **Recommendation:** - Implement a command whitelist at the project config level - Restrict `dir` option through `CleanFilePath` validation - Log all command executions for audit purposes --- ### MEDIUM #### M1. Error Message Information Disclosure **Location:** `internal/utils/error.go`, `internal/dbapi/helper.go` **CVSS-Schätzung:** 5.3 **Description:** By default (`config.response.error.message` not set), the raw error message is included in API responses: ```go errorInResponse := errorString if config.Server.Response.Error.Message != "" { errorInResponse = config.Server.Response.Error.Message } ``` When `response.error.stack: true` is set, Go stack traces are exposed: ```go if config.Server.Response.Error.Stack { o["errorStack"] = errorStack } ``` **Impact:** Detailed error messages can reveal internal paths, database structure, Go type names, and stack traces. **Recommendation:** - Default to generic error messages in production - Only enable detailed errors when explicitly configured for development - Never expose stack traces in production --- #### M2. File Upload: No Size Limit or Type Validation **Location:** `internal/controller/collection.go:75-140` **CVSS-Schätzung:** 5.3 **Description:** File uploads via base64-encoded data in JSON have no size limit or content type validation: ```go if i := strings.Index(srcPrefix, ","); i > 0 && strings.HasPrefix(srcPrefix, "data:") && strings.Index(srcPrefix, "base64") > 0 { // validate as data ``` The MIME type is extracted from the `data:` URL but not validated against an allowlist. Any file type can be uploaded. **Impact:** - Denial of service via extremely large base64 payloads (decoded in memory) - Storage exhaustion attacks - Upload of executable files or malicious content **Recommendation:** - Implement configurable maximum file size limits - Add optional MIME type allowlists per collection field - Consider limiting request body size at the middleware level (Gin's `MaxMultipartMemory`) --- #### M3. File Upload: Filename Not Sanitized **Location:** `internal/controller/collection.go:119-121` **CVSS-Schätzung:** 5.3 **Description:** The original filename from the upload payload is used directly in the file path: ```go if filename, ok := fileObj["path"].(string); ok { path += "/" + filename } ``` While the `path` is within the upload directory tree, the `filename` value is not sanitized. A filename like `../../etc/cron.d/malicious` combined with upload path could potentially escape the intended directory. **Mitigation:** The upload path structure `uploadPath + "/" + id + "/" + field` provides some containment. The ID is server-generated. **Recommendation:** - Sanitize filenames: strip directory separators, use `filepath.Base()`, restrict to alphanumeric + allowed extensions - Apply `CleanFilePath` validation to constructed upload paths --- #### M4. ReDoS Risk via $regex Filter Operator **Location:** `internal/dbapi/filter_valid_functions.go:28` **CVSS-Schätzung:** 5.3 **Description:** The `$regex` operator is allowed in user-provided query filters: ```go "$regex": true, ``` MongoDB's `$regex` uses PCRE, which is vulnerable to catastrophic backtracking with malicious patterns. An attacker can craft regex patterns that cause exponential CPU usage on the database server. **Impact:** Denial of service via CPU exhaustion on MongoDB. **Recommendation:** - Set a timeout for MongoDB read operations - Consider restricting `$regex` to authenticated users only - Add regex complexity validation (max length, disallow nested quantifiers) - Document ReDoS risks --- #### M5. Prometheus /metrics Endpoint Unauthenticated **Location:** `internal/controller/setup.go:135` **CVSS-Schätzung:** 5.0 **Description:** The Prometheus metrics endpoint is publicly accessible without authentication: ```go r.GET("/metrics", gin.WrapH(promhttp.Handler())) ``` **Impact:** Exposes internal metrics including request rates, error rates, latency distributions, and potentially Go runtime metrics, providing reconnaissance data to attackers. **Recommendation:** - Add authentication (admin token or basic auth) to `/metrics` - Or restrict access via network-level controls and document this requirement --- #### M6. No Request Timeout Middleware **Location:** `internal/controller/setup.go` **CVSS-Schätzung:** 5.0 **Description:** A TODO exists for request timeout middleware that was never implemented: ```go // TODO: timeout middleware ``` **Impact:** Long-running requests (e.g. complex queries, large file uploads, slow hooks) can tie up server goroutines indefinitely, enabling resource exhaustion attacks. **Recommendation:** Implement `http.TimeoutHandler` or Gin timeout middleware with configurable duration (e.g. 30s default, configurable per collection). --- #### M7. JWT Token Contains Full User Object **Location:** `internal/models/login.go:95-103` **CVSS-Schätzung:** 4.3 **Description:** The JWT access token includes the full user object (minus password), including arbitrary `meta` data: ```go userCopy := *user userCopy.InsertTime = nil userCopy.UpdateTime = nil userCopy.Meta = nil tokenData = &userCopy ``` The `Meta` is set to `nil`, but `Permissions`, `LLMBudgets`, and other fields are included. The token is not encrypted, only signed. **Impact:** Token leakage exposes user metadata and internal permission structures. Tokens are also long-lived (60 minutes) without revocation support. **Recommendation:** - Minimize token claims to `{id, username, role}` - Load full user data from DB when needed - Consider shorter token lifetime with more frequent refresh - Implement token revocation (e.g. via token blacklist or `iat` validation) --- ### LOW #### L1. Typo in Log Output (Unchanged) **Location:** `internal/controller/setup.go:62` **Description:** Log message reads `"senty gin activated"` instead of `"sentry gin activated"`. --- #### L2. Weak Password Policy **Location:** `internal/models/user.go:38` **CVSS-Schätzung:** 3.7 **Description:** Password minimum length is 3 characters: ```go Password *string `json:"password,omitempty" bson:"password" validator:"required,min=3"` ``` **Impact:** Extremely weak passwords are permitted, compounding the brute-force risk. **Recommendation:** Increase minimum to 8+ characters. Consider complexity requirements. --- #### L3. `allowUpperPaths: true` in Default Config **Location:** `config.yml:15` **Description:** The default config allows traversal above config directory: ```yaml security: allowAbsolutePaths: false allowUpperPaths: true ``` This enables `fs.readFile("../../etc/passwd")` from hooks (though constrained by suexec UID). **Recommendation:** Default to `false` for `allowUpperPaths` and only enable when explicitly needed. --- #### L4. Hook Cookie Functions Without Scope Restrictions **Location:** `internal/hook/context_cookie.go` **Description:** The `cookie.set` function in hooks can set cookies with arbitrary names, paths, domains, and security flags. A malicious hook could overwrite authentication cookies or set cookies for different domains. **Recommendation:** Document this as a trust boundary — hook code has the same trust level as server-side code. Consider restricting cookie domain to the current request's domain. --- #### L5. LLM Quota Race Condition **Location:** `internal/llm/quota.go:61-104` **Description:** LLM budget checking and recording happens in two separate operations (read, then write). Under concurrent requests, a user could exceed their token budget before the write propagates: ```go // 1. Check budget (read) if err := llm.CheckBudget(budgets, req.Provider); err != nil { ... } // 2. Use tokens (API call happens here) // 3. Record usage (write) _ = qm.RecordUsage(auth.ID, req.Provider, resp.Model, usage, budgets) ``` **Impact:** Minor over-budget usage under high concurrency; not exploitable for significant financial impact. **Recommendation:** Accept current behavior with documentation, or use MongoDB atomic `$inc` for budget tracking. --- ### RESOLVED #### ~~R1. Commented-Out Collection Count Endpoint~~ **Location:** `internal/controller/setup.go`, `internal/controller/collection_count.go` The dedicated `GET /_/:project/:collection/_count` endpoint is now implemented with full permission pipeline. --- ## Security Posture Assessment ### Strengths ✅ 1. **MongoDB Operator Whitelist:** Filter operators are validated against an explicit allowlist (`filter_valid_functions.go`), blocking dangerous operators like `$where` 2. **Suexec Process Isolation:** Projects run with dedicated UID/GID on Linux, providing filesystem-level sandboxing 3. **Permission System Depth:** Multi-layered permission model (public → token → user → custom) with pre-filters and projection restrictions 4. **Hook Timeout Support:** JS hooks have configurable timeouts to prevent infinite loops 5. **Audit Logging:** Comprehensive audit trail for admin actions with token identification 6. **Bulk Operation Guard:** Requires explicit `bulk: true` config, preventing accidental mass operations 7. **File Path Sanitization:** `CleanFilePath` provides directory traversal protection with configurable strictness 8. **Error Code Tracking:** Each error gets a unique code for correlation without exposing internals (when `response.error.message` is configured) 9. **Env-Var Substitution:** Secrets can be externalized via `${ENV_VAR}` syntax 10. **Admin Token Granularity:** Fine-grained permission system with wildcards for server admin tokens ### Weaknesses ❌ 1. **Cryptographic:** ~~MD5 password hashing~~ ✅ behoben (bcrypt), no salt ✅ behoben (bcrypt nutzt eingebauten Salt), weak password policy 2. **Network Security:** No TLS termination (expected to be behind proxy), ~~no rate limiting~~ ✅ behoben, ~~Cookie Secure=false~~ ✅ behoben (konfigurierbar, Default: true) 3. **Token Security:** ~~Timing-attack-vulnerable comparisons~~ ✅ behoben (constant-time compare), query parameter leakage ℹ️ akzeptiertes Risiko, no revocation 4. **Input Validation Gaps:** No file upload size limits, filename not sanitized, ReDoS via `$regex` 5. **Execution Safety:** Unrestricted shell commands and HTTP fetch in hooks (SSRF) 6. **Missing Middleware:** No request timeout, ~~no rate limiting~~ ✅ behoben (Login-Rate-Limiting), no body size limits --- ## Prioritized Remediation Plan ### Phase 1 — Critical (Immediate, 1-2 Weeks) | # | Finding | Effort | Impact | |---|---------|--------|--------| | ~~C1~~ | ~~Replace MD5 with bcrypt + migration path~~ | ~~Medium~~ | ✅ **ERLEDIGT** — bcrypt + MD5-Migration implementiert | | ~~C2~~ | ~~Add login rate limiting~~ | ~~Low~~ | ✅ **ERLEDIGT** — Exponentielles Backoff Rate Limiting (IP + Username) implementiert | | ~~C3~~ | ~~Use `subtle.ConstantTimeCompare` for tokens~~ | ~~Low~~ | ✅ **ERLEDIGT** — Alle Token-Vergleiche auf constant-time umgestellt | | H1 | Remove hardcoded JWT secret from config.yml | Low | Prevents token forgery from source | ### Phase 2 — High (2-4 Weeks) | # | Finding | Effort | Impact | |---|---------|--------|--------| | H3 | Make cookie `Secure` flag configurable | Low | ✅ **ERLEDIGT** — `api.secureCookies` Config-Eintrag (Default: true) | | L2 | Increase minimum password length to 8 | Low | Reduces brute-force surface | | M1 | Default to generic error messages | Low | Prevents info disclosure | | M6 | Implement request timeout middleware | Medium | Prevents resource exhaustion | ### Phase 3 — Medium (1-2 Months) | # | Finding | Effort | Impact | |---|---------|--------|--------| | ~~H2~~ | ~~Deprecate query-parameter tokens~~ | ~~Medium~~ | ℹ️ **AKZEPTIERTES RISIKO** — bewusste Design-Entscheidung, Timing-Fix angewendet | | H5 | Add SSRF protection for http.fetch | Medium | Blocks internal network access | | H6 | Add exec command whitelist config | Medium | Limits command execution | | M2 | Add file upload size/type validation | Medium | Prevents resource exhaustion | | M3 | Sanitize uploaded filenames | Low | Prevents path manipulation | | M4 | Add regex complexity limits | Medium | Prevents ReDoS on MongoDB | | M5 | Protect /metrics endpoint | Low | Hides internal metrics | | M7 | Minimize JWT token claims | Medium | Reduces exposure surface | ### Phase 4 — Hardening (Ongoing) | # | Finding | Effort | Impact | |---|---------|--------|--------| | L3 | Default `allowUpperPaths` to false | Low | Tighter default security | | L4 | Document hook trust boundaries | Low | Operational awareness | | L5 | Atomic LLM budget tracking | Low | Prevents minor over-use | --- ## Security Best Practices ### Configuration ```yaml api: jwtSecret: ${JWT_SECRET} # Always use env vars for secrets cors: allowOrigins: - https://yourdomain.com # Never use "*" in production allowCredentials: true security: allowAbsolutePaths: false allowUpperPaths: false # Restrict to config directory response: error: message: "An error occurred" # Generic error for production stack: false # Never in production ``` ### Permissions ```yaml permissions: public: methods: get: true # Minimal public access filter: published: true # Always pre-filter public data user: methods: get: true post: true put: true filter: author: $subst: expr: "auth.id" # Users only see own documents validProjections: - summary # Restrict visible fields ``` ### Token Permissions ```yaml permissions: "x-api-key:${API_KEY}": # Use env vars for token values methods: get: true filter: public: true ``` ### Hooks Security ```js // Always validate input var req = request(); var body = json.parse(req.body()); if (!body.email || body.email.indexOf("@") === -1) { throw { status: 400, json: { error: "invalid_email" } }; } // Use bcrypt for password hashing in custom auth flows var hash = bcrypt.hash(body.password, { cost: 12 }); // Sanitize exec commands var allowedCommands = ["convert", "ffmpeg", "python3"]; if (allowedCommands.indexOf(command) === -1) { throw { status: 400, json: { error: "command_not_allowed" } }; } ```