Skip to content

feat(langchain): enhance SQL security with validation, parameterization and injection protection #8377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/core_docs/docs/how_to/sql_large_db.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ import DbCheck from "@examples/use_cases/sql/db_check.ts";

<CodeBlock language="typescript">{DbCheck}</CodeBlock>

:::info Security Considerations for Large Databases

When working with large databases, security becomes even more critical:

```typescript
const secureDb = await SqlDatabase.fromDataSourceParams({
appDataSource: datasource,
allowedStatements: ["SELECT"], // Read-only access
includesTables: ["safe_table1"], // Limit accessible tables
enableSqlValidation: true, // SQL injection protection
maxQueryLength: 5000, // Prevent long-running queries
});
```

Always use parameterized queries when filtering data: `db.run("SELECT * FROM users WHERE id = ?", [userId])`

:::

## Many tables

One of the main pieces of information we need to include in our prompt is the schemas of the relevant tables.
Expand Down
21 changes: 21 additions & 0 deletions docs/core_docs/docs/how_to/sql_prompting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ import DbCheck from "@examples/use_cases/sql/db_check.ts";

<CodeBlock language="typescript">{DbCheck}</CodeBlock>

:::tip Enhanced Security Features

The `SqlDatabase` class allows to set security parameters, e.g.:

```typescript
const secureDb = await SqlDatabase.fromDataSourceParams({
appDataSource: datasource,
allowedStatements: ["SELECT"], // Restrict to read-only queries
enableSqlValidation: true, // SQL injection protection (default)
maxQueryLength: 10000, // Query length limit
});

// Use parameterized queries for safer parameter binding
const result = await secureDb.run(
"SELECT * FROM Artist WHERE ArtistId > ? LIMIT ?",
[5, 10]
);
```

:::

## Dialect-specific prompting

