Implementation Guide

    Preventing LLM-Generated SQL from Querying Restricted Columns

    Do not rely on prompts or hidden schemas. Treat the model as an untrusted user and layer controls outside it: a least-privilege database identity, native column permissions, query analysis before execution, and policy enforced at runtime on the tool call that decides allow, redact, block, or escalate. Each layer should record what it decided.

    A layered control architecture

    1. 1

      1. Least-privilege database identity

      Define what the account needs to access and grant only that, rather than starting broad and removing.

    2. 2

      2. Native database controls

      Column privileges, row-level security, and masking remain the final backstop when everything upstream fails.

    3. 3

      3. Pre-execution query analysis

      Parse the SQL and resolve each referenced column through aliases, wildcards, CTEs, joins, and views.

    4. 4

      4. Runtime policy on the tool call

      Evaluate the resolved columns against policy for the requesting identity and decide allow, redact, block, or escalate.

    5. 5

      5. Human approval for privileged operations

      Route sensitive or ambiguous requests to a reviewer instead of resolving them automatically.

    6. 6

      6. Evidence at each layer

      Each layer records its decision so the outcome can be reconstructed later.

    Evidence each layer should produce

    • Requesting user or agent identity, and the database identity the query ran under
    • The generated SQL exactly as received, plus the columns it resolved to
    • The policy version and rules evaluated
    • The decision (allow, redact, block, or escalate) and the reason
    • Any redaction applied, and the approver and outcome for escalated requests
    • Parse failures and the default action taken

    Why prompt and schema controls fail

    A common first attempt tells the model which columns are off limits, or leaves those columns out of the schema shown in the prompt. Both approaches are probabilistic. OWASP notes that system-prompt restrictions on the data an LLM may return can give some mitigation, but because LLM behavior is unpredictable, such restrictions may not always be honored and can be circumvented through prompt injection or other vectors. OWASP's LLM01:2025 guidance adds that, given the stochastic nature of models, it is unclear whether fool-proof prevention of prompt injection exists.

    Schema hiding has a second weakness. The database does not care what the model was shown. A model can guess column names, or issue SELECT * and let the database expand the wildcard. Injected instructions can also arrive through any channel the model reads, including retrieved documents, conversation history, and tool results, so data returned from one query can steer the next. OWASP notes that labeling untrusted content does not by itself enforce the boundary.

    The practical conclusion matches OWASP's guidance on improper output handling: treat the model like any other user, with zero trust, and validate what it produces before it reaches a backend function. Parameterization and bound values address SQL injection. Whether a well-formed query is authorized to touch a particular column is a separate question that needs its own control.

    How generated SQL reaches restricted columns

    Enforcement has to reason about every way a column can be referenced, not only the SELECT list. A wildcard returns whatever the schema contains. An alias renames a restricted column, so a name-matching rule on the output never fires. A subquery or common table expression can select the column internally and expose it one level up under a different name. A join can bring a restricted table into the result through a key the user is allowed to see.

    Views add indirection. A view may select restricted underlying columns, so a check that looks only at the view name says nothing about what it exposes. Analysis has to expand view definitions to the base columns, or the policy has to be defined on the view with the same care as on the table.

    Inference is the hardest case. A restricted column can influence a result without being returned: in a WHERE clause, a GROUP BY, an ORDER BY, or an aggregate such as a count filtered on a sensitive value. A defensible policy treats predicates, joins, and sort keys as references to the column, not only projections. Parsers can fail on dialect-specific syntax, so the system must also decide what happens to a query it cannot fully resolve. The safe default is to block it or route it for review, never to pass it through.

    Identity: where native controls break down

    Native column privileges, row-level security, and masking are strong because the database enforces them regardless of what the application does. They are only as granular as the identity the query runs under. When a text-to-SQL service connects through one shared service account, every user inherits that account's permissions, and per-user restrictions collapse into whatever the account can see.

    There are two broad ways to restore per-user enforcement. The first is to propagate the end user's or agent's identity to the database, for example through per-user credentials, short-lived delegated tokens, or session context that row-level and column policies read. The second is to keep a narrow service account but evaluate the requesting identity at an enforcement point before the query is sent. The choice is a tradeoff. Propagation keeps decisions inside the database but adds connection and credential management. Enforcement at the tool call works across data stores but depends on accurate identity being passed to it. Many deployments use both, with a restrictive service account as the floor.

    Whichever pattern you choose, requests must be attributable to a user or agent identity. That also makes per-identity controls such as rate limiting possible, which helps deter systematic probing of the schema.

    Runtime policy enforcement at the tool call

    The tool call between the model or agent and the database is the point where the generated SQL becomes an action. Enforcing policy there means the decision is made in the execution path and at runtime, not reviewed after the fact. The enforcement point receives the requesting identity and the generated SQL, evaluates policy against the resolved columns, and returns one of several outcomes: allow the query, redact restricted fields from the result, block the call, or escalate it for human approval.

    Trussed enforces policy in the execution path on tool calls across models, agents, and MCP servers, with allow, redact, block, rate limit, and escalate for human approval as available decisions, and it produces audit-ready evidence from that enforcement. It is designed to sit alongside database-native controls, not replace them. The database remains the backstop, and the enforcement layer adds a decision point that sees the request before execution and the result before it returns to the model.

    When evaluating any enforcement layer, ask four questions. Does it act outside the model? Does it preserve the identity of the requester? How does it handle queries it cannot parse? And which decisions does it support beyond a simple block? Redaction suits cases where a query is legitimate but a field should not leave the boundary. Escalation suits privileged operations, where OWASP recommends human-in-the-loop controls.

    Layers between a model and a restricted column

    Database identity

    Least-privilege account and native column permissions.

    Query analysis

    Resolve every column the SQL touches before it runs.

    Runtime policy

    Decide allow, redact, block, or escalate at the tool call.

    Evidence

    Record identity, SQL, policy, decision, and redaction.

    Enforce column-level policy where the query executes

    See how policy enforcement on tool calls and audit-ready evidence fit alongside your database-native controls.

    Request a Demo