Skip to content

Latest commit

 

History

History
191 lines (155 loc) · 22.1 KB

SECURITY.md

File metadata and controls

191 lines (155 loc) · 22.1 KB

Read this document in Portuguese (pt-BR).

Security Policy

Supported Versions

  • The table below lists which sqlite-graphrag versions currently receive security patches
  • Users on deprecated lines are STRONGLY encouraged to upgrade to a supported release
  • Upgrading early reduces exposure window and aligns with the coordinated disclosure policy
Version Status Security Patches
1.2.x Supported Yes, receives fixes (current line; latest v1.2.7 / crate 1.2.7)
1.1.x Supported Yes, receives critical fixes; upgrade to 1.2.x recommended
1.0.x Supported Yes, receives critical fixes; upgrade to 1.2.x recommended
0.x Unsupported No patches provided

v1.2.7 security-relevant notes

  • Database provenance is now published. Every side-effecting verb emits db_path_source and db_path_resolved in the envelope, so the caller can see which database a write actually landed in
  • Omitting --db does NOT use the current directory: the call silently falls back to the XDG database and exits 0. The provenance fields make that previously silent write to the wrong database visible

v1.2.6 security-relevant notes

  • An unknown key in --select or --filter is no longer silenced. Such a request used to degrade into an empty result set with exit 0; it is now rejected with exit 2
  • A --filter now evaluates the whole set instead of only the current page
  • A declared knob with no effect no longer returns exit 0
  • Security consequence: a caller can no longer mistake a malformed request for a legitimate empty result

v1.2.2 security-relevant notes

  • Failure envelopes are never filtered. The agent-native output surface (--filter, --select, --max-items, …) reshapes result rows only; an envelope carrying error: true or ok: false reaches the caller verbatim. A caller can therefore never be led to read a failure as an empty-but-successful result set
  • Truncation is never silent. --truncate-content and --max-output-bytes record what they removed under agent_surface and raise the top-level truncated flag; --max-output-bytes drops trailing elements rather than slicing JSON text, so a capped envelope still parses
  • --no-input refuses stdin declaratively. Every stdin reader fails up front with exit 65 instead of blocking, so an unattended invocation cannot hang waiting for input that will never arrive. Precedence: flag > XDG cli.no_input > false
  • A malformed --filter exits 2 rather than returning zero rows, so a typo is never mistaken for a legitimate empty result
  • Schema stays at v16 (no main-DB migration); the surface is additive and opt-in

v1.2.1 security-relevant notes

  • Namespace claim isolation on the enrich sidecar: dequeue_next_pending / count_eligible_pending / resume require operation and namespace, so a drain in one namespace cannot claim or process pending work from another (reduces cross-namespace processing risk and circuit-breaker fallout)
  • Schema stays at v16 (no main-DB migration); changes are sidecar queue behaviour only

v1.2.0 security-relevant notes

  • No product env (SQLITE_GRAPHRAG_*) on the hot path; runtime config is flag > XDG config set > default
  • DEFAULT_EMBEDDING_DIM=1024 (override via --embedding-dim / XDG embedding.dim; existing DBs keep schema_meta.dim)
  • GAP-SG-139: host surfaces accept --db as a documented no-op (config, slots, cache, completions); graph surfaces unchanged
  • Offline quality gate: scripts/e2e_offline_v120.sh

v1.1.05 Integrity Note — Atomic Envelope and Graph Writes (--output, merge, link)

  • v1.1.05 introduces deep-research --output PATH, which writes the full JSON envelope through the atomwrite algorithm (tempfile → fsync → rename) and emits a short ack on stdout carrying a blake3 checksum
  • The I/O contract holds: JSON on stdout, logs on stderr. Never redirect both into the same file with &> — that contaminates the JSON parse
  • The global --quiet/-q flag suppresses non-error tracing on stderr, cutting noise in agent pipelines without altering the envelope
  • Prefer link --from-id/--to-id when the identity is a numeric entity ID; purely numeric names are rejected by validate_entity_name (v1.1.05) so --create-missing cannot create phantom entities
  • Self-referential merges in merge-entities (target ID/name also listed as a source) are rejected BEFORE any database work (v1.1.05), protecting graph integrity against shell word-splitting
  • The shared helpers in src/atomic_io.rs (write_atomic, write_json_atomic) are unit-tested; the tests/v1105_incident_bugs_regression.rs suite covers the CLI path
  • Output-file integrity: check the blake3 checksum from the stdout ack against the written content when the pipeline needs end-to-end verification

Reporting a Vulnerability

  • OBRIGATÓRIO report security issues through GitHub Security Advisories in the public sqlite-graphrag repository as the preferred private channel
  • Use email at daniloaguiarbr@proton.me only as fallback when GitHub private reporting is unavailable
  • JAMAIS open a public GitHub issue, pull request, or discussion for security-related reports
  • Include a minimal reproduction, affected versions, and expected versus actual behavior
  • Include your environment details such as OS, architecture, and rustc version
  • Include CVSS 3.1 severity estimate when possible to accelerate triage

Response SLA

  • Triage of every advisory is committed to start within 72 business hours of submission
  • Initial acknowledgment email will be sent within that same 72-hour window
  • You will receive a case identifier and an assigned maintainer contact
  • Progress updates are shared at minimum every 7 days until resolution or public disclosure

Fix SLA by CVSS Severity

  • Critical severity (CVSS 9.0 to 10.0) receives a patch within 7 calendar days of validated triage
  • High severity (CVSS 7.0 to 8.9) receives a patch within 14 calendar days of validated triage
  • Medium severity (CVSS 4.0 to 6.9) receives a patch within 30 calendar days of validated triage
  • Low severity (CVSS 0.1 to 3.9) receives a patch within 90 calendar days of validated triage
  • Released fixes follow immediately with a CHANGELOG entry and a GitHub Security Advisory when the affected line is still supported

Disclosure Policy

  • We follow coordinated disclosure with a standard 90-day embargo window from initial report
  • The embargo can be shortened when a fix is released earlier than 90 days
  • The embargo can be extended when a fix demands more time and the reporter agrees
  • Public disclosure includes a CVE identifier when the impact warrants one
  • Public disclosure includes the GitHub Security Advisory with affected versions and patched version
  • Credit is attributed to the reporter unless anonymity is explicitly requested

Security Update Policy

  • Patches for supported versions ship as a new patch release on crates.io and GitHub Releases
  • Every release is validated with the full 10-command quality gate described in CONTRIBUTING
  • Advisory and license checks run LOCALLY by the operator with cargo audit and cargo deny check advisories licenses bans sources; the repository has no CI workflows, and cargo test is the only automatic gate
  • Supply chain is enforced via pinned constant_time_eq = "=0.4.2" to protect MSRV 1.88
  • Transitive dependency MSRV drift is monitored proactively per PRD policy

v1.0.76 OAuth-Only LLM Credential Enforcement — HISTORICAL, the guarded mechanism no longer exists

  • CURRENT SURFACE, measured on the 1.2.8 binary of this working tree: NOTHING in src/ starts a subprocess. Embedding and enrichment are HTTPS calls to OpenRouter, made in-process and one shot, selected by --embedding-backend openrouter|auto and --llm-backend openrouter|none.
  • CURRENT CREDENTIAL PATH: config add-key --provider openrouter --from-stdin stores the key at rest under XDG and keeps it out of shell history and out of the process table; --openrouter-api-key <KEY> overrides that store for a single invocation and IS visible in the process table, so prefer the store. config list-keys shows masked fingerprints, config remove-key deletes one, and config doctor reports which layer resolved the key. Product environment variables are not read at runtime and are not a configuration channel.
  • CURRENT RESIDUAL RISK, stated without invention: the key sits in an XDG file protected by filesystem permissions, and it is transmitted to the provider over TLS. There is no OAuth-only guard, no env-clear whitelist and no spawn boundary in this build, because there is no child process — the protections described in the rest of this section are NOT in force and must not be relied upon.
  • EVERY BULLET BELOW IS HISTORICAL. It records the subprocess era, removed in v1.2.0, and is kept as a register of what was built rather than as a guarantee. Verification: ANTHROPIC_API_KEY and OPENAI_API_KEY occur zero times in src/, and claude_runner.rs, codex_spawn.rs and ingest_claude.rs are not files of this repository any more.
  • HISTORICAL: the default build was LLM-only and one-shot, and every embedding call spawned a headless claude code or codex subprocess.
  • HISTORICAL: the spawn ABORTED with AppError::Validation and exit code 1 when ANTHROPIC_API_KEY or OPENAI_API_KEY was detected in the environment. No such abort exists today, and no code reads either variable.
  • HISTORICAL: the OAuth flow (Claude Pro/Max or ChatGPT Pro subscription) was the ONLY accepted credential mechanism. Today the accepted mechanism is an OpenRouter API key held at rest under XDG.
  • HISTORICAL: both API-key env vars were INTENTIONALLY ABSENT from the env-clear whitelist in claude_runner.rs, codex_spawn.rs and ingest_claude.rs, as defence in depth against a future refactor moving the guard. That defence disappeared together with the risk it covered: no child is started, so no variable can reach one.
  • HISTORICAL: the --bare flag (which would also demand an API key) was REMOVED from every executable path in v1.0.69.
  • HISTORICAL: four #[serial_test::serial(env)] tests validated the canonical flag set and the abort behaviour.
  • HISTORICAL RATIONALE: docs/decisions/adr-0011-oauth-only-enforcement.md for the full reasoning and docs/decisions/adr-0025-oauth-only-embedding.md for the v1.0.76 embedding-specific application.
  • Migration: an operator still relying on ANTHROPIC_API_KEY or OPENAI_API_KEY must move to an OpenRouter key, because since v1.2.0 neither variable is read by anything.

v1.0.83 Custom Provider Credential Preservation (ADR-0041)

  • HISTORICAL: the default build PRESERVED seven custom-provider env vars when spawning claude -p or codex exec subprocesses, enabling Anthropic-compatible providers (MiniMax/api.minimax.io, OpenRouter, AWS Bedrock, corporate gateways)
  • The preserved vars are ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, OPENAI_BASE_URL, CODEX_ACCESS_TOKEN, CLAUDE_CODE_ENTRYPOINT, DISABLE_TELEMETRY, and OTEL_EXPORTER_OTLP_ENDPOINT
  • HISTORICAL: those vars were SEMANTICALLY DISTINCT from the OAuth-only-rejected ANTHROPIC_API_KEY and OPENAI_API_KEY, and the OAuth-only guard rejected the API keys with exit 1. Measured on this working tree: claude_runner.rs, codex_spawn.rs and ingest_claude.rs are not files of this repository any more, and no guard runs
  • HISTORICAL: the whitelist lived in a shared helper src/spawn/env_whitelist.rs exposing apply_env_whitelist(cmd, strict) and is_strict_env_clear(), and the three spawners delegated to it. The whole src/spawn/ directory was removed with the subprocess backends
  • HISTORICAL: for compliance environments that forbid credential forwarding via env vars (PCI-DSS, SOC2, HIPAA), operators passed --strict-env-clear, and strict mode preserved only PATH while dropping every other env var. The flag lived on the global surface until v1.2.2, as src/cli/globals.rs still records; a 1.2.8 binary answers unexpected argument '--strict-env-clear' found with exit 2. Nothing forwards a credential today, because nothing spawns. Product env SQLITE_GRAPHRAG_* is not read at runtime, so it is not the configuration path either
  • HISTORICAL: five #[serial_test::serial(env)] regression tests lived in tests/claude_runner_env.rs, a file this repository no longer contains, covering custom-provider propagation, OAuth-only abort preservation, codex base-URL inheritance, strict-mode credential dropping, and a no-leak audit that scans subprocess stderr for the literal token value with RUST_LOG=trace
  • No telemetry is emitted anywhere in this product, then or now. HISTORICAL: the fix was silent unless the OAuth-only guard fired, surfacing an orientative marker arg pointing to ANTHROPIC_AUTH_TOKEN or ~/.codex/auth.json as legitimate resolutions; that guard no longer exists
  • HISTORICAL threat model: credential values for custom providers flowed from the orchestrator process to the LLM subprocess over the process boundary, the audit-of-no-leak test guarded against a tracing macro printing the raw token to stderr, and operators on shared hosts were told to prefer --strict-env-clear. That boundary does not exist in v1.2.8 and the flag is rejected with exit 2; the credential surface is now a single OpenRouter key read from the XDG store and sent over TLS
  • See docs/decisions/adr-0041-preserve-custom-provider-env.md (EN) and .pt-BR.md for the full architectural decision and the alternatives considered

v1.0.87+ Pre-flight Validation Layer (ADR-0045)

  • Every LLM subprocess spawn passes through src/spawn/preflight.rs (15 unit tests, 7 guards) BEFORE the fork. Failures return AppError::PreFlightFailed (exit code 16, EX_CONFIG).
  • 7 guards: check_argv_size, check_binary_exists, check_mcp_config_inline (replaces literal --mcp-config '{}' with tempfile, fixes BUG-2), check_mcp_config_path, check_walkup_mcp_json (validates .mcp.json walk-up, fixes BUG-5), check_output_buffer (fixes BUG-4), check_claude_config_dir (avoids MCP bleed-through).
  • Bypass (emergencies only): sqlite-graphrag config set spawn.skip_preflight 1 disables all 7 guards. Last-resort opt-out; bypassing reverts to direct Command::spawn() and inherits all 5 BUG classes. Product env is not the supported path in v1.1.8.
  • v1.0.88 hotfixes: BUG-11 (preflight failure in extract/llm_embedding.rs did not propagate to remember; fixed with embed_via_backend_strict), BUG-12 (OAuth-only emitted 2 identical stderr lines; fixed with single-line stderr), BUG-13 (link --create-missing bypassed entity-name validation; fixed by validating BEFORE normalizing in entity_validation_integration.rs, 8 tests, 4-char boundary).
  • See docs/decisions/adr-0045-preflight-validation-layer.md and adr-0046-preflight-remediation.md for the full architectural decision.

v1.0.89 Embedding Pipeline Remediation and Safety Fixes (ADR-0050)

  • BUG-YES-FLAG-IGNORED: three destructive commands (slots release, purge, cleanup-orphans) declared --yes but executed deletions without it. All now abort with AppError::Validation when --yes is absent, matching the 5 other destructive commands that already enforced this
  • BUG-BOOLISH-ENV: four boolean CLI flags (--skip-embedding-on-failure, --strict-env-clear, --dry-run-backend, --llm-slot-no-wait) rejected standard Unix env values (1, yes, on) with exit 2. Fixed via BoolishValueParser. The SQLITE_GRAPHRAG_SKIP_EMBEDDING_ON_FAILURE form is historical and was removed; pass the flag value on the command line instead
  • BUG-STRICT-ENV-PROPAGATION (HISTORICAL): the --strict-env-clear CLI flag was silently ignored because main.rs did not propagate it to the env var. Fixed at the time via set_var before command dispatch; the flag itself was removed after v1.2.2 and a 1.2.8 binary rejects it with exit 2
  • GAP-FLAGS-MORTAS: 7 global LLM flags were accepted by clap but silently ignored because internal modules read env vars directly. Fixed: main.rs now bridges CLI flags to env vars via set_var
  • GAP-RECALL-001: embedding deadlock from stale LLM subprocess slots resolved via explicit drop(stdin), reduced timeout (300s to 30s), stale slot reaper, and sqlite-graphrag orphan process cleanup
  • See docs/decisions/adr-0050-embedding-deadlock-remediation.md for the full architectural decision

v1.0.93 OpenRouter API Key Handling (ADR-0052)

  • v1.0.93 introduces --embedding-backend openrouter which uses a real API key (NOT OAuth) for direct REST API calls to OpenRouter
  • The API key is provided via --openrouter-api-key flag or XDG config add-key openrouter (OPENROUTER_API_KEY is ignored at runtime; G-T-XDG-04)
  • The key is wrapped in secrecy::SecretString and zeroized on drop — NEVER held as plain String in memory after initialization
  • The key is NEVER logged to stderr even at RUST_LOG=trace level
  • The key is NEVER persisted in graphrag.sqlite or any cache file
  • The key is NEVER forwarded to LLM subprocesses (claude, codex, opencode) — it flows only to reqwest HTTPS calls to api.openrouter.ai
  • This is SEMANTICALLY DISTINCT from the OAuth-only enforcement on LLM backends: ANTHROPIC_API_KEY and OPENAI_API_KEY still ABORT with exit 1
  • Historical note: OPENROUTER_API_KEY was never in the env-clear whitelist; the product never reads that env var now — use flag or config add-key only
  • Operators on shared hosts SHOULD prefer config add-key (file mode 0600) or the CLI flag; product env is not a supported channel
  • See docs/decisions/adr-0052-openrouter-embedding-backend.md for the full architectural decision

Hall of Fame

  • We publicly acknowledge researchers who report vulnerabilities responsibly
  • This section is open to contributions: your name will be added after coordinated disclosure
  • If you prefer anonymity, we honor that preference without exception

Best Practices for Users

  • SEMPRE install published releases with cargo install sqlite-graphrag --locked
  • Use cargo install --path . only when testing an unreleased local checkout intentionally
  • SEMPRE rotate your crates.io API tokens on a regular schedule
  • SEMPRE keep your rustc toolchain updated to the latest stable release compatible with MSRV 1.88
  • SEMPRE review CHANGELOG entries before upgrading across major versions
  • JAMAIS commit secrets or tokens to the repository or to derived forks
  • JAMAIS disable the memory guard in production via undocumented flags
  • JAMAIS raise heavy-command concurrency blindly on memory-constrained hosts; prefer serial execution during audits
  • JAMAIS bypass cargo audit warnings without opening a tracked security advisory
  • HISTORICAL advice, kept as a record: JAMAIS set ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment, because the spawn would abort with exit 1. A 1.2.8 binary reads neither variable and spawns nothing, so setting them changes nothing — and removing them protects nothing.
  • HISTORICAL advice: JAMAIS rely on ANTHROPIC_AUTH_TOKEN forwarding when the host is shared with untrusted processes, preferring --strict-env-clear so credentials stayed in the parent process only. Both the forwarding and the flag are gone; on a shared host the exposure to watch today is --openrouter-api-key on the command line, visible in the process table — store the key with config add-key --provider openrouter --from-stdin instead.
  • Prefer link --from-id/--to-id over name-based linking when the identity is an integer entity ID; pure-numeric entity names are rejected by validate_entity_name (v1.1.05) so --create-missing cannot create ghost numeric entities
  • Self-referential merge-entities (target ID/name also listed as a source) is rejected BEFORE any DB work (v1.1.05), protecting graph integrity against shell word-splitting mistakes
  • v1.1.06: first-scan wall-clock for enrich --operation entity-connect / cross-domain-bridges uses InterruptHandle and returns Timeout exit 1 (not singleton exit 75). Orchestrators must not treat scan timeout as a lock contention signal. See ADR-0066 and docs/HEADLESS_INVOCATION.md.

v1.0.94 Headless Mode Hardening (ADR-0053)

  • v1.0.94 makes enrich --mode REQUIRED (removed the claude-code default); omitting it is rejected by clap with exit 2.
  • This prevents an accidental claude -p spawn that would inherit the caller project .mcp.json and execute untrusted MCP servers in a headless context.
  • No new exit code and no new environment variable are introduced; the change is a safer default surface only.
  • Valid modes are claude-code, codex, opencode; pick the one matching your --llm-backend.

v1.0.95 OpenRouter Chat Key Handling (ADR-0054)

  • v1.0.95 adds enrich --mode openrouter, which routes the JUDGE step to OpenRouter /chat/completions over HTTPS (src/chat_api.rs) instead of spawning a local CLI.
  • It reuses the SAME OpenRouter credential already documented for the embedding backend (flag / config add-key; OPENROUTER_API_KEY is ignored at runtime), with the SAME handling: wrapped in secrecy::SecretBox, zeroized on drop, NEVER logged, NEVER passed to any subprocess.
  • The key flows only into the reqwest HTTPS client targeting openrouter.ai; it is not in the env-clear whitelist and stays in the parent process only.
  • No new credential surface is introduced beyond what is already documented for the OpenRouter embedding backend.

v1.0.96 Bounded REST Fan-out and Dead-letter Convergence (ADR-0055)

  • v1.0.96 adds bounded concurrency to the OpenRouter embedding REST path (embed_passages_parallel_with_embedding_choice); in-flight requests are clamped to 1..=16 (default 8, a Cloudflare-safe range) via a tokio::task::JoinSet, with NO new dependency.
  • The fan-out only parallelizes outbound HTTPS reads to openrouter.ai; it does NOT widen the credential or network surface, and the SAME OpenRouter key handling (flag/config add-key, secrecy/zeroize, never logged, never passed to a subprocess; product env ignored) applies unchanged.
  • SQLite writes stay serialized via WAL plus atomic claim — the database remains single-writer; the dead-letter queue (error_class, next_retry_at, terminal dead) only schedules retries and never bypasses the writer lock.
  • enrich --status is read-only: it inspects the queue without an LLM call and without acquiring the singleton, so it is safe to wire into hooks and timers.
  • No new exit code and no new credential surface are introduced.