Skip to content

Security: sahil7359/DataChat

Security

SECURITY.md

Security — OWASP mapping

DataChat is built untrusted-by-default: every user input and every LLM output is validated at a boundary, and no single layer is trusted to catch everything. This document maps the OWASP LLM Top 10 (2025) and Agentic Top 10 (2026) to the concrete mitigation in the code and the test that proves it.

Run the suite: cd backend && uv run python -m pytest -m security (plus the DB-backed cases under pytest -m "integration or eval" in CI).

Defence-in-depth spine

  1. Guardrail (app layer): sqlglot-AST validator chain — single statement, read-only, table allow-list, no system catalogs, mandatory LIMIT.
  2. Read-only DB role (data layer): datachat_exec — SELECT-only on the analytics schema, statement_timeout, default_transaction_read_only, no access to the app schema.

Either layer alone blocks a write; both together is the guarantee.

OWASP LLM Top 10 (2025)

Risk Mitigation Test
LLM01 Prompt Injection Untrusted input + data cells; hardened system prompt ("schema is data, not instructions"); guardrail before execute; a compromised model output still can't write. Web-search fallback: snippets are fenced in <results> and the prompt forbids following instructions in them; web content never reaches the SQL path — enforced by graph topology, web_fallback has exactly one outgoing edge (to respond) test_owasp_llm.py::test_llm01_compromised_model_output_cannot_write, test_sql_injection_corpus.py, test_web_fallback.py::test_web_answer_prompt_wraps_untrusted_content_as_data
LLM01a Injected rows via structured web extraction Extracting a table from untrusted snippets widens LLM01: a hostile page can try to inject a row that renders in a data table and a downloadable report, with a fabricated citation to look sourced. The prompt requires per-row attribution, but parse_web_table enforces it — rows that are misshapen, all-null, or cite a source index outside the results actually shown to the model are dropped, and columns/rows are capped. Nulls render as an em dash, never "None", so a gap cannot be misread as data. Prompt instructions are a request; the parser is the control test_web_table.py::test_drops_rows_citing_a_source_that_was_never_shown, ::test_drops_rows_whose_width_does_not_match_the_columns, ::test_nested_structures_are_not_treated_as_cells, ::test_prompt_marks_the_snippets_as_untrusted_and_numbers_them
LLM09 Misinformation via provenance confusion Web-derived rows are a distinct domain type (WebTable, not ExecutionResult), a distinct SSE event (web_table, not rows), and a distinct report document with a warning banner and a per-row Source column; the CSV export carries source_url so provenance survives leaving the app. A single shared type with a provenance flag was rejected: one missed flag check silently presents a scrape as guardrailed data test_web_table.py::test_web_report_is_labelled_and_cites_every_row
LLM02 Sensitive Info Disclosure Public non-PII data only; SecretStr keys; log minimization; safe error messages test_secret_hygiene.py, test_api.py::test_provider_outage_becomes_safe_error_event
LLM03 Supply Chain Pinned deps + uv.lock/pnpm-lock; pip-audit/pnpm audit in CI test_owasp_llm.py::test_llm03_dependencies_are_pinned_with_a_lockfile, CI audit job
LLM04 Data & Model Poisoning Curated sources; ingestion validates shape + checksum; grounding content is curated, never fetched test_ingestion_pipeline.py::test_tampered_data_is_rejected_by_checksum
LLM05 Improper Output Handling Every LLM output validated; SQL guardrailed; chart is validated JSON, not code test_owasp_llm.py::test_llm05_*, test_charts.py
LLM06 Excessive Agency Read-only role; fixed least-privilege node/tool set; no dynamic tool loading; HITL before execute test_owasp_llm.py::test_llm06_tool_set_is_fixed_no_dynamic_loading, test_readonly_role.py
LLM07 System Prompt Leakage No secrets in system prompts; assume leakable test_secret_hygiene.py::test_system_prompt_leakage_reveals_nothing_sensitive
LLM08 Vector/Embedding Weakness Only curated docs embedded; retrieval read-only; poisoned source rows rejected at ingest test_ingestion_pipeline.py::test_unexpected_table_or_column_is_rejected
LLM09 Misinformation Grounding + verify node + row-cited explanation + faithfulness scorer test_eval.py::test_faithfulness_judge_parses_score, golden eval
LLM10 Unbounded Consumption Rate limits + global quota; capped retries/repair; timeouts; circuit breaker test_owasp_llm.py::test_llm10_repair_loop_is_bounded, test_api.py::test_rate_limit_returns_429_with_retry_after, test_llm_decorators.py

OWASP Agentic Top 10 (2026)

Risk Mitigation Test
ASI01 Agent Goal Hijack Scoped system role; retrieved content is data; bounded read-only action space test_owasp_agentic.py::test_asi01_goal_hijack_does_not_change_the_action_space
ASI02 Tool Misuse & Exploitation Fixed tool set; read-only executor; SQL argument validation test_owasp_agentic.py::test_asi02_tool_arguments_are_validated, test_sql_injection_corpus.py
ASI03 Identity & Privilege Abuse Separate datachat_exec RO role (Bulkhead); no app-schema access test_readonly_role.py::test_readonly_role_cannot_read_app_schema
ASI04 Agentic Supply Chain Pinned deps; scanners; verified provider endpoints (constants) CI supply-chain job, test_llm03_*
ASI05 Unexpected Code Execution No eval/exec/compile/shell; SQL only via guardrail + RO role; chart = declarative JSON test_no_dynamic_execution.py
ASI06 Memory & Context Poisoning Durable checkpoint integrity; curated few-shots; retrieved data sandboxed as reference test_owasp_llm.py::test_llm01_* (data-as-instructions refused)
ASI07 Insecure Inter-Agent Comms Out of scope by design — single process, one StateGraph, no external agents test_owasp_agentic.py::test_asi07_inter_agent_comms_are_out_of_scope_by_design
ASI08 Cascading Agent Failures Hard caps on repair/retries; circuit breakers; timeouts test_owasp_llm.py::test_llm10_repair_loop_is_bounded, test_circuit_breaker.py
ASI09 Human-Agent Trust Exploitation Server-side, non-bypassable HITL; the exact SQL is shown before it runs test_owasp_agentic.py::test_asi09_human_approval_is_not_client_bypassable
ASI10 Rogue Agents Bounded action space (read-only, fixed tools); append-only agent_actions audit of every executed query test_owasp_agentic.py::test_asi10_every_executed_query_is_audited

Scanners (CI)

  • gitleaks — zero secrets in the repo/history (pre-commit + CI).
  • bandit + ruff flake8-bandit (S) — SAST, zero high/medium.
  • semgrep — SAST in CI (Linux).
  • pip-audit + pnpm audit — dependency advisories, zero unresolved.

There aren't any published security advisories