Skip to content

Commit 3721ccd

Browse files
committed
Block SQL queries that fail tokenization
Our SQL injection detection tokenizes queries to check them. If the tokenizer can't parse a query, we skip it and the query goes through. Some databases still execute partially valid queries though: ClickHouse ignores junk after ; and SQLite runs everything before an unclosed /*. Now when user input shows up in a query we can't tokenize, we treat it as an attack. On by default, opt out with AIKIDO_BLOCK_INVALID_SQL=false.
1 parent 27ea0d5 commit 3721ccd

6 files changed

Lines changed: 51 additions & 22 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.aikido.agent_api.helpers.env;
2+
3+
public class BlockInvalidSqlEnv extends BooleanEnv {
4+
private static final String environmentName = "AIKIDO_BLOCK_INVALID_SQL";
5+
private static final boolean defaultValue = true;
6+
7+
public BlockInvalidSqlEnv() {
8+
super(environmentName, defaultValue);
9+
}
10+
}

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/sql_injection/RustSQLInterface.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,19 @@ public interface SqlLib {
2424
int detect_sql_injection(String query, long queryLen, String userinput, long userinputLen, int dialect);
2525
}
2626

27-
public static boolean detectSqlInjection(String query, String userInput, Dialect dialect) {
27+
public static int detectSqlInjection(String query, String userInput, Dialect dialect) {
2828
int dialectInteger = dialect.getDialectInteger();
2929
try {
3030
SqlLib lib = loadLibrary();
3131
if (lib != null) {
3232
long queryLen = query != null ? query.getBytes(StandardCharsets.UTF_8).length : 0;
3333
long userInputLen = userInput != null ? userInput.getBytes(StandardCharsets.UTF_8).length : 0;
34-
int result = lib.detect_sql_injection(query, queryLen, userInput, userInputLen, dialectInteger);
35-
return result == 1;
34+
return lib.detect_sql_injection(query, queryLen, userInput, userInputLen, dialectInteger);
3635
}
3736
} catch (Throwable e) {
3837
logger.trace(e);
3938
}
40-
return false;
39+
return 0;
4140
}
4241
public static SqlLib loadLibrary() {
4342
String path = getPathForBinary();

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/sql_injection/SqlDetector.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package dev.aikido.agent_api.vulnerabilities.sql_injection;
22

3+
import dev.aikido.agent_api.helpers.env.BlockInvalidSqlEnv;
34
import dev.aikido.agent_api.helpers.logging.LogManager;
45
import dev.aikido.agent_api.helpers.logging.Logger;
56
import dev.aikido.agent_api.vulnerabilities.Detector;
67

8+
import java.util.HashMap;
79
import java.util.Map;
810
import java.util.regex.Pattern;
911

@@ -23,21 +25,32 @@ public DetectorResult run(String userInput, String[] arguments) {
2325
}
2426
String query = arguments[0];
2527
Dialect dialect = new Dialect(arguments[1]);
26-
boolean detectedAttack = detectSqlInjection(query, userInput, dialect);
27-
if (detectedAttack) {
28+
int result = detectSqlInjection(query, userInput, dialect);
29+
30+
if (result == 1) {
2831
Map<String, String> metadata = Map.of(
2932
"sql", query,
3033
"dialect", dialect.getHumanName()
3134
);
32-
return new DetectorResult(/* detectedAttack*/ true, metadata, SQLInjectionException.get(dialect));
35+
return new DetectorResult(true, metadata, SQLInjectionException.get(dialect));
36+
}
37+
38+
if (result == 3 && new BlockInvalidSqlEnv().getValue()) {
39+
Map<String, String> metadata = new HashMap<>();
40+
metadata.put("sql", query);
41+
metadata.put("dialect", dialect.getHumanName());
42+
metadata.put("failedToTokenize", "true");
43+
return new DetectorResult(true, metadata, SQLInjectionException.get(dialect));
3344
}
45+
3446
return new DetectorResult();
3547
}
36-
public static boolean detectSqlInjection(String query, String userInput, Dialect dialect) {
48+
49+
public static int detectSqlInjection(String query, String userInput, Dialect dialect) {
3750
String queryLower = query.toLowerCase();
3851
String userInputLower = userInput.toLowerCase();
3952
if (shouldReturnEarly(queryLower, userInputLower)) {
40-
return false;
53+
return 0;
4154
}
4255
return RustSQLInterface.detectSqlInjection(queryLower, userInputLower, dialect);
4356
}

agent_api/src/test/java/vulnerabilities/RustSQLInterfaceTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
import dev.aikido.agent_api.vulnerabilities.sql_injection.RustSQLInterface;
55
import org.junit.jupiter.api.Test;
66

7-
import static org.junit.jupiter.api.Assertions.assertFalse;
8-
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
98

109
public class RustSQLInterfaceTest {
1110
@Test
1211
public void testItWorks() {
13-
boolean injectionResult = RustSQLInterface.detectSqlInjection("SELECT * FROM table;", "table;", new Dialect("postgresql"));
14-
assertTrue(injectionResult);
12+
int injectionResult = RustSQLInterface.detectSqlInjection("SELECT * FROM table;", "table;", new Dialect("postgresql"));
13+
assertEquals(1, injectionResult);
1514
injectionResult = RustSQLInterface.detectSqlInjection("SELECT * FROM table;", "table", new Dialect("postgresql"));
16-
assertFalse(injectionResult);
15+
assertEquals(0, injectionResult);
1716
}
1817
}

agent_api/src/test/java/vulnerabilities/SqlInjectionTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,30 @@
55
import org.junit.jupiter.api.Test;
66

77
import static dev.aikido.agent_api.vulnerabilities.sql_injection.SqlDetector.detectSqlInjection;
8-
import static org.junit.jupiter.api.Assertions.assertFalse;
9-
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
1010

1111
public class SqlInjectionTest {
1212
private void isNotSqlInjection(String sql, String input, String dialect) {
13-
boolean result;
13+
int result;
1414
if ("mysql".equals(dialect) || "all".equals(dialect)) {
1515
result = detectSqlInjection(sql, input, new Dialect("mysql"));
16-
assertFalse(result, String.format("Expected no SQL injection for SQL: %s and input: %s", sql, input));
16+
assertNotEquals(1, result, String.format("Expected no SQL injection for SQL: %s and input: %s", sql, input));
1717
}
1818
if ("postgresql".equals(dialect) || "all".equals(dialect)) {
1919
result = detectSqlInjection(sql, input, new Dialect("postgresql"));
20-
assertFalse(result, String.format("Expected no SQL injection for SQL: %s and input: %s", sql, input));
20+
assertNotEquals(1, result, String.format("Expected no SQL injection for SQL: %s and input: %s", sql, input));
2121
}
2222
}
2323
private void isSqlInjection(String sql, String input, String dialect) {
24-
boolean result;
24+
int result;
2525
if ("mysql".equals(dialect) || "all".equals(dialect)) {
2626
result = detectSqlInjection(sql, input, new Dialect("mysql"));
27-
assertTrue(result, String.format("Expected SQL injection for SQL: %s and input: %s", sql, input));
27+
assertEquals(1, result, String.format("Expected SQL injection for SQL: %s and input: %s", sql, input));
2828
}
2929
if ("postgresql".equals(dialect) || "all".equals(dialect)) {
3030
result = detectSqlInjection(sql, input, new Dialect("postgresql"));
31-
assertTrue(result, String.format("Expected SQL injection for SQL: %s and input: %s", sql, input));
31+
assertEquals(1, result, String.format("Expected SQL injection for SQL: %s and input: %s", sql, input));
3232
}
3333
}
3434

docs/invalid-sql-queries.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Blocking invalid SQL queries
2+
3+
Zen blocks SQL queries that it can't tokenize when they contain user input. This prevents attackers from bypassing SQL injection detection with malformed queries. For example, ClickHouse ignores invalid SQL after `;`, and SQLite runs queries before an unclosed `/*` comment.
4+
5+
This is on by default. In blocking mode, these queries are blocked. In detection-only mode, they are reported but still executed.
6+
7+
If you see false positives (legitimate queries being blocked), set the
8+
`AIKIDO_BLOCK_INVALID_SQL` environment variable to `false`.

0 commit comments

Comments
 (0)