https://github.com/nur-zaman/sql-guard
Validate AI generated PostgreSQL queries against explicit allowlists. This package parses SQL into an AST and denies anything outside your policy.
https://github.com/nur-zaman/sql-guard
security sql sql-injection sql-sanitizer
Last synced: 3 days ago
JSON representation
Validate AI generated PostgreSQL queries against explicit allowlists. This package parses SQL into an AST and denies anything outside your policy.
- Host: GitHub
- URL: https://github.com/nur-zaman/sql-guard
- Owner: nur-zaman
- License: mit
- Created: 2026-03-02T10:13:48.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-04T20:39:11.000Z (3 months ago)
- Last Synced: 2026-04-04T22:41:23.749Z (3 months ago)
- Topics: security, sql, sql-injection, sql-sanitizer
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/sql-guard
- Size: 74.2 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# sql-guard
Validate AI generated PostgreSQL queries against explicit allowlists. This package parses SQL into an AST and denies anything outside your policy.
## Installation
```bash
npm install sql-guard
```
## Quickstart
```typescript
import { validate, assertSafeSql, ErrorCode } from 'sql-guard';
const policy = {
allowedTables: ['public.users', 'public.orders'],
allowedFunctions: ['count', 'lower'],
};
const result = validate('SELECT * FROM public.users', policy);
if (!result.ok) {
console.log('Denied:', result.errorCode);
console.log('Violations:', result.violations);
}
// Or fail fast with an exception
assertSafeSql('SELECT lower(u.email) FROM public.users u', policy);
```
## API Reference
### validate(sql, policy)
Validates SQL against a policy.
- Returns: `ValidationResult`
- On failure: `ok === false`, `violations` populated, and `errorCode` set
### assertSafeSql(sql, policy)
Validates SQL and throws when validation fails.
- Returns: `void`
- Throws: `SqlValidationError` with `code: ErrorCode` and `violations: Violation[]`
```typescript
import { assertSafeSql, SqlValidationError, ErrorCode } from 'sql-guard';
try {
assertSafeSql('SELECT pg_catalog.current_database() FROM public.users', {
allowedTables: ['public.users'],
allowedFunctions: ['lower'],
});
} catch (err) {
if (err instanceof SqlValidationError) {
if (err.code === ErrorCode.FUNCTION_NOT_ALLOWED) {
console.error('Blocked a function call:', err.violations);
}
}
throw err;
}
```
### ErrorCode
Enum of error codes returned by `validate()` and used by `SqlValidationError`.
### Policy
Policy settings that drive validation.
```ts
export interface Policy {
allowedTables: string[];
allowedStatements?: ('select' | 'insert' | 'update' | 'delete')[];
allowMultiStatement?: boolean;
allowedFunctions?: string[];
tableIdentifierMatching?: 'strict' | 'caseInsensitive';
resolver?: (unqualified: string) => string | null;
defaultSchema?: string;
}
```
Defaults and behavior:
- `allowedTables` is required.
- `allowedTables` entries must be schema-qualified (`schema.table`). Invalid entries return `INVALID_POLICY`.
- `allowedStatements` defaults to `['select']`.
- `allowMultiStatement` defaults to `false`.
- `allowedFunctions` defaults to `[]`, which means any function call is denied unless allowlisted.
- `tableIdentifierMatching` defaults to `'strict'` (exact case-sensitive table matching).
- Set `tableIdentifierMatching: 'caseInsensitive'` to preserve case-insensitive table matching.
- Unqualified table references in SQL are denied unless you provide `defaultSchema` or `resolver` to map them to `schema.table`.
- `defaultSchema`: when provided, unqualified `allowedTables` entries are auto-qualified with this schema, and unqualified SQL references resolve to it.
- `resolver`: optional function to map unqualified names to qualified names. Takes precedence over `defaultSchema`.
- Metadata schemas (`information_schema`, `pg_catalog`) are treated specially and must be explicitly allowlisted even when using `defaultSchema`. Setting `defaultSchema` to a metadata schema name does not grant automatic access.
- Unqualified function allowlist entries (for example, `lower`) match only unqualified calls (`lower(...)`).
- Schema-qualified function calls require schema-qualified allowlist entries (`pg_catalog.current_database`).
Policy examples:
```ts
// Explicit schema-qualified tables
const strictPolicy = {
allowedTables: ['public.users', 'analytics.events'],
allowedFunctions: ['lower', 'pg_catalog.current_database'],
resolver: (unqualified: string) =>
unqualified === 'users' ? 'public.users' : null,
};
// Using defaultSchema for simpler configuration
const defaultSchemaPolicy = {
defaultSchema: 'public',
allowedTables: ['users', 'orders', 'products'],
// Treated as ['public.users', 'public.orders', 'public.products']
};
// Mixed: defaultSchema + explicit qualified tables
const mixedPolicy = {
defaultSchema: 'public',
allowedTables: ['users', 'analytics.events'],
// Treated as ['public.users', 'analytics.events']
};
// Resolver takes precedence over defaultSchema
const resolverPolicy = {
defaultSchema: 'public',
allowedTables: ['public.users', 'archive.users'],
resolver: (name: string) =>
name === 'old_users' ? 'archive.users' : null,
// 'users' resolves to 'public.users' via defaultSchema
// 'old_users' resolves to 'archive.users' via resolver
};
```
## Security Model
- AST based validation, not regex matching.
- Fail closed: unsupported or uncertain parser features are denied.
- Data-modifying CTE payloads (for example `WITH x AS (INSERT ...) SELECT ...`) are denied as unsupported.
- `SELECT INTO` is denied as unsupported.
- Table allowlists: every referenced table must be in `policy.allowedTables` by fully qualified name.
- Statement type restrictions: only `select` is allowed unless you opt in via `allowedStatements`.
- Multi statement restriction: `SELECT 1; SELECT 2` is denied unless `allowMultiStatement: true`.
- Function allowlists: schema-qualified calls are allowed only by exact schema-qualified entries.
- Metadata table protection: relations in `information_schema` and `pg_catalog` are denied unless explicitly allowlisted by fully qualified name.
This is a guardrail for LLM output. It helps enforce least privilege at the query shape level. Use it alongside parameterization, prepared statements, and database permissions.
## Limitations
- PostgreSQL focused (v1). Other dialects are not supported.
- No SQL rewriting or sanitization. This package validates, it doesn't transform queries.
- Not a complete SQL injection defense by itself. Treat it as defense in depth.
- No database context: it can't check column level permissions, RLS policies, or runtime schema changes.
## Error Codes
`validate()` returns a single `errorCode` plus a list of `violations`. Invalid policy configuration is reported before SQL parsing.
| Code | Description |
|------|-------------|
| `PARSE_ERROR` | SQL could not be parsed into an AST. |
| `UNSUPPORTED_SQL_FEATURE` | Parsed SQL contains features outside the supported subset (fail closed). |
| `TABLE_NOT_ALLOWED` | A referenced table is not in `policy.allowedTables`, or an unqualified table can't be resolved. |
| `STATEMENT_NOT_ALLOWED` | Statement type is not allowed (defaults to `select` only). |
| `FUNCTION_NOT_ALLOWED` | A function call is not in `policy.allowedFunctions`. |
| `MULTI_STATEMENT_DISABLED` | Query contains multiple statements while `allowMultiStatement` is disabled. |
| `INVALID_POLICY` | Policy configuration is invalid (for example non-qualified table allowlist entries). |
## Violation Types
`Violation.type` can be:
- `parse`
- `unsupported`
- `policy`
- `statement`
- `table`
- `function`
## License
MIT