10 KiB
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.jsonauto-migrates to vault on first run - ✅ CRIT-01 —
/api/open-pathcommand injection fixed (execFile + path validation + auth) - ✅ CRIT-02 — MCP stdio command allowlist +
shell:false+ env var sanitization - ✅ CRIT-03 —
/api/approvalsauth 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 matchingapi_key|token|secret|passwordbefore 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.tsworkspace check replaced with properpath.resolve + path.relativeconfinement; absolute path scanner added - ✅ MED-01 —
/api/memory/confirmraw 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:
SecretValuewrapper: plaintext is private (#value).toString(),toJSON(), andutil.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.logwith 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
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 <token>(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.stringifybefore scrubbing (no raw object dumps) - Separates security events (
log.security()) tosecurity.log, never mixed intoapp.log - Supports
SMALLCLAW_LOG_LEVELenv var (debug/info/warn/error) - Supports
SMALLCLAW_LOG_DIRenv 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
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
- Before updating: note that
.smallclaw/credentials/oauth-openai.jsonexists and is readable. - After updating and restarting SmallClaw: the file should be gone.
.smallclaw/vault/vault.encshould contain aopenai.oauth_tokensentry with no readable token strings..smallclaw/vault/vault-audit.logshould showmigration:oauthandoauth:saveentries.
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.<id>.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.