Skip to content

Commit cec583c

Browse files
committed
Detect SQL tokenize errors and report them as attacks
With `failed_to_tokenize: true` as metadata
1 parent 62d750c commit cec583c

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

library/vulnerabilities/sql-injection/checkContextForSqlInjection.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { InterceptorResult } from "../../agent/hooks/InterceptorResult";
33
import { SOURCES } from "../../agent/Source";
44
import { getPathsToPayload } from "../../helpers/attackPath";
55
import { extractStringsFromUserInputCached } from "../../helpers/extractStringsFromUserInputCached";
6-
import { detectSQLInjection } from "./detectSQLInjection";
6+
import {
7+
detectSQLInjection,
8+
SQLInjectionDetectionResult,
9+
} from "./detectSQLInjection";
710
import { SQLDialect } from "./dialects/SQLDialect";
811

912
/**
@@ -28,16 +31,28 @@ export function checkContextForSqlInjection({
2831
}
2932

3033
for (const str of userInput) {
31-
if (detectSQLInjection(sql, str, dialect)) {
34+
const result = detectSQLInjection(sql, str, dialect);
35+
36+
if (
37+
result === SQLInjectionDetectionResult.INJECTION_DETECTED ||
38+
result === SQLInjectionDetectionResult.FAILED_TO_TOKENIZE
39+
) {
40+
const metadata: Record<string, string> = {
41+
sql: sql,
42+
dialect: dialect.getHumanReadableName(),
43+
};
44+
45+
if (result === SQLInjectionDetectionResult.FAILED_TO_TOKENIZE) {
46+
// eslint-disable-next-line camelcase
47+
metadata.failed_to_tokenize = "true";
48+
}
49+
3250
return {
3351
operation: operation,
3452
kind: "sql_injection",
3553
source: source,
3654
pathsToPayload: getPathsToPayload(str, context[source]),
37-
metadata: {
38-
sql: sql,
39-
dialect: dialect.getHumanReadableName(),
40-
},
55+
metadata: metadata,
4156
payload: str,
4257
};
4358
}

library/vulnerabilities/sql-injection/detectSQLInjection.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { SQLDialectPostgres } from "./dialects/SQLDialectPostgres";
1010
import { SQLDialectSQLite } from "./dialects/SQLDialectSQLite";
1111

1212
t.test("It ignores invalid queries", async () => {
13-
isNotSqlInjection("SELECT * FROM users WHERE id = 'users\\'", "users\\");
13+
isTokenizeError("SELECT * FROM users WHERE id = 'users\\'", "users\\", [
14+
new SQLDialectMySQL(),
15+
]);
1416
});
1517

1618
t.test("It ignores safely escaped backslash", async () => {
@@ -41,11 +43,11 @@ t.test("user input inside IN (...)", async () => {
4143
});
4244

4345
t.test("It checks whether the string is safely escaped", async () => {
44-
isNotSqlInjection(
46+
isTokenizeError(
4547
`SELECT * FROM comments WHERE comment = 'I'm writting you'`,
4648
"I'm writting you"
4749
);
48-
isNotSqlInjection(
50+
isTokenizeError(
4951
`SELECT * FROM comments WHERE comment = "I"m writting you"`,
5052
'I"m writting you'
5153
);
@@ -336,7 +338,7 @@ function isSqlInjection(
336338
for (const dialect of dialects) {
337339
t.same(
338340
detectSQLInjection(sql, input, dialect),
339-
true,
341+
1,
340342
`${sql} (${dialect.constructor.name})`
341343
);
342344
}
@@ -356,7 +358,27 @@ function isNotSqlInjection(
356358
for (const dialect of dialects) {
357359
t.same(
358360
detectSQLInjection(sql, input, dialect),
359-
false,
361+
0,
362+
`${sql} (${dialect.constructor.name})`
363+
);
364+
}
365+
}
366+
367+
function isTokenizeError(
368+
sql: string,
369+
input: string,
370+
dialects = [
371+
new SQLDialectGeneric(),
372+
new SQLDialectMySQL(),
373+
new SQLDialectPostgres(),
374+
new SQLDialectSQLite(),
375+
new SQLDialectClickHouse(),
376+
]
377+
) {
378+
for (const dialect of dialects) {
379+
t.same(
380+
detectSQLInjection(sql, input, dialect),
381+
3,
360382
`${sql} (${dialect.constructor.name})`
361383
);
362384
}
@@ -369,3 +391,12 @@ t.test("get human readable name", async () => {
369391
t.same(new SQLDialectSQLite().getHumanReadableName(), "SQLite");
370392
t.same(new SQLDialectClickHouse().getHumanReadableName(), "ClickHouse");
371393
});
394+
395+
t.test("it returns 3 if tokenize fails", async () => {
396+
const result = detectSQLInjection(
397+
"SELECT * FROM users WHERE id = '1",
398+
"id = '1",
399+
new SQLDialectGeneric()
400+
);
401+
t.same(result, 3);
402+
});

library/vulnerabilities/sql-injection/detectSQLInjection.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,39 @@ import { shouldReturnEarly } from "./shouldReturnEarly";
33
// eslint-disable-next-line camelcase
44
import { wasm_detect_sql_injection } from "../../internals/zen_internals";
55

6+
export const SQLInjectionDetectionResult = {
7+
SAFE: 0,
8+
INJECTION_DETECTED: 1,
9+
INTERNAL_ERROR: 2,
10+
FAILED_TO_TOKENIZE: 3,
11+
} as const;
12+
13+
export type SQLInjectionDetectionResultType =
14+
(typeof SQLInjectionDetectionResult)[keyof typeof SQLInjectionDetectionResult];
15+
616
export function detectSQLInjection(
717
query: string,
818
userInput: string,
919
dialect: SQLDialect
10-
) {
20+
): SQLInjectionDetectionResultType {
1121
if (shouldReturnEarly(query, userInput)) {
12-
return false;
22+
return SQLInjectionDetectionResult.SAFE;
1323
}
1424

15-
return wasm_detect_sql_injection(
25+
const code = wasm_detect_sql_injection(
1626
query.toLowerCase(),
1727
userInput.toLowerCase(),
1828
dialect.getWASMDialectInt()
1929
);
30+
31+
if (
32+
code === SQLInjectionDetectionResult.SAFE ||
33+
code === SQLInjectionDetectionResult.INJECTION_DETECTED ||
34+
code === SQLInjectionDetectionResult.INTERNAL_ERROR ||
35+
code === SQLInjectionDetectionResult.FAILED_TO_TOKENIZE
36+
) {
37+
return code;
38+
}
39+
40+
throw new Error("Unexpected return code from WASM: " + code);
2041
}

scripts/build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const {
1111
const execAsync = promisify(exec);
1212

1313
// Zen Internals configuration
14-
const INTERNALS_VERSION = "v0.1.39";
14+
const INTERNALS_VERSION = "v0.1.41";
1515
const INTERNALS_URL = `https://github.com/AikidoSec/zen-internals/releases/download/${INTERNALS_VERSION}`;
1616
// ---
1717

0 commit comments

Comments
 (0)