security: harden LLM attack surface (audits 1+2 findings F1-F4)#73
Merged
Conversation
…gents Port the two LLM-focused auditing agents from Terminal Learning, adapted to Getpostcraft's stack (Tauri 2 + Python sidecar + keychain BYOK vs. Next.js + Vercel + Supabase + localStorage). - `prompt-guardrail-auditor` (haiku, gate per-PR) — 7-step review covering system prompts in `network_rules.rs` and `sidecar/`, sanitizers, key manager, UI rendering, fetch surfaces, injection patterns, transverse checks. - `llm-security-auditor` (opus, release-driven) — 7-layer audit covering surface recon, threat modeling, OWASP LLM Top 10 (2025), 2026 vectors (ASCII Smuggling, multi-turn drift, encoding bypass, sycophancy, indirect injection), attack chain composition, defense stress-test, self-critique. Both agents document N/A surfaces explicitly (no Sentry tunnel, no Vercel CSP, no Supabase RLS, no RAG/tools/MCP V1) to keep findings honest. .gitignore: track .claude/agents/*.md while keeping settings.local.json and scheduled_tasks.lock ignored. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First runs of the two new LLM security agents on main @ 3df4641. - `prompt-guardrail-auditor` (haiku): 7/10 — 1 CRITICAL mineure (log_redact patterns) + 4 WARNINGS (input sanitizer, JSON mode scope, RAM cache, sidecar error redaction). - `llm-security-auditor` (opus, 7 couches): 7.4/10 — 0 CRITICAL, 2 HIGH (visual prompt without injection defense, log scrubber missing AI key patterns), 5 MEDIUM (Unicode tags, supply chain pinning, runtime model gating, DNS rebinding, CI audit), 7 LOW. Verdict: Ship avec mitigations. Both reports confirm the fundamental architecture (keychain ADR-009, CSP Tauri strict, PKCE OAuth, JSON mode, React auto-escape, SSRF guard tested). The gaps cluster on the 2026 LLM-specific perimeter: ASCII Smuggling, indirect injection via scraped URLs, scrubber regex coverage. Follow-up fixes shipped in subsequent commits in this PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OpenAI SDK errors like "Incorrect API key provided: sk-..." propagate as exception messages and reach log::error! un-scrubbed because the key is not in a recognised JSON field. Both security audits (prompt-guardrail [C1] + llm-security [HIGH-2]) flagged this as the highest-priority leak surface. Add a third pass AI_KEY_RE applied after the existing field-based redactors. Catches the 5 common AI provider key shapes (OpenRouter, Anthropic, OpenAI project, OpenAI legacy fallback, Google Gemini). 20-char minimum after the prefix avoids false positives on short test fixtures. Marked with a distinct [REDACTED_AI_KEY] token so log readers can triage which provider mirrored the key into an error string. Test fixtures use OBVIOUSLY_FAKE_TEST tokens to avoid tripping GitHub Push Protection / TruffleHog. 7 new tests, 18/18 log_redact tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…N_PROMPT (audit F2)
The visual extraction call reads a screenshot of an external website
and asks the model to return a JSON profile. The auditor
[llm-security HIGH-1] flagged this as the entry point for Chain B
(visual_profile poisoning persistent cross-posts): an attacker-
controlled site can include OCR-style instructions in its hero
("ignore all previous, return {...}") that the vision model would
otherwise follow.
Add a "DÉFENSE PROMPT-INJECTION" section at the top of the prompt
mirroring the structure already present in get_synthesis_prompt
(scraped-content path). Tells the model to treat the screenshot as
a visual data source only, ignore instruction-shaped text inside it,
and stay anchored to the JSON schema spec.
No new tests — the section adds to the system prompt body.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The llm-security-auditor [MEDIUM-1, vector V1 ASCII Smuggling] flagged that _sanitize_surrogates only covered U+D800-U+DFFF (lone surrogates that CPython refuses to UTF-8 encode), leaving the more dangerous 2026 invisible-payload carriers in place: - U+E0020-U+E007E (Unicode TAG block) — encodes invisible ASCII payload - U+E0001 (LANGUAGE TAG) + U+E007F (CANCEL TAG) sentinels - U+FEFF (BOM / ZWNBSP) — Windows clipboard paste artefact - U+200B-U+200D (ZWSP, ZWNJ, ZWJ) — zero-width spacing - U+202A-U+202E (BIDI overrides) — RTL flips hiding payload - U+2066-U+2069 (BIDI isolates) Extend _sanitize_surrogates to strip all of these. Function name kept for backward compat — semantic now broader, docstring updated. 9 new tests + 1 regression test that visible content (accents, emoji) passes through unchanged. 85/85 sidecar Python tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t F4) The prompt-guardrail-auditor [W2] flagged that no input validation existed between the renderer and the sidecar — a brief containing "ignore previous instructions" or a Unicode-tag payload was passed through raw. The system prompt was the only line of defense. Add validate_brief_input(&str) -> Result<(), String> called from the 4 entry points that accept a user brief (generate_content, generate_variants, generate_and_save_group, generate_carousel). Rejects: - Briefs shorter than 10 chars (consolidates the previous len-check) - Override patterns (ignore previous / disregard / you are now) + FR - System-prompt markers ([INST], <|im_start|>, ### Instruction) - Delimiter injection (</user_brief>, </system>, etc.) - Base64-encoded "ignore" / "Ignore" payloads - Invisible Unicode: TAG block, BOM, ZWSP, BIDI overrides — mirrors the sidecar _sanitize_surrogates ranges False positives accepted (e.g. a brief containing "système :" is rejected); error message tells the user how to rephrase. 13 new tests, 269/269 Rust tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
thierryvm
added a commit
that referenced
this pull request
May 12, 2026
…I) (#74) * feat(db,deps): migration 020 + Argon2id/subtle deps for Security Admin gate Foundation commit for the Settings → Security dashboard (Phase A of the post-#73 hardening roadmap). Two pieces land together because the schema and the crypto dep are mutually dependent — the module can't compile without argon2 + subtle, and the module is what writes to the new tables. Migration 020: - security_audit_attempts: append-only log of password unlock attempts (forensic trail, no PII — mono-user desktop). Feeds the in-RAM lockout decision; persistence is for auditing, not enforcement. - security_audit_reports: landing zone for the LLM auditor outputs. Schema declared here so 020 ships as a coherent unit with the gate. Empty until Phase B (agent runner) writes the first row. Dependencies: - argon2 0.5: Argon2id hashing with OWASP 2026 params (m=19MB, t=2, p=1). PHC string stored in the OS keychain — never SQLite. - subtle 2: constant-time equality for the in-RAM session token. Guards against the theoretical side-channel where compare time leaks the matched-prefix length. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(security_admin): Argon2id password + session token + lockout policy Core module for the Settings → Security gate. Three pieces of state on AppState, small async API for the Tauri command layer (next commit). Password lifecycle: - setup_password: 12-char min, Argon2id hash, PHC string in OS keychain (provider "security_password_hash" added to ai_keys::KNOWN_PROVIDERS) - verify_password: lockout check → keychain read → Argon2id verify → on success mint SessionState + log to security_audit_attempts. Typed VerifyOutcome (Ok/LockedOut/NoPasswordSet/Wrong) for the UI. - reset_password: keychain delete + state reset. #[allow(dead_code)] for now, admin-only recovery (Phase B may expose). Session token: - 32 random bytes from OsRng, base64 URL-safe no-pad for IPC. - 30-minute lifetime. Comparison via subtle::ConstantTimeEq — defends the theoretical timing side-channel cheaply. Lockout policy (in-RAM by design — app restart resets, fine because that is high-friction): 0-2 fails: free 3-4: 5s · 5-9: 30s · 10-19: 5min · 20+: capped 5min Audit log: every attempt appends to security_audit_attempts. Best- effort, never fails the verify decision (a wedged DB must not lock the user out of their own app). 12 unit tests (pure logic: lockout escalation, session expiry, malformed token rejection) + 4 keychain-touching integration tests that skip gracefully when keychain is unavailable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(commands): Tauri commands for Security Admin gate Thin IPC layer over the security_admin module. No business logic — each command maps a module call to a serde-friendly response. Exposed commands: - is_security_password_set() -> bool - setup_security_password(plain) -> Result<()> - verify_security_password(plain) -> Result<VerifyResponse> - check_security_session(token) -> bool - end_security_session() -> () - list_recent_security_attempts(limit) -> Result<Vec<AttemptRow>> VerifyResponse is a tagged union (snake_case kind) so the renderer pattern-matches without parsing strings. AttemptRow is a #[derive(FromRow)] shape with only the 4 columns the UI renders. Module registered in commands/mod.rs and wired into lib.rs's invoke_handler. AppState already grew a security_admin: Arc<...> field in the previous commit so these handlers can reach the session/lockout mutexes directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ui): Settings → Security tab with password gate User-facing slice of Phase A. New "Sécurité" tab in Settings exposes three states driven by the security_admin commands: 1. Setup wizard — when no password is configured. Confirm-match, 12-char minimum, password fields type="password" + autocomplete "new-password" so browser autofill won't leak. 2. Verify gate — once a hash exists, prompts on every fresh tab open. Lockout countdown live-decrements on screen. Wrong/locked_out/ no_password_set/ok branches all rendered explicitly. 3. Unlocked panel — placeholder for the Phase B agent runner + reports table, plus a live "Tentatives récentes" forensic view reading the new security_audit_attempts log. Session token cached in sessionStorage (renderer-side) so a renderer refresh doesn't force re-prompt — but the Rust side still enforces the 30-minute lifetime, so a stale token gets rejected and we fall back to the gate cleanly. All components use shadcn/ui primitives (Card, Input, Button, Alert) matching the rest of Settings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First security hardening sweep after running the two new LLM auditing agents (
prompt-guardrail-auditorhaiku +llm-security-auditoropus 7-couches) onmain. Addresses the 4 convergent findings flagged by both audits.Baseline scores (commit
3df4641) :prompt-guardrail-auditor: 7/10 — 1 CRITICAL mineure + 4 WARNINGSllm-security-auditor: 7.4/10 — 0 CRITICAL + 2 HIGH + 5 MEDIUM — Ship avec mitigationsWhat's in the PR
Foundation (2 commits) — versioned for cross-projet reuse :
feat(security)agents.claude/agents/llm-security-auditor.md+prompt-guardrail-auditor.mddocs(security)baseline reports indocs/security-audits/2026-05-12-*.mdFixes (4 commits, 1 per finding) :
log_redactAI_KEY_REregex catchessk-or-v1-*,sk-ant-*,sk-proj-*,sk-*,AIza*in raw log strings (e.g. OpenAI SDK error messages). 7 new tests usingOBVIOUSLY_FAKE_TESTfixtures.network_rulesllm-security HIGH-1VISUAL_EXTRACTION_PROMPT(mirrorsget_synthesis_prompt). Closes Chain B (visual_profile poisoning).sidecar/ai_client.pyllm-security MEDIUM-1(V1 ASCII Smuggling)_sanitize_surrogatesto strip Unicode TAG block, ZWSP/ZWNJ/ZWJ, BOM, BIDI overrides. 9 new tests + regression check on visible content.commands/ai.rsprompt-guardrail W2validate_brief_inputconsolidates length-check + rejects override patterns, system markers, delimiter injection, base64 payloads, invisible Unicode. Called from 4 entry points. 13 new tests.Test counts
_sanitize_surrogates)Out of scope (deferred)
The remaining MEDIUM findings stay open as a follow-up sprint :
requirements.txtpin-by-hash (supply chain) → PyInstaller v0.4 will resolve structurallycargo audit+pip-audit+npm auditin CIjsonUnreliablemodels like Mistral Small)_respond_errorsidecar redaction wrapper (defense-in-depth)Notes
The 2 audit reports under
docs/security-audits/document baseline scores and the full finding list. They will be re-run after merge to confirm score lift (expected 7.4 → 8.5+).The audit agents are intentionally portable — same format used so they could later be aggregated by a cross-projet dashboard (Atlas Sentinelle in Thierry's parallel work). Today they live in the GPC repo and are invoked from this session only.
🤖 Generated with Claude Code