Files
homeclaw/SECURITY-CHANGES.md
T
2026-04-29 11:45:59 +09:00

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.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

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.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

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.<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.