# 04A Application Security Audit **Date**: 2026-03-20 **Auditor**: Automated (Claude Opus 4.6) **Scope**: Waggle desktop app + local server — CSP, vault crypto, agent tools, input validation, sessions, connectors, dangerous patterns --- ## Findings Summary | Severity | Count | |----------|-------| | CRITICAL | 2 | | HIGH | 5 | | MEDIUM | 6 | | LOW | 4 | --- ## CRITICAL ### SEC-001: OAuth Refresh Tokens Stored Unencrypted in Vault Metadata - **Severity**: CRITICAL - **File**: `packages/core/src/vault.ts:156-161` - **Issue**: `setConnectorCredential()` stores the `refreshToken` in the `metadata` field, which is written to `vault.json` as **plaintext JSON**. Only the `value` (access token) is encrypted via `this.set()`. The metadata object — including `refreshToken`, `expiresAt`, and `scopes` — is persisted alongside the encrypted blob but is itself **not encrypted**. - **Impact**: An attacker with filesystem access can read `vault.json` and extract OAuth refresh tokens in cleartext. Refresh tokens are long-lived credentials that grant persistent access to user accounts (Google Calendar, GitHub, etc.) without requiring re-authentication. - **Fix**: Encrypt the refresh token as a separate vault entry (e.g., `connector:{id}:refresh_token`) or encrypt the entire metadata blob. At minimum, the refresh token must go through the same `encrypt()` path as the primary credential value. ### SEC-002: Server CSP Allows `unsafe-eval` and `unsafe-inline` for Scripts - **Severity**: CRITICAL - **File**: `packages/server/src/local/security-middleware.ts:24` - **Issue**: The security middleware CSP sets `script-src 'self' 'unsafe-inline' 'unsafe-eval'`. Both `unsafe-inline` and `unsafe-eval` completely defeat the purpose of CSP for script injection protection. This CSP header is sent on every API response. - **Impact**: If any XSS vector exists (e.g., via `dangerouslySetInnerHTML` in the UI, or a reflected value in an API response rendered by the WebView), an attacker can execute arbitrary JavaScript. `unsafe-eval` also permits attacks via `eval()`, `Function()`, and `setTimeout('string')`. - **Fix**: Remove `unsafe-eval` entirely. Replace `unsafe-inline` with nonce-based or hash-based CSP directives. If a library requires `unsafe-eval` (e.g., some markdown parsers), isolate it and document the necessity. The Tauri CSP (in `tauri.conf.json` line 41) correctly omits `unsafe-eval` from `script-src` — the server middleware should match. --- ## HIGH ### SEC-003: Vault Key File Has No Protection Beyond Filesystem Permissions - **Severity**: HIGH - **File**: `packages/core/src/vault.ts:56` - **Issue**: The AES-256-GCM encryption key is a randomly generated 32-byte value stored in `.vault-key` as hex. File permissions are set to `0o600` (owner read/write only), but on Windows this permission flag is ignored — any user on the machine can read the file. There is no key derivation from a user password (PBKDF2, scrypt, argon2), no OS keychain integration, and no hardware-backed key storage. - **Impact**: Any process or user on the same machine can read `.vault-key` and decrypt all vault contents (API keys, OAuth tokens, connector credentials). This is the master key for all secrets. - **Fix**: For desktop deployment: integrate with the OS keychain (Windows Credential Manager via `keytar`, macOS Keychain, Linux Secret Service). Alternatively, derive the key from a user-provided passphrase using PBKDF2 with 600k+ iterations or argon2id. Store only the derived key in memory, never on disk. ### SEC-004: Bash Tool Has No Command Restrictions - **Severity**: HIGH - **File**: `packages/agent/src/system-tools.ts:55-116` - **Issue**: The `bash` tool passes any command string directly to the system shell (`cmd.exe` or `/bin/sh`) without any filtering, sanitization, or sandboxing. While the confirmation gate (`confirmation.ts`) requires user approval for unknown/destructive commands, a malicious or jailbroken LLM response could craft commands that appear safe but are destructive (e.g., chaining with `&&` or `;` after a safe-looking prefix). - **Impact**: A prompt injection attack could cause the agent to execute arbitrary system commands — data exfiltration, malware installation, credential theft, or system destruction. The confirmation gate helps but relies on regex pattern matching that can be bypassed (e.g., `ls ; rm -rf /` would not match `DESTRUCTIVE_BASH_PATTERNS` because the pattern checks the start of the command). - **Fix**: (1) Run bash commands in a restricted sandbox (Docker container, firejail, or Windows Sandbox). (2) Add a denylist of dangerous binaries (`curl`, `wget`, `nc`, `powershell`, `certutil`) that cannot appear anywhere in the command, not just at the start. (3) Parse commands into AST before execution to detect chained operations. (4) Consider requiring approval for ALL bash commands, not just "unknown" ones. ### SEC-005: CORS Set to `origin: true` (Reflects Any Origin) - **Severity**: HIGH - **File**: `packages/server/src/local/index.ts:1122` - **Issue**: The local server registers CORS with `{ origin: true }`, which reflects back any `Origin` header. Additionally, the notifications SSE endpoint (`notifications.ts:87`) hardcodes `Access-Control-Allow-Origin: *`. While this is a localhost-only server, any malicious website opened in the user's browser can make authenticated cross-origin requests to the Waggle server. - **Impact**: A malicious website could call Waggle API endpoints (read vault secrets via `/api/vault/:name/reveal`, execute agent commands via `/api/chat`, read conversation history, access workspace data) from the user's browser session. The vault reveal endpoint has origin checking, but all other endpoints do not. - **Fix**: Restrict CORS origins to the known Tauri app origins (`tauri://localhost`, `http://localhost:1420`, `http://127.0.0.1:1420`). Remove the wildcard from the notifications endpoint. The chat endpoint (`chat.ts:535`) also reflects the origin header — it should be restricted. ### SEC-006: Approval Gate Auto-Approves After 5-Minute Timeout - **Severity**: HIGH - **File**: `packages/server/src/local/routes/chat.ts:685-691` - **Issue**: When the agent requests approval for a destructive operation (file write, git commit, capability install), the server waits for user response. If no response arrives within 5 minutes, the action is **automatically approved** (`resolve(true)`). This is intended to prevent infinite hangs but creates a security gap. - **Impact**: A prompt injection could trigger a destructive operation and then keep the LLM generating tokens (long response) for 5 minutes, after which the destructive tool call auto-executes without user consent. This effectively bypasses the entire approval gate mechanism. - **Fix**: Change the timeout behavior to **auto-deny** instead of auto-approve. Replace `resolve(true)` with `resolve(false)` on line 689. A hung approval should fail safe, not fail open. Users can always re-trigger the operation. ### SEC-007: Updater Public Key Is Empty - **Severity**: HIGH - **File**: `app/src-tauri/tauri.conf.json:56` - **Issue**: The Tauri updater configuration has `"pubkey": ""` — an empty public key. This means auto-update signature verification is disabled. The updater endpoint points to `https://github.com/marolinik/waggle/releases/latest/download/latest.json`. - **Impact**: If the GitHub account is compromised, or if a man-in-the-middle attack intercepts the update check (unlikely with HTTPS but possible with certificate compromise), a malicious update binary could be pushed to all users without signature verification. - **Fix**: Generate an Ed25519 keypair using `tauri signer generate`, set the public key in `tauri.conf.json`, and sign all release builds with the private key. This is required before any production release. --- ## MEDIUM ### SEC-008: `dangerouslySetInnerHTML` in ChatMessage Without Strict DOMPurify Config - **Severity**: MEDIUM - **File**: `packages/ui/src/components/chat/ChatMessage.tsx:168,211` - **Issue**: Assistant messages are rendered by converting markdown to HTML via `marked.parse()`, then sanitizing with `DOMPurify.sanitize(rawHtml)` using default configuration. Default DOMPurify allows `` in some configurations and does not restrict `
`, ``, or `