One of the simplest things we can do is make our prompt specific to the SQL dialect we're using.
Expand Down
31 changes: 30 additions & 1 deletion docs/core_docs/docs/tutorials/sql_qa.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@
"\n",
"## ⚠️ Security note ⚠️\n",
"\n",
"Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your chain/agent's needs. This will mitigate though not eliminate the risks of building a model-driven system. For more on general security best practices, [see here](/docs/security).\n",
"Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your chain/agent's needs. \n",
"\n",
"**Enhanced Security Features**: The `SqlDatabase` class now includes built-in security protections:\n",
"- **SQL injection detection**: Automatically blocks dangerous patterns\n",
"- **Statement restrictions**: Configure `allowedStatements` to limit query types (e.g., `[\"SELECT\"]` for read-only)\n",
"- **Parameterized queries**: Use `db.run(query, [param1, param2])` for safer parameter binding\n",
"- **Query validation**: Enable `enableSqlValidation` (default: true) for additional protection\n",
"\n",
"These features mitigate though do not eliminate the risks of building a model-driven system.\n",
"\n",
"\n",
"## Architecture\n",
Expand Down Expand Up @@ -88,11 +96,32 @@
"});\n",
"const db = await SqlDatabase.fromDataSourceParams({\n",
" appDataSource: datasource,\n",
" // Security options (new in enhanced SqlDatabase):\n",
" // allowedStatements: [\"SELECT\"], // Restrict to read-only queries\n",
" // enableSqlValidation: true, // SQL injection protection (default: true)\n",
" // maxQueryLength: 10000, // Query length limit (default: 10000)\n",
"});\n",
"\n",
"await db.run(\"SELECT * FROM Artist LIMIT 10;\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"// βœ… SECURE: Using parameterized queries (recommended for user input)\n",
"// Parameters are safely bound, preventing SQL injection\n",
"const artistId = 5;\n",
"const limit = 3;\n",
"const safeResult = await db.run(\n",
" \"SELECT Name FROM Artist WHERE ArtistId > ? LIMIT ?\", \n",
" [artistId, limit]\n",
");\n",
"console.log(\"Parameterized query result:\", safeResult);\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
212 changes: 200 additions & 12 deletions langchain/src/sql_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,72 @@ import {

export type { SqlDatabaseDataSourceParams, SqlDatabaseOptionsParams };

/**
* Patterns to detect dangerous SQL commands.
*/
const dangerousPatterns = [
/;\s*drop\s+/i, // DROP statements
/;\s*delete\s+/i, // DELETE statements
/;\s*update\s+/i, // UPDATE statements
/;\s*insert\s+/i, // INSERT statements
/;\s*alter\s+/i, // ALTER statements
/;\s*create\s+/i, // CREATE statements
/;\s*truncate\s+/i, // TRUNCATE statements
/;\s*exec\s*\(/i, // EXEC statements
/;\s*execute\s*\(/i, // EXECUTE statements
/xp_cmdshell/i, // SQL Server command execution
/sp_executesql/i, // SQL Server dynamic SQL
/--[^\r\n]*/g, // SQL comments (can hide malicious code)
/\/\*[\s\S]*?\*\//g, // Multi-line comments
/\bunion\s+select\b/i, // Union-based injection
/\bor\s+1\s*=\s*1\b/i, // Common injection pattern
/\band\s+1\s*=\s*1\b/i, // Common injection pattern
/'\s*or\s*'1'\s*=\s*'1/i, // String-based injection
/;\s*shutdown\s+/i, // Database shutdown
/;\s*backup\s+/i, // Database backup
/;\s*restore\s+/i, // Database restore
];

/**
* Allowed SQL statements.
*
* @todo(@christian-bromann): In the next major version, the default allowed statements
* will be restricted to ["SELECT"] only for improved security. Users requiring other
* statement types should explicitly configure allowedStatements in the constructor.
*/
const ALLOWED_STATEMENTS = [
"SELECT",
"INSERT",
"UPDATE",
"DELETE",
"CREATE",
"DROP",
"ALTER",
] as const;

const DEFAULT_SAMPLE_ROWS_IN_TABLE_INFO = 3;

const DEFAULT_MAX_QUERY_LENGTH = 10000;

const DEFAULT_ENABLE_SQL_VALIDATION = true;

/**
* Class that represents a SQL database in the LangChain framework.
*
* @security **Security Notice**
* This class generates SQL queries for the given database.
* The SQLDatabase class provides a getTableInfo method that can be used
* to get column information as well as sample data from the table.
* To mitigate risk of leaking sensitive data, limit permissions
* to read and scope to the tables that are needed.
* Optionally, use the includesTables or ignoreTables class parameters
* to limit which tables can/cannot be accessed.
* This class executes SQL queries against a database, which poses significant security risks.
*
* **Security Best Practices:**
* 1. Use a database user with minimal read-only permissions
* 2. Scope access to only necessary tables using includesTables/ignoreTables
* 3. Keep enableSqlValidation=true in production
* 4. Monitor SQL execution logs for suspicious activity
* 5. Consider using prepared statements for complex queries
*
* **⚠️ Breaking Change Notice:**
* In the next major version, the default `allowedStatements` will be restricted
* to `["SELECT"]` only for improved security. Applications requiring other SQL
* operations should explicitly configure `allowedStatements` in the constructor.
*
* @link See https://js.langchain.com/docs/security for more information.
*/
Expand All @@ -48,10 +103,16 @@ export class SqlDatabase

ignoreTables: Array<string> = [];

sampleRowsInTableInfo = 3;
sampleRowsInTableInfo = DEFAULT_SAMPLE_ROWS_IN_TABLE_INFO;

customDescription?: Record<string, string>;

allowedStatements: string[] = [...ALLOWED_STATEMENTS];

enableSqlValidation = DEFAULT_ENABLE_SQL_VALIDATION;

maxQueryLength = DEFAULT_MAX_QUERY_LENGTH;

protected constructor(fields: SqlDatabaseDataSourceParams) {
super(...arguments);
this.appDataSource = fields.appDataSource;
Expand All @@ -63,6 +124,11 @@ export class SqlDatabase
this.ignoreTables = fields?.ignoreTables ?? [];
this.sampleRowsInTableInfo =
fields?.sampleRowsInTableInfo ?? this.sampleRowsInTableInfo;
this.allowedStatements =
fields?.allowedStatements ?? this.allowedStatements;
this.enableSqlValidation =
fields?.enableSqlValidation ?? this.enableSqlValidation;
this.maxQueryLength = fields?.maxQueryLength ?? this.maxQueryLength;
}

static async fromDataSourceParams(
Expand Down Expand Up @@ -151,12 +217,85 @@ export class SqlDatabase
* Execute a SQL command and return a string representing the results.
* If the statement returns rows, a string of the results is returned.
* If the statement returns no rows, an empty string is returned.
*
* @security This method executes raw SQL queries and has security implications.
* Only SELECT queries are allowed by default. To enable other operations,
* set allowedStatements in the constructor options.
*
* @example
* ```typescript
* // βœ… recommended
* const result = await db.run("SELECT * FROM users WHERE age > ?", [18]);
* // ❌ not recommended
* const result = await db.run("SELECT * FROM users WHERE age > 18");
* ```
*
* @param command - SQL query string
* @param fetch - Return "all" rows or just "one"
* @returns JSON string of results
*/
async run(command: string, fetch: "all" | "one" = "all"): Promise<string> {
// TODO: Potential security issue here
const res = await this.appDataSource.query(command);
async run(command: string, fetch?: "all" | "one"): Promise<string>;

if (fetch === "all") {
/**
* Execute a parameterized SQL query with safer parameter binding.
* This overload is recommended for queries with user input.
*
* @param command - SQL query with parameter placeholders (?)
* @param parameters - Array of parameter values to bind
* @param fetch - Return "all" rows or just "one"
* @returns JSON string of results
*
* @example
* ```typescript
* const result = await db.run(
* "SELECT * FROM users WHERE age > ? AND name = ?",
* [18, "John"]
* );
* ```
*/
async run(
command: string,
parameters: unknown[],
fetch?: "all" | "one"
): Promise<string>;

/**
* Execute a SQL command with optional parameters and return results.
*
* @param command - SQL query string
* @param fetchOrParameters - Either fetch mode or parameters array
* @param fetch - Fetch mode when parameters are provided
* @returns JSON string of results
*/
async run(
command: string,
fetchOrParameters?: "all" | "one" | unknown[],
fetch: "all" | "one" = "all"
): Promise<string> {
let parameters: unknown[] | undefined;
let actualFetch: "all" | "one" = "all";

// Determine if second parameter is fetch mode or parameters array
if (Array.isArray(fetchOrParameters)) {
parameters = fetchOrParameters;
actualFetch = fetch;
} else if (fetchOrParameters === "all" || fetchOrParameters === "one") {
actualFetch = fetchOrParameters;
} else if (fetchOrParameters === undefined) {
actualFetch = "all";
}

// Validate and sanitize the SQL command if validation is enabled
if (this.enableSqlValidation) {
this.validateSqlCommand(command);
}

// Execute query with or without parameters
const res = parameters
? await this.appDataSource.query(command, parameters)
: await this.appDataSource.query(command);

if (actualFetch === "all") {
return JSON.stringify(res);
}

Expand All @@ -167,6 +306,55 @@ export class SqlDatabase
return "";
}

/**
* Validates a SQL command for security vulnerabilities.
* Throws an error if the command is potentially unsafe.
*/
private validateSqlCommand(command: string): void {
if (!command || typeof command !== "string") {
throw new Error("SQL command must be a non-empty string");
}

// Check for dangerous patterns
for (const pattern of dangerousPatterns) {
if (pattern.test(command)) {
throw new Error(
`Potentially unsafe SQL command detected. Pattern: ${pattern.source}`
);
}
}

// Remove leading/trailing whitespace and normalize
const normalizedCommand = command.trim().toLowerCase();

// Check if the command starts with an allowed statement
const startsWithAllowedStatement = this.allowedStatements.some((stmt) =>
normalizedCommand.startsWith(stmt.toLowerCase())
);
if (!startsWithAllowedStatement) {
throw new Error(
`Only ${this.allowedStatements.join(
", "
)} queries are allowed for security reasons`
);
}

// Check for multiple statements (semicolon followed by non-whitespace)
const statementCount = command
.split(";")
.filter((stmt) => stmt.trim().length > 0).length;
if (statementCount > 1) {
throw new Error("Multiple SQL statements are not allowed");
}

// Additional validation: check for excessively long queries (potential DoS)
if (command.length > this.maxQueryLength) {
throw new Error(
`SQL command exceeds maximum allowed length of ${this.maxQueryLength} characters`
);
}
}

serialize(): SerializedSqlDatabase {
return {
_type: "sql_database",
Expand Down
Loading