# SmallClaw Security Hardening — Change Log > **Format:** Each entry records *what changed*, *where*, *why*, and *how to verify*. > This file is the running reference for a security update post. > Last updated: 2026-02-28 --- ## Overview SmallClaw is being hardened against the most common vulnerabilities reported in open-source agent frameworks. Changes are grouped by threat area from the SmallClaw Security Architecture document (v0.1). Addressed so far: - ✅ **Section 1.1** — Secret Vaulting (AES-256-GCM encrypted credential storage) - ✅ **Section 1.3** — Log Hardening (scrubber pipeline, SecretValue wrapper, secure logger) - ✅ **Credential migration** — Existing plaintext `oauth-openai.json` auto-migrates to vault on first run - ✅ **CRIT-01** — `/api/open-path` command injection fixed (execFile + path validation + auth) - ✅ **CRIT-02** — MCP stdio command allowlist + `shell:false` + env var sanitization - ✅ **CRIT-03** — `/api/approvals` auth bypass fixed (gateway auth + decision validation + audit log) - ✅ **HIGH-01** — All channel/search/hook tokens auto-migrate to vault on next config save - ✅ **HIGH-02** — `redactConfigForUI()` masks all keys matching `api_key|token|secret|password` before sending to browser - ✅ **HIGH-03** — Startup banner resolves vault references before presence check; key values never logged - ✅ **HIGH-04** — MCP env block sanitized — blocks PATH, NODE_OPTIONS, LD_PRELOAD, SHELL, and 12 other dangerous vars - ✅ **HIGH-05** — `shell.ts` workspace check replaced with proper `path.resolve + path.relative` confinement; absolute path scanner added - ✅ **MED-01** — `/api/memory/confirm` raw body logging fixed (sanitizeToolLog + auth) - ✅ **MED-02** — Session files scrubbed via `scrubSecrets()` before writing to disk Pending (next iterations): - 🔲 Section 1.2 — Scoped Token Lifecycle (TTL enforcement + rotation hooks) - 🔲 Section 1.4 — Egress Controls (domain allowlist at network layer) - 🔲 MED-03 — Zod schema validation on settings endpoints - 🔲 MED-04 — Rate limiting on `/api/chat` - 🔲 Section 2.x — Lethal Trifecta controls (data reach, input quarantine, outbound confirmation) --- ## Change 001 — Secret Vault (`src/security/vault.ts`) **Date:** 2026-02-28 **Threat addressed:** Credential leakage — plaintext keys, tokens stored on disk ### What changed New file: `src/security/vault.ts` Implements `SecretVault` — an AES-256-GCM encrypted key-value store for all credentials. Each entry is independently encrypted with a fresh IV (IV doubles as the PBKDF2 salt, 200,000 iterations, SHA-512). The vault master key lives at `.smallclaw/vault/vault.key` (chmod 600). Encrypted entries live at `.smallclaw/vault/vault.enc`. These two files are stored separately — compromising one does not yield the other. Key features: - `SecretValue` wrapper: plaintext is private (`#value`). `toString()`, `toJSON()`, and `util.inspect()` all return `"[REDACTED]"` — secrets cannot accidentally appear in logs or JSON serialisation. - `.expose()` is the only way to get the raw string, making accidental logging obvious in code review. - All vault reads/writes are appended to `.smallclaw/vault/vault-audit.log` with timestamp, action, key name, and caller tag. The secret value is never in the audit log. - `.rotate()` re-encrypts with a fresh IV while preserving the original TTL. - `.has()` checks existence without triggering a GET audit event. - Expired entries are lazily pruned on first access. ### Files changed | File | Change | |------|--------| | `src/security/vault.ts` | **New** — SecretVault, SecretValue, scrubSecrets() | | `src/security/index.ts` | **New** — barrel export | ### How to verify ```ts import { getVault, SecretValue } from './src/security/vault'; const vault = getVault('/path/to/.smallclaw'); vault.set('test.key', 'super-secret-value', 'test'); const s = vault.get('test.key', 'test'); console.log(s); // SecretValue([REDACTED]) console.log(String(s)); // [REDACTED] console.log(JSON.stringify({ secret: s })); // {"secret":"[REDACTED]"} console.log(s!.expose()); // super-secret-value ← only here // Check vault.enc is not plaintext // cat .smallclaw/vault/vault.enc → JSON with hex enc/iv/tag fields, no readable strings ``` --- ## Change 002 — Log Scrubber + Secure Logger (`src/security/log-scrubber.ts`) **Date:** 2026-02-28 **Threat addressed:** Credential leakage via logs; logs as injection surface ### What changed New file: `src/security/log-scrubber.ts` Implements `scrubSecrets(input: string): string` — a pipeline function that must be called on any string before it goes to a log sink or the UI. Pattern registry covers: - `Bearer ` (OAuth / API tokens) - `sk-<...>` (OpenAI-style API keys) - `AKIA<...>` (AWS access key IDs) - JWT header.payload.signature blobs - JSON/query-string fields named `api_key`, `token`, `password`, `secret`, `credential`, etc. - High-entropy string detector: any base64/hex blob > 32 chars with >= 20 unique characters is flagged as `[REDACTED-HE]` as a catch-all. Also implements `log` — a structured secure logger that: - Scrubs every argument before writing to stdout/file - Serialises objects via `JSON.stringify` before scrubbing (no raw object dumps) - Separates security events (`log.security()`) to `security.log`, never mixed into `app.log` - Supports `SMALLCLAW_LOG_LEVEL` env var (`debug`/`info`/`warn`/`error`) - Supports `SMALLCLAW_LOG_DIR` env var for log file location `sanitizeToolLog(toolName, data, maxChars)` utility for debug-logging tool call inputs/outputs: truncates large payloads AND scrubs secrets. ### Files changed | File | Change | |------|--------| | `src/security/log-scrubber.ts` | **New** — scrubSecrets, log, sanitizeToolLog | ### Why this matters The most common accidental credential leak pattern in agent frameworks is not `console.log(apiKey)` — it's `console.log('Tool result:', JSON.stringify(toolOutput))` where `toolOutput` happens to contain an API response with a credential field. The scrubber catches this even when the caller doesn't know the payload contains secrets. ### How to verify ```ts import { scrubSecrets } from './src/security/vault'; scrubSecrets('Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.abc.def'); // → 'Authorization: [REDACTED]' scrubSecrets('{"api_key": "sk-abc123456789012345678"}'); // → '{"api_key": "[REDACTED]"}' scrubSecrets('normal log message with no secrets'); // → 'normal log message with no secrets' (unchanged) ``` --- ## Change 003 — OAuth Token Storage Hardened (`src/auth/openai-oauth.ts`) **Date:** 2026-02-28 **Threat addressed:** Plaintext OAuth tokens in `credentials/oauth-openai.json` ### What changed **Before:** `saveTokens()` wrote a raw JSON file to `.smallclaw/credentials/oauth-openai.json` containing `access_token`, `refresh_token`, `api_key`, and `id_token` in plaintext. Anyone with filesystem access (another process, a compromised tool with read scope) could read all tokens. **After:** `saveTokens()` stores the token bundle via `SecretVault` under the key `openai.oauth_tokens`, AES-256-GCM encrypted at rest. The plaintext file no longer exists after first run. **Auto-migration:** `loadTokens()` now calls `migrateLegacyCredentials()` on every load. If the old `oauth-openai.json` exists, it is automatically moved into the vault and the plaintext file is deleted. Users do not need to re-authenticate. TTL: vault entry for OAuth tokens is set to 8 hours (tokens have their own `expires_at` field internally; the vault TTL is an outer safety net). Security events are emitted to `security.log` for migration, save, and clear operations. ### Files changed | File | Change | |------|--------| | `src/auth/openai-oauth.ts` | **Modified** — vault-backed token storage, auto-migration, security logging | ### How to verify 1. Before updating: note that `.smallclaw/credentials/oauth-openai.json` exists and is readable. 2. After updating and restarting SmallClaw: the file should be gone. 3. `.smallclaw/vault/vault.enc` should contain a `openai.oauth_tokens` entry with no readable token strings. 4. `.smallclaw/vault/vault-audit.log` should show `migration:oauth` and `oauth:save` entries. --- ## Change 004 — Secure Logger wired into Provider Factory (`src/providers/factory.ts`) **Date:** 2026-02-28 **Threat addressed:** Miscellaneous log hardening; consistent logging approach ### What changed `console.warn()` in the provider factory fallback path replaced with `log.warn()` from the secure logger. This ensures even the fallback path benefits from secret scrubbing. This is a small change but establishes the pattern: **all new code in SmallClaw must use `log.*` from `src/security/log-scrubber.ts` rather than `console.*`.** Existing `console.*` calls will be migrated progressively. ### Files changed | File | Change | |------|--------| | `src/providers/factory.ts` | **Modified** — `console.warn` → `log.warn` | --- ## What's Next The following are queued for the next session: ### Section 1.2 — Scoped Token Lifecycle - Per-connector token storage with individual vault keys (`connector..token`) - Rotation hook infrastructure (`vault.rotate()` is already implemented) - Short TTL enforcement per token type (1h action, 8h read-only) - Token revocation test harness ### Section 1.4 — Egress Controls - Domain allowlist in config (`tools.permissions.network.allowed_domains`) - Network-layer enforcement wrapper around `fetch` / outbound HTTP calls - Block internal network ranges from agent-triggered requests (SSRF prevention) - First-time domain alert to `security.log` ### Section 2.x — Lethal Trifecta - Path allowlists on file connector (already partially in config, needs enforcement) - Content quarantine / source tagging before LLM ingestion - Outbound action confirmation gate for irreversible actions - Session isolation (no cross-session persistent state by default) - Memory write approval for externally-sourced content --- *This log is maintained alongside the SmallClaw Security Architecture document (v0.1).* *Each entry here corresponds to a control in that document.*