Skip to content

Commit 5edc0a4

Browse files
committed
feat(javascript): implement Go-based SQL injection checker
1 parent ac294e2 commit 5edc0a4

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

checkers/checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ type Analyzer struct {
6767
var AnalyzerRegistry = []Analyzer{
6868
{
6969
TestDir: "checkers/javascript/testdata", // relative to the repository root
70-
Analyzers: []*goAnalysis.Analyzer{javascript.NoDoubleEq},
70+
Analyzers: []*goAnalysis.Analyzer{javascript.NoDoubleEq, javascript.SQLInjection},
7171
},
7272
}
7373

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package javascript
2+
3+
import (
4+
sitter "github.com/smacker/go-tree-sitter"
5+
"globstar.dev/analysis"
6+
)
7+
8+
var SQLInjection = &analysis.Analyzer{
9+
Name: "sql_injection",
10+
Language: analysis.LangJs,
11+
Description: "Using raw SQL queries with unvalidated input can lead to SQL injection vulnerabilities",
12+
Category: analysis.CategorySecurity,
13+
Severity: analysis.SeverityCritical,
14+
Run: detectSQLInjection,
15+
}
16+
17+
func detectSQLInjection(pass *analysis.Pass) (interface{}, error) {
18+
// Map of vulnerable function names to watch for
19+
vulnerableFunctions := map[string]bool{
20+
"query": true,
21+
"raw": true,
22+
"$queryRawUnsafe": true,
23+
"$executeRawUnsafe": true,
24+
}
25+
26+
// Map to track variable definitions
27+
varDefinitions := make(map[string]*sitter.Node)
28+
29+
// First pass: collect all variable definitions
30+
analysis.Preorder(pass, func(node *sitter.Node) {
31+
if node == nil || node.Type() != "variable_declarator" {
32+
return
33+
}
34+
35+
nameNode := node.ChildByFieldName("name")
36+
valueNode := node.ChildByFieldName("value")
37+
38+
// Ensure that the variable definition is valid
39+
if nameNode != nil && nameNode.Type() == "identifier" && valueNode != nil {
40+
varName := nameNode.Content(pass.FileContext.Source)
41+
if varName != "" {
42+
varDefinitions[varName] = valueNode
43+
}
44+
}
45+
})
46+
47+
// Second pass: detect SQL injection vulnerabilities
48+
analysis.Preorder(pass, func(node *sitter.Node) {
49+
if node == nil || node.Type() != "call_expression" {
50+
return
51+
}
52+
53+
funcNode := node.ChildByFieldName("function")
54+
if funcNode == nil || funcNode.Type() != "member_expression" {
55+
return
56+
}
57+
58+
propertyNode := funcNode.ChildByFieldName("property")
59+
if propertyNode == nil {
60+
return
61+
}
62+
63+
// Extract the function name
64+
funcName := propertyNode.Content(pass.FileContext.Source)
65+
66+
// Check if this is a function that executes raw SQL
67+
if !vulnerableFunctions[funcName] {
68+
return
69+
}
70+
71+
// Get the arguments of the function
72+
args := node.ChildByFieldName("arguments")
73+
if args == nil || args.NamedChildCount() == 0 {
74+
return
75+
}
76+
77+
// Get the first argument
78+
firstArg := args.NamedChild(0)
79+
if firstArg == nil {
80+
return
81+
}
82+
83+
// Check if the argument is vulnerable
84+
if isSQLInjectionVulnerable(firstArg, pass.FileContext.Source, varDefinitions) {
85+
pass.Report(pass, node, "Potential SQL injection vulnerability detected, use parameterized queries instead")
86+
}
87+
})
88+
89+
return nil, nil
90+
}
91+
92+
func isSQLInjectionVulnerable(node *sitter.Node, sourceCode []byte, varDefs map[string]*sitter.Node) bool {
93+
if node == nil {
94+
return false
95+
}
96+
97+
switch node.Type() {
98+
case "binary_expression":
99+
// Check for string concatenation
100+
left := node.ChildByFieldName("left")
101+
right := node.ChildByFieldName("right")
102+
103+
// If either side is an identifier, this could be user input
104+
if (left != nil && left.Type() == "identifier") ||
105+
(right != nil && right.Type() == "identifier") {
106+
return true
107+
}
108+
109+
// Recursively check both sides
110+
return isSQLInjectionVulnerable(left, sourceCode, varDefs) ||
111+
isSQLInjectionVulnerable(right, sourceCode, varDefs)
112+
113+
case "template_string":
114+
// Check for template strings with interpolation
115+
for i := range int(node.NamedChildCount()) {
116+
child := node.NamedChild(i)
117+
if child != nil && child.Type() == "template_substitution" {
118+
return true
119+
}
120+
}
121+
122+
case "identifier":
123+
// If it's a variable, check its definition
124+
varName := node.Content(sourceCode)
125+
if defNode, exists := varDefs[varName]; exists {
126+
return isSQLInjectionVulnerable(defNode, sourceCode, varDefs)
127+
} else { // If definition is not found, assume it to be vulnerable
128+
return true
129+
}
130+
}
131+
132+
return false
133+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// VULNERABLE PATTERNS
2+
3+
// Direct string concatenation/interpolation with user input
4+
const v1 = "SELECT * FROM users WHERE username = '" + username + "'";
5+
const v2 = `SELECT * FROM users WHERE email = '${email}' AND phone = ${phone}`;
6+
7+
// <expect-error>
8+
connection.query(v1);
9+
10+
// <expect-error>
11+
pool.query(v2);
12+
13+
// Variable defined elsewhere
14+
// <expect-error>
15+
connection.query(v3);
16+
17+
// ORMs with raw queries
18+
// <expect-error>
19+
sequelize.query(`SELECT * FROM products WHERE category = '${category}'`, {
20+
type: sequelize.QueryTypes.SELECT,
21+
});
22+
23+
// <expect-error>
24+
knex.raw(`SELECT * FROM users WHERE id = ${userId}`);
25+
26+
// <expect-error>
27+
knex.raw(`SELECT * FROM users WHERE id = ${userId}` + `AND email = ${email}`);
28+
29+
// <expect-error>
30+
const users = await prisma.$queryRawUnsafe(
31+
`SELECT * FROM ${table} WHERE id = ${id}`,
32+
);
33+
34+
// <expect-error>
35+
const result = await prisma.$executeRawUnsafe(
36+
`DELETE FROM users WHERE email = '${email}'`,
37+
);
38+
39+
// SAFE PATTERNS
40+
41+
connection.query(s1, "SELECT * FROM user WHERE name LIKE 'A%'");
42+
43+
// Parameterized queries
44+
const s1 = "SELECT * FROM users WHERE username = ?";
45+
connection.query(s1, [username]);
46+
47+
const s2 = "SELECT * FROM users WHERE email = $1 AND phone = $2";
48+
pool.query(s2, [email, phone]);
49+
50+
// Prepared statements
51+
const preparedStatement = connection.prepare(
52+
"SELECT * FROM users WHERE id = ?",
53+
);
54+
preparedStatement.execute([userId]);

0 commit comments

Comments
 (0)