text-to-sql-agent-langgraph

Security

This is a demo/portfolio project, not a production-hardened product. Please read this before pointing it at anything real.

2026-09-01 update: this document reflects an enterprise-oriented security audit (22 attack-surface categories, tested against the running code, not just reasoned about from the design). It found and closed two real gaps in the SQL validator — see “What’s actually enforced” below and docs/security-changelog.md’s dated entry for the full detail — plus added several defense-in-depth layers (identifier quoting, secret redaction, a SecretStr wrapper, a best-effort write-privilege check, enforced column sensitivity classification, structured security-event logging, a RAG-poisoning detection scan, and closing a process-wide result-cache cross-session risk). Everything below is written to still be accurate after that pass, not a separate “what’s new” list bolted on top.

What’s actually enforced

What is explicitly not guaranteed

Database content is untrusted input too

It’s tempting to think of “the user’s typed question” as the only adversarial input this app has to worry about. It isn’t. Two other things flow into an LLM prompt with roughly the same trust level as a typed question, and are treated the same way:

None of this is sanitized because it’s expected to contain literal SQL injection in the traditional sense (it’s never concatenated into executable SQL — only into prompt text, and the SQL validator still gates whatever the LLM produces regardless). The risk here is prompt injection via stored data, not SQL injection via stored data, and it gets the same layered treatment as user input: normalize at the source, frame it as untrusted data in the system prompt, and don’t rely on either of those alone — the SELECT-only allowlist and read-only connection are what actually bound the consequences if a poisoned value ever does influence what the model generates.

Multi-source RAG and web search (optional, off by default)

Everything in this section only exists when ENABLE_MULTI_SOURCE_ROUTER=true and the specific source’s own flag/config is set — see docs/MULTI_SOURCE_GUIDE.md. With the router off (the default), none of this code path runs at all.

Resource exhaustion / abuse protections

Two independent, deliberately simple protections guard against both accidental abuse (a user re-asking the same broad question repeatedly) and deliberate attempts to degrade the app’s availability — catching the common cases before execution, alongside the row cap and query timeout that already catch anything at execution time (see “What’s actually enforced” above; none of this replaces those).

The REST API (api/) shares this posture, not a separate one

api/main.py’s POST /ask calls the same agent.graph.run_agent the UI calls — every control described above (validator, rate limits, row cap, timeout, sensitive-column blocking) applies identically, there is no parallel, weaker code path. The API adds exactly one new control, an optional shared-bearer-token check (API_AUTH_TOKEN) — explicitly not real per-user authentication; see docs/API.md’s “Auth” section and docs/RISK_REGISTER.md’s R-001. Anything reachable beyond a trusted local network, UI or API, needs a real authenticating reverse proxy in front of it — see docs/DEPLOYMENT.md.

Bottom line

Do not point this at a production database, or a database containing real sensitive/personal data, without an independent security review and your own risk assessment first. For local exploration, portfolio demos, or a sandboxed/synthetic dataset, the read-only validator plus a genuinely-read-only DB account is a reasonable posture. For anything more sensitive, treat this as a starting point, not a finished answer.

See also: docs/GOVERNANCE.md (ownership, change control), docs/COMPLIANCE.md (OWASP/NIST AI RMF/ ISO 42001/EU AI Act self-assessment), docs/RESPONSIBLE_AI.md (design choices through a responsible-AI lens), and docs/RISK_REGISTER.md (every open risk named in this document, tracked with severity and a review date) — together these five documents describe this project’s full risk posture, each linking back to the others.

Reporting a vulnerability

This is a personal project without a dedicated security contact — please open a GitHub issue (or a private security advisory, if enabled on this repo) describing the issue. For something that would let generated SQL bypass the read-only allowlist, or a question/sampled value that gets past agent/input_guard.py and actually changes the model’s behavior (not just past the regex pre-filter — that’s expected to be beatable; past the system prompt’s untrusted-data framing too), please include the exact input that triggered it.