Skip to content

P2: PII filter and PAAC secret masking default to disabled (config + --init wizard), leaking vault secrets/PII to LLM providers and debug dumps #6263

Description

@bug-ops

Vulnerability

Two zeph-sanitizer defense-in-depth controls that specifically exist to prevent sensitive data from reaching third-party LLM providers and on-disk debug dumps ship disabled by default, both in zeph-config struct defaults and in the interactive --init setup wizard:

  • PiiFilterConfig::default()enabled: false (crates/zeph-config/src/sanitizer.rs:440-451)
  • SecretMaskingConfig::default()enabled: false (crates/zeph-config/src/sanitizer.rs:222-227)
  • src/init/security.rs:205-210 — PII filter prompt: Confirm::new()...default(false)
  • src/init/security.rs:380-385 — secret masking prompt: Confirm::new()...default(false)

Both controls are cheap: synchronous, regex-based (PII filter) or in-process placeholder substitution (secret masking) — neither requires an extra LLM call, unlike GuardrailFilter/QuarantinedSummarizer/TurnCausalAnalyzer, whose opt-in-by-default posture is defensible on cost grounds. There is no such cost justification for PII filtering or secret masking, yet an operator who presses Enter through every --init prompt (the common path) ends up with both protections off.

This directly conflicts with the project's own stated security posture. CLAUDE.md states: "ALL secrets and API keys are stored exclusively in the Zeph age vault ... never leak" and secret_mask.rs's own module doc frames PAAC masking as preventing "vault-resolved secrets from appearing in LLM payloads." With the shipped defaults, that guarantee does not hold unless the operator manually opts in.

Severity

Medium (P2) — real but bounded. Exploitation requires tool output (shell, MCP, file read, web scrape) to actually surface a raw secret or PII, which is a plausible but not universal occurrence (e.g. env, reading a config/.env-adjacent file, an MCP server response containing customer records, a script that echoes a resolved API key during debugging). When it happens, the secret/PII is sent verbatim to the external LLM provider and is also captured in cleartext in message-history persistence (SQLite) and in debug dumps (.local/testing/debug/, or the equivalent production debug-dump path) if enabled.

Location

  • crates/zeph-config/src/sanitizer.rs:439-451 (PiiFilterConfig::default)
  • crates/zeph-config/src/sanitizer.rs:221-228 (SecretMaskingConfig::default)
  • src/init/security.rs:205-210, src/init/security.rs:379-386

Attack Scenario

  1. Operator runs zeph --init, accepts default answers throughout (a very common setup path — the wizard has 15+ prompts and most operators keep the recommended true defaults, but these two land on false).
  2. Vault holds ZEPH_OPENAI_API_KEY etc. per CLAUDE.md's mandatory vault-only policy.
  3. During a session, a tool call (e.g. shell running env, or reading a .env-adjacent config file for debugging, or an MCP server that echoes a customer record containing an SSN/email) surfaces the raw secret or PII in its output.
  4. Because secret_masking.enabled = false, SecretMaskRegistry is never constructed (src/bootstrap/mod.rs:252-257 gates construction entirely on this flag) — the raw secret is sent unmasked to the external LLM API in the next turn's context, and is written unmasked into SQLite message history and any active debug dump.
  5. Because pii_filter.enabled = false, the same applies to any email/phone/SSN/credit-card pattern in the same tool output.
  6. Neither the LLM response nor the local log/debug-dump path recovers confidentiality after the fact — the data has already left the trust boundary.

Remediation

Pick one:

  • Flip both PiiFilterConfig::default().enabled and SecretMaskingConfig::default().enabled to true, and change the two --init prompts to .default(true), matching the treatment already given to skill_scan_on_load and skill_require_integrity_check_on_promote (both default(true) with a "(recommended)" prompt suffix) elsewhere in the same wizard file.
  • If a default flip is judged too risky for existing users on upgrade (behavior change, potential false-positive PII redaction noise), at minimum: (a) mark both prompts (recommended) like the other security prompts in the same file, and (b) add a config-migration step (per CLAUDE.md's migration-on-add-parameter rule) that emits a one-time startup warning when both remain disabled, so operators on existing configs are made aware of the gap rather than silently inheriting it.

References

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingllmzeph-llm crate (Ollama, Claude)securitySecurity-related issue

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions