24 KiB
title
| 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
- Role-Based: Admin (role=0) bypasses all checks
- Project-Based: Users assigned to projects via
project.users[] - Collection-Based: Per-collection permission sets with method restrictions
- Pre-Filters: Automatic query filters based on permission context
- 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:
- The standard method permission must be granted (
post,putordeletewithallow: true) - The
bulkflag must be set totruein 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 gehashtlogin.go:LoginCheck()— Login prüft perCheckPassword()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-AfterHeader bei Blockierung - Konfigurierbar via
config.yml(rateLimit-Sektion) und Environment-Variablen - Standardmäßig aktiviert, kann via
rateLimit.enabled: falsedeaktiviert 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üfungconfig/admin_token.go:GetAdminTokenInfo()— Admin-Token-Metadaten-Abfragecontroller/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:
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.ymlwith env-var references - Warn on startup if
jwtSecretdoes not use${...}syntax - Add
.gitignoreentry 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 jetztconfig.Server.API.IsSecureCookies() - Default
true→ Cookies werden nur über HTTPS gesendet - Für lokale Entwicklung (HTTP) kann
secureCookies: falsegesetzt 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).
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:
cmd := exec.CommandContext(timeoutCtx, command, cmdArgs...)
The dir option also allows setting arbitrary working directories:
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
diroption throughCleanFilePathvalidation - 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:
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:
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:
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:
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
CleanFilePathvalidation 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:
"$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
$regexto 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:
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:
// 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:
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
iatvalidation)
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:
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:
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:
// 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 ✅
- MongoDB Operator Whitelist: Filter operators are validated against an explicit allowlist (
filter_valid_functions.go), blocking dangerous operators like$where - Suexec Process Isolation: Projects run with dedicated UID/GID on Linux, providing filesystem-level sandboxing
- Permission System Depth: Multi-layered permission model (public → token → user → custom) with pre-filters and projection restrictions
- Hook Timeout Support: JS hooks have configurable timeouts to prevent infinite loops
- Audit Logging: Comprehensive audit trail for admin actions with token identification
- Bulk Operation Guard: Requires explicit
bulk: trueconfig, preventing accidental mass operations - File Path Sanitization:
CleanFilePathprovides directory traversal protection with configurable strictness - Error Code Tracking: Each error gets a unique code for correlation without exposing internals (when
response.error.messageis configured) - Env-Var Substitution: Secrets can be externalized via
${ENV_VAR}syntax - Admin Token Granularity: Fine-grained permission system with wildcards for server admin tokens
Weaknesses ❌
- Cryptographic:
MD5 password hashing✅ behoben (bcrypt), no salt ✅ behoben (bcrypt nutzt eingebauten Salt), weak password policy - Network Security: No TLS termination (expected to be behind proxy),
no rate limiting✅ behoben,Cookie Secure=false✅ behoben (konfigurierbar, Default: true) - Token Security:
Timing-attack-vulnerable comparisons✅ behoben (constant-time compare), query parameter leakage ℹ️ akzeptiertes Risiko, no revocation - Input Validation Gaps: No file upload size limits, filename not sanitized, ReDoS via
$regex - Execution Safety: Unrestricted shell commands and HTTP fetch in hooks (SSRF)
- 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 |
|---|---|---|---|
| ✅ ERLEDIGT — bcrypt + MD5-Migration implementiert | |||
| ✅ ERLEDIGT — Exponentielles Backoff Rate Limiting (IP + Username) implementiert | |||
subtle.ConstantTimeCompare for tokens |
✅ 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 |
|---|---|---|---|
| ℹ️ 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
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
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
permissions:
"x-api-key:${API_KEY}": # Use env vars for token values
methods:
get: true
filter:
public: true
Hooks Security
// 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" } };
}