Skip to content

feat(nl2sql): prompt-injection boundary defence + EXPLAIN dry-run pre-flight#257

Merged
devopam merged 2 commits into
mainfrom
feat/nl2sql-injection-hardening-explain-preflight
Jul 7, 2026
Merged

feat(nl2sql): prompt-injection boundary defence + EXPLAIN dry-run pre-flight#257
devopam merged 2 commits into
mainfrom
feat/nl2sql-injection-hardening-explain-preflight

Conversation

@devopam

@devopam devopam commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Ships the two remaining security-hardening.md items — both scoped to translate_nl_to_sql, both now ✅.

1. Prompt-injection boundary defence. Defence-in-depth at the prompt layer (the AST allowlist stays the enforcement backstop):

  • The user question is fenced in <user_request></user_request> delimiters.
  • The system prompt instructs the model to treat that text as data, never instructions, and to refuse any out-of-scope request (write/DDL, secrets, "ignore previous instructions") by emitting the sentinel -- MCPG_REFUSED: <reason>.
  • translate_nl_to_sql detects the sentinel (in the JSON sql field or a bare reply) and returns TranslationResult(refused=True, refusal_reason=…) with empty sql — never forwarding it to SafeSqlDriver / execution.

2. EXPLAIN dry-run pre-flight. Semantic validation before returning/executing:

  • Runs a non-executing EXPLAIN (reusing mcpg.query.explain_query, so the SQL is SafeSqlDriver-validated first) to catch queries that pass the structural allowlist but reference a non-existent column/table or mistype something.
  • Planner rejection → structured error="query invalid: <message>" (SQL returned for inspection, nothing executed). A pre-flight statement_timeout degrades to "proceed" rather than blocking a valid translation.
  • New explain_preflight tool arg (default on), bypassable per call.

Surface/return-shape changes: TranslationResult gains refused / refusal_reason; the tool gains the explain_preflight input. Both contract snapshots regenerated (diffs scoped to exactly these). 5 new unit tests (delimiter wrap, JSON-field + bare-line refusal, invalid-query pre-flight, pre-flight bypass); 101 unit + contract tests green; ruff/mypy clean.

Roadmap linkage

Advances roadmap row: N/A — clears the two open items in docs/security-hardening.md (marked ✅ in this PR).

Checklist

  • Tests added first; suite passes locally (101 relevant tests green)
  • ruff, ruff format, and mypy src/mcpg pass
  • CHANGELOG.md updated under [Unreleased]
  • Both contract snapshots regenerated + committed
  • No hand-edits to src/mcpg/_vendor/

🤖 Generated with Claude Code


Generated by Claude Code

Summary by Sourcery

Strengthen nl2sql security and robustness by adding prompt-injection refusal handling and an EXPLAIN-based semantic pre-flight check, and surface these via extended TranslationResult and tool parameters.

New Features:

  • Add prompt-level refusal mechanism where the model can decline out-of-scope or injected requests using a refusal sentinel that is surfaced as a structured TranslationResult.
  • Introduce an EXPLAIN-based dry-run pre-flight for nl2sql translations, controllable via a new explain_preflight argument and enabled by default.

Enhancements:

  • Extend TranslationResult with refusal metadata to distinguish clean refusals from other error conditions.
  • Update the nl2sql system prompt to wrap user questions in explicit delimiters and emphasize treating them as data, not instructions.

Documentation:

  • Mark the nl2sql prompt-injection hardening and EXPLAIN pre-flight items as shipped in security-hardening documentation, describing the new behaviour and controls.

Tests:

  • Add unit tests covering user question delimiter wrapping, refusal sentinel handling in both JSON and bare responses, EXPLAIN pre-flight invalid-query reporting, and the ability to disable pre-flight.

Chores:

  • Regenerate contract snapshots and update the changelog to reflect the new nl2sql security and validation behaviours.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/mcpg/tools.py" line_range="3164" />
<code_context>
     question: str,
     schema: str,
     execute: bool = False,
+    explain_preflight: bool = True,
     table_filter: tuple[str, ...] | None = None,
     max_tokens: int = DEFAULT_MAX_TOKENS,
</code_context>
<issue_to_address>
**issue (bug_risk):** Adding `explain_preflight` before `table_filter` changes positional argument order and can break existing callers.

Because this is a public-facing function, inserting `explain_preflight` before `table_filter` changes the positional argument order and can silently misroute arguments for any existing positional callers. To avoid breaking those callers, consider adding `explain_preflight` at the end of the parameter list or making new parameters keyword-only.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/mcpg/tools.py
@gemini-code-assist-2

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

claude added 2 commits July 7, 2026 20:36
Defence-in-depth at the prompt layer for translate_nl_to_sql (the AST
allowlist stays the enforcement backstop):

- Wrap the user question in <user_request>...</user_request> delimiters.
- System prompt instructs the model to treat that text as data (never
  instructions) and to REFUSE any out-of-scope request (write/DDL,
  secrets, 'ignore previous instructions', ...) by emitting the sentinel
  '-- MCPG_REFUSED: <reason>'.
- translate detects the sentinel (JSON sql field or bare reply) and
  returns TranslationResult(refused=True, refusal_reason=...) with empty
  sql, never forwarding it to SafeSqlDriver / execution.

Adds refused / refusal_reason to the result (return-shape snapshot
regenerated; tool input surface unchanged). 3 new unit tests cover the
delimiter wrap, the JSON-field sentinel, and the bare-line sentinel.

This is the first of the two remaining security-hardening.md items; the
EXPLAIN dry-run pre-flight is a fast-follow on this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122yLZLJ8t4W43sdN6BmTZc
Second of the two remaining security-hardening.md items. Before returning
or executing the generated SQL, run a non-executing EXPLAIN (reusing
mcpg.query.explain_query, so it's SafeSqlDriver-validated) to catch queries
that pass the structural allowlist but reference a non-existent
column/table or mistype something:

- Planner rejection -> structured error='query invalid: <message>' with the
  SQL returned for inspection; nothing executed.
- Pre-flight statement_timeout -> degrade to 'skip, proceed' (don't block a
  valid translation).
- New explain_preflight tool arg (default on); bypassable per call.

Surface snapshot regenerated (translate_nl_to_sql gains explain_preflight).
2 new unit tests (invalid-query surfaced; bypass when disabled) + an EXPLAIN
route in the nl2sql test schema so the default-on pre-flight passes.

Completes security-hardening.md — both NL->SQL items now shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122yLZLJ8t4W43sdN6BmTZc
@devopam devopam force-pushed the feat/nl2sql-injection-hardening-explain-preflight branch from cc1ebfa to 04b7d74 Compare July 7, 2026 20:37
@devopam devopam merged commit 748b7f5 into main Jul 7, 2026
7 of 10 checks passed
@devopam devopam deleted the feat/nl2sql-injection-hardening-explain-preflight branch July 7, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants