Skip to content

davoroh/sql-shield

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

sql-shield

CI License: Apache 2.0 Node.js >= 18 Zero Dependencies

SQL security middleware for LLM-generated queries.

LLMs can generate any SQL. DROP TABLE, DELETE FROM, SELECT from tables with sensitive data. The model doesn't know your security policy. Most text-to-SQL tools run whatever the LLM produces.

sql-shield sits between your LLM and your database:

         ┌──────────┐     ┌────────────┐     ┌──────────┐
User ──► │ Your LLM  │ ──► │ sql-shield │ ──► │ Database │
         └──────────┘     └────────────┘     └──────────┘
                           │ validate    │
                           │ fixup       │
                           │ enforce     │

What it does

  • Table whitelist — only pre-approved tables can be queried
  • Column whitelist — block access to sensitive columns (email, salary, SSN)
  • SELECT-only — physically blocks INSERT, UPDATE, DELETE, DROP, and 15+ other statements
  • Anti-injection — blocks UNION, stacked queries, subqueries in FROM
  • LLM fixup — corrects common LLM SQL mistakes before they hit your DB
  • Bind variable helpers — normalize and convert named/positional params
  • Row limits — auto-appends LIMIT to prevent full table scans
  • Schema auto-discovery — generate a whitelist from your PostgreSQL schema

Install

npm install davoroh/sql-shield

Works with any LLM tool

sql-shield is a security layer that works with whatever you already use:

  • Vanna AI → pipe output through sql-shield
  • LangChain SQL Agent → add sql-shield as validation step
  • Custom OpenAI/Anthropic prompt → validate before executing
  • Any other tool → if it produces SQL, sql-shield can validate it

How it compares

Most text-to-SQL tools focus on generating SQL. sql-shield focuses on what happens after generation: validating, fixing, and enforcing security policy before the query reaches your database.

Feature Vanna AI LangChain SQL Wren AI Dataherald sql-shield
Table whitelist No Schema filter (not enforced) Prompt-level only No Code-enforced
Column whitelist No No No No Code-enforced
SELECT-only Label only Prompt only Prompt only Keyword blocklist Code-enforced
Bind variables No No No No Yes
Anti-injection (UNION, stacked) No No Semicolon strip No Yes
SQL fixup No LLM checker (opt-in) LLM retry loop Agent retry loop Rule-based (9 rules)

Note: sql-shield is not a replacement for these tools but complements them. Use your preferred engine for SQL generation, then pipe the output through sql-shield before executing.

Comparison verified April 2026. Features may have changed since then.

Quick start

Validate a query

import { validateSQL } from 'sql-shield';

const result = validateSQL("SELECT name, email FROM customers WHERE region = 'EU'", {
  allowedTables: ['customers', 'orders', 'products'],
  allowedColumns: {
    customers: ['name', 'region', 'segment'],  // email is NOT allowed
    orders: ['order_id', 'order_date', 'total'],
  },
  maxRows: 100,
});

// result:
// {
//   safe: false,
//   sql: null,
//   violations: [
//     { type: 'COLUMN_NOT_ALLOWED', table: 'CUSTOMERS', column: 'EMAIL' }
//   ]
// }

Fix LLM mistakes

import { fixupSQL } from 'sql-shield';

const fixed = fixupSQL(
  "SELECT COUNT(orders), name FROM custmers WHERE SUM(total) > 1000;",
  { knownTables: ['customers', 'orders', 'products'] },
);

// fixed:
// {
//   sql: "SELECT COUNT(*), name FROM CUSTOMERS HAVING SUM(total) > 1000",
//   corrections: [
//     { type: 'TABLE_FUZZY_MATCH', from: 'custmers', to: 'CUSTOMERS', distance: 1 },
//     { type: 'COUNT_TABLE_TO_STAR' },
//     { type: 'AGGREGATE_WHERE_TO_HAVING' },
//     { type: 'STRAY_SEMICOLON_REMOVED' }
//   ]
// }

Full pipeline with any LLM

import { validateSQL, fixupSQL } from 'sql-shield';
import OpenAI from 'openai';

const openai = new OpenAI();

// 1. Your LLM generates SQL (your prompt, your model)
const completion = await openai.chat.completions.create({
  model: 'gpt-4.1-mini',
  messages: [{ role: 'user', content: 'Show me top customers by revenue' }],
});
const rawSQL = completion.choices[0].message.content;

// 2. sql-shield fixes and validates
const fixed = fixupSQL(rawSQL, { knownTables: whitelist.tables });
const validation = validateSQL(fixed.sql, {
  allowedTables: whitelist.tables,
  allowedColumns: whitelist.columns,
  maxRows: 100,
});

// 3. Only execute if safe
if (validation.safe) {
  const result = await db.query(validation.sql);
}

Auto-generate whitelist from your database

import { generateWhitelist } from 'sql-shield/catalog';

const whitelist = await generateWhitelist({
  host: 'localhost',
  database: 'my_shop',
  schema: 'public',
  excludeTables: ['migrations', 'sessions', 'admin_users'],
});

// whitelist:
// {
//   tables: ['customers', 'orders', 'products', 'regions'],
//   columns: {
//     customers: ['customer_id', 'name', 'region', 'segment', ...],
//     orders: ['order_id', 'customer_id', 'order_date', 'total', ...],
//   }
// }

API

validateSQL(sql, options)

Validates SQL against a security policy.

Options:

Option Type Default Description
allowedTables string[] Whitelisted table names (case-insensitive)
allowedColumns { table: string[] } Per-table whitelisted columns
maxRows number 200 Auto-appends LIMIT if missing
blockUnion boolean true Block UNION / UNION ALL
blockSubqueries boolean true Block subqueries in FROM/JOIN
blockStacked boolean true Block multiple statements

Returns: { safe: boolean, sql: string|null, violations: Array<{ type: string, detail?: string, table?: string, column?: string }> }

Violation types: NOT_SELECT, FORBIDDEN_KEYWORD, STACKED_QUERIES, UNION, SUBQUERY_IN_FROM, TABLE_NOT_ALLOWED, COLUMN_NOT_ALLOWED

fixupSQL(sql, options)

Applies correction rules to LLM-generated SQL.

Options:

Option Type Default Description
knownTables string[] Known table names for fuzzy matching
knownColumns { table: string[] } Per-table columns for heuristic matching
fuzzyThreshold number 0.85 Table name similarity threshold (0–1)

Returns: { sql: string, corrections: Array<{ type: string, from?: string, to?: string }> }

9 fixup rules:

  1. Fuzzy table name matching (Levenshtein distance)
  2. Column-based table heuristic (when fuzzy match fails)
  3. COUNT(table)COUNT(*)
  4. Case-insensitive LIKE wrapping (UPPER() on both sides)
  5. Reserved word alias fix (CURRENT → CURR, PREVIOUS → PREV)
  6. SQL keyword qualifier fix (WHERE.col → col)
  7. Positional bind conversion ($1 → :b1)
  8. Aggregate in WHERE → HAVING
  9. Stray semicolon removal

normalizeBinds(binds)

Normalizes bind variable keys: {"$1": val}{"b1": val}.

toPositionalParams(sql, binds)

Converts :name placeholders to $N positional params for PostgreSQL. Safely skips :: type casts (e.g. column::integer is not treated as a bind).

Returns: { sql: string, values: any[] }

detectInterpolation(sql)

Checks for potential string interpolation (unsafe patterns).

Returns: { safe: boolean, warnings: string[] }

generateWhitelist(options) (from sql-shield/catalog)

Auto-generates a table/column whitelist from PostgreSQL schema introspection. Requires pg as a peer dependency (optional — only needed for this function).

Options:

Option Type Default Description
host string 'localhost' PostgreSQL host
port number 5432 PostgreSQL port
database string Database name (required)
schema string 'public' Schema to introspect
user string Database user
password string Database password
excludeTables string[] [] Tables to exclude from the whitelist
pool pg.Pool Existing Pool instance (skips creating a new one)

Returns: Promise<{ tables: string[], columns: { [table]: string[] } }>

Zero dependencies

Core validate + fixup + bind functions have zero npm dependencies.

The optional sql-shield/catalog helper requires pg for PostgreSQL introspection.

License

Apache 2.0 — see LICENSE.

About

SQL security middleware for LLM-generated queries. Table/column whitelist, SELECT-only enforcement, anti-injection, LLM SQL fixup. Zero dependencies.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors