Skip to content

LLMProxy — Threat Model & Security Whitepaper

A formal, code-grounded threat model for LLMProxy. Every control below maps to a real module in this repository (cited as path.py), and every coverage number is generated by an executable test — not a marketing claim. Where a defense is partial or gray-zone, this document says so: honest measurement is the point.

  • Methodology: STRIDE (per-category threat analysis) + OWASP LLM Top 10 (2025) mapping.
  • Living scorecard: OWASP_LLM_COVERAGE.md, regenerated by pytest tests/test_owasp_corpus.py on every build.

1. Scope & trust boundaries

LLMProxy is an OpenAI-compatible gateway that sits between untrusted callers and one or more upstream LLM providers. It is itself a security control plane, so its own attack surface is in scope.

        (untrusted)                    ┌─────────────── LLMProxy trust boundary ───────────────┐
 client ─── HTTP ──▶ ASGI byte firewall ─▶ auth/RBAC ─▶ SecurityShield ─▶ plugin rings ─▶ forwarder ─── HTTPS ──▶ upstream LLM
                     (firewall_asgi.py)   (rbac.py,     (security.py)      (plugin_engine)  (forwarder.py)          (provider)
                                           identity.py)
                                                          │                                        │
                                                   threat ledger,                            response signer,
                                                   session memory                            audit hash-chain
                                                   (threat_ledger.py)                         (response_signer.py)

Trust boundaries crossed: (a) network → firewall; (b) firewall → authenticated identity; (c) authenticated request → security inspection; (d) inspected request → upstream provider; (e) response → signed/audited before return. State stores (SQLite audit ledger, Redis rate-limit/circuit-breaker buckets) are inside the boundary. In scope: /v1/* inference, /api/v1/* admin, plugin loader, state stores. Out of scope: the upstream model's own behavior, the caller's host, and the build pipeline (covered separately by SBOM + provenance attestations in CI).


2. Defense in depth — the request pipeline

A request is inspected by independent layers; any one can reject it. This is deliberate: signature evasion at one layer is caught by scoring at the next.

#LayerModuleWhat it stops
L0ASGI byte firewallcore/firewall_asgi.py180 attack signatures scanned across 8 decoding layers (URL, Unicode-escape, Base64, hex, ROT13) with iterative chain-decoding — catches encoded/obfuscated known payloads before any Python object is built.
L1Auth + RBACcore/rbac.py, core/identity.pyAPI-key / OIDC-JWT (RS256) validation; per-role permission matrix; admin paths fail-closed.
L2Zero-trust identitycore/zero_trust.pyVerifies Tailscale device identity for network-level provenance.
L3Injection scoringcore/security.py, core/confidence.pyRegex threat patterns (incl. 7-locale multilingual) over both raw and homoglyph-normalized text → high-confidence hard block (≥ 0.85) or composite (regex + semantic-Jaccard + session-trajectory) → block / gray-zone escalate.
L4PII maskingcore/security.py (mask_pii)Reversible vault tokenization of emails, SSN, cards, IBAN, phones, API keys (regex + optional Presidio/ONNX NER).
L5Plugin ringscore/plugin_engine.pySandboxed pre/routing/post/background hooks; AST import scan, SHA-256 pin verification, path-traversal guard, allow-listed modules.
L6Trajectory + ledgercore/security.py, core/threat_ledger.pyMulti-turn "crescendo" detection per session; cross-session/IP threat accumulation.
L7Provenance + auditcore/response_signer.py, audit hash-chainHMAC response signing; tamper-evident append-only audit ledger (/api/v1/audit/verify).

3. STRIDE analysis

3.1 Spoofing (identity)

Threats: posing as a legitimate client or admin; forging upstream identity.

  • Controls: API-key verification (_verify_api_key) and OIDC/JWT with RS256 (identity.py); admin endpoints gated by _check_admin_auth (fail-closed when auth is enabled); Tailscale device verification (zero_trust.py) for network provenance; invalid-key attempts are logged as SECURITY events to the live feed.
  • Residual: static API keys remain a supported (simpler) mode; operators who choose it inherit key-management responsibility. OIDC is the hardened path.

3.2 Tampering (data / instructions)

Threats: prompt injection (OWASP LLM01); corrupting ledger/config state.

  • Controls: the L0/L3 layers above. Prompt-injection detection scans both raw and confusable-normalized text, so leetspeak, zero-width, Cyrillic-homoglyph, and native non-Latin scripts are all covered (see §4). Config edits from the UI are admin-only, validated (dry-run) before an atomic write, backed up, and audited (proxy/routes/admin.py). Volatile state (rate limits, breakers) lives in Redis with Lua-atomic updates to remove race windows.
  • Honest note: static pattern-matching is necessary but not sufficient against a probabilistic model. It is one layer; the composite score + optional AI gray-zone escalation exist precisely because no regex set is complete.

3.3 Repudiation

Threats: a malicious action (e.g. budget drain) with no provable trail.

  • Controls: EventLogger records SECURITY/SYSTEM events; the audit ledger is an append-only hash chain verifiable via /api/v1/audit/verify (a broken link is detectable); ResponseSigner HMAC-signs outgoing responses so a consumer can prove the payload transited the proxy unmodified.

3.4 Information disclosure

Threats: PII regurgitation; secret/stack-trace leakage.

  • Controls: vault-based PII masking (L4); Authorization headers scrubbed from all logs; the runtime config view (/api/v1/config/yaml) is secrets-redacted; FastAPI exception handlers return generic errors (no stack traces to clients); the Server banner is rebranded (no uvicorn fingerprint).
  • Compliance: GDPR DSAR export + Article-17 erasure (/api/v1/gdpr/*).

3.5 Denial of service

Threats: payload flooding; wallet-exhaustion.

  • Controls: Redis Lua-backed token buckets enforce per-IP/per-key rate limits at the ASGI edge (before the expensive pipeline); hard body-size + max-token caps; predictive FinOps budget gating returns HTTP 402 before a costly upstream call; ReDoS-resistant threat patterns (bounded quantifiers, no catastrophic backtracking — see core/security.py comment on _THREAT_PATTERNS).

3.6 Elevation of privilege

Threats: RCE via the plugin loader; path traversal on admin endpoints.

  • Controls: the plugin loader (plugin_engine.py) AST-scans source for forbidden imports (allow-list ALLOWED_MODULES), verifies a SHA-256 pin when the manifest records one, and blocks path traversal by resolving + containing every entrypoint under plugins_dir. RBAC (rbac.py) constrains what an authenticated role may do.
  • Residual (declared): the Python plugin path is trusted-code execution by design. Untrusted third-party plugins should use the WASM runtime path; the AST scan is a lint, not a sandbox. This is an explicit, documented trust assumption — not a claimed guarantee.

4. OWASP LLM Top 10 (2025) coverage

Full scorecard: OWASP_LLM_COVERAGE.md (regenerated every build).

CategoryCoverageNotes
LLM01 — Prompt Injection100 % (26/26)Direct, encoded (base64/hex/zero-width), leetspeak, role-play, suffix, multilingual (it/de/fr/es/pt/zh/ru), jailbreak-framing, tool-call injection.
LLM02 — Sensitive Info100 %Email · SSN · Visa · Amex · IBAN · phones · API keys.
LLM07 — System-Prompt Leakage100 % (6/6)Direct + indirect + continuation-bait + translation-trick + meta-instruction + persona-rebase.
Benign false-positive rate6 %18 controls incl. roleplay/fiction/multilingual benigns; only meta-discussion of attacks trips, on purpose.

LLM03/04/06/08/09/10 are out-of-scope for the proxy (build-time, training-time, caller-side, model-side) and are documented as N/A rather than silently claimed.


5. Measurement honesty (why this model is study-worthy)

The corpus harness (tests/test_owasp_corpus.py) evaluates each attack through the exact decision the live proxy makes in SecurityShield.inspect():

  1. firewall byte-signature scan, or
  2. high-confidence regex short-circuit (threat_score ≥ _HARD_BLOCK_SCORE), or
  3. composite calculate_confidence "block".

An "escalate" verdict — which needs an upstream model to adjudicate — is not counted as a deterministic block. Attacks that only AI-escalation would catch (e.g. bare refusal-suppression, which overlaps benign pleas) are flagged expected_pass_known_gap and reported in the "known gaps" table, never folded into the headline number.

This matters because an earlier version of the harness measured a helper (_check_injections, block ≥ 0.7) that the runtime doesn't use — inflating LLM07 to a reported 100 % when the live composite let 4 of 6 extraction attacks through. Aligning the harness to the runtime exposed the gap; the fix (the hard-block short-circuit) closed it. A security number you can't reproduce against the running system is theater. This model's numbers are reproducible.


6. Residual risks & roadmap

  • Probabilistic evasion: no static detector is complete against a model. Mitigation is layered (composite + AI gray-zone escalation) and measured, not assumed. Ongoing: expand the multilingual/technique corpus; local ONNX classifier in pre-flight (opt-in plugin already shipped).
  • Plugin trust: Python plugins are trusted code; untrusted extensions belong in the WASM runtime. The AST scan is a lint, not isolation.
  • Static API-key mode: supported for simplicity; OIDC is the hardened default for multi-user deployments.
  • Gray-zone attacks: documented, not hidden. AI escalation (when an upstream judge is configured) raises real-world block rate above the deterministic floor.

Reproduce every claim in §4/§5: pytest tests/test_owasp_corpus.py -v.

MIT License