Files
tibi-docs/src/content/docs/server/14-security.md
T

24 KiB
Raw Blame History

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

  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:

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

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).

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 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:

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 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:

"$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:

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 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:

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.


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 ✅

  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

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" } };
}