Skip to content

Commit fbc65b3

Browse files
authored
Merge pull request #558 from sauyon/add-sample-queries
Add sample DB-related queries
2 parents 65e9262 + 4c5d3ff commit fbc65b3

37 files changed

Lines changed: 2131 additions & 0 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import "gorm.io/gorm"
4+
5+
func getUsers(db *gorm.DB, names []string) []User {
6+
res := make([]User, 0, len(names))
7+
for _, name := range names {
8+
var user User
9+
db.Where("name = ?", name).First(&user)
10+
res = append(res, user)
11+
}
12+
return res
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>Database calls in loops are slower than running a single query and consume more resources. This
8+
can lead to denial of service attacks if the loop bounds can be controlled by an attacker.</p>
9+
</overview>
10+
<recommendation>
11+
12+
<p>Ensure that where possible, database queries are not run in a loop, instead running a single query to get all relevant data.</p>
13+
14+
</recommendation>
15+
<example>
16+
17+
<p>In the example below, users in a database are queried one by one in a loop:</p>
18+
19+
<sample src="DatabaseCallInLoop.go" />
20+
21+
<p>This is corrected by running a single query that selects all of the users at once:</p>
22+
23+
<sample src="DatabaseCallInLoopGood.go" />
24+
25+
</example>
26+
<references>
27+
28+
</references>
29+
</qhelp>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @name Database call in loop
3+
* @description Detects database operations within loops.
4+
* Doing operations in series can be slow and lead to N+1 situations.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id go/examples/database-call-in-loop
9+
*/
10+
11+
import go
12+
13+
class DatabaseAccess extends DataFlow::MethodCallNode {
14+
DatabaseAccess() {
15+
exists(string name |
16+
this.getTarget().hasQualifiedName(Gorm::packagePath(), "DB", name) and
17+
// all terminating Gorm methods
18+
name =
19+
[
20+
"Find", "Take", "Last", "Scan", "Row", "Rows", "ScanRows", "Pluck", "Count", "First",
21+
"FirstOrInit", "FindOrCreate", "Update", "Updates", "UpdateColumn", "UpdateColumns",
22+
"Save", "Create", "Delete", "Exec"
23+
]
24+
)
25+
}
26+
}
27+
28+
class CallGraphNode extends Locatable {
29+
CallGraphNode() {
30+
this instanceof LoopStmt
31+
or
32+
this instanceof CallExpr
33+
or
34+
this instanceof FuncDef
35+
}
36+
}
37+
38+
/**
39+
* Holds if `pred` calls `succ`, i.e. is an edge in the call graph,
40+
* This includes explicit edges from call -> callee, to produce better paths.
41+
*/
42+
predicate callGraphEdge(CallGraphNode pred, CallGraphNode succ) {
43+
// Go from a loop to an enclosed expression.
44+
pred.(LoopStmt).getBody().getAChild*() = succ.(CallExpr)
45+
or
46+
// Go from a call to the called function.
47+
pred.(CallExpr) = succ.(FuncDef).getACall().asExpr()
48+
or
49+
// Go from a function to an enclosed loop.
50+
pred.(FuncDef) = succ.(LoopStmt).getEnclosingFunction()
51+
or
52+
// Go from a function to an enclosed call.
53+
pred.(FuncDef) = succ.(CallExpr).getEnclosingFunction()
54+
}
55+
56+
query predicate edges(CallGraphNode pred, CallGraphNode succ) {
57+
callGraphEdge(pred, succ) and
58+
// Limit the range of edges to only those that are relevant.
59+
// This helps to speed up the query by reducing the size of the outputted path information.
60+
exists(LoopStmt loop, DatabaseAccess dbAccess |
61+
// is between a loop and a db access
62+
callGraphEdge*(loop, pred) and
63+
callGraphEdge*(succ, dbAccess.asExpr())
64+
)
65+
}
66+
67+
from LoopStmt loop, DatabaseAccess dbAccess
68+
where edges*(loop, dbAccess.asExpr())
69+
select dbAccess, loop, dbAccess, "$@ is called in $@", dbAccess, dbAccess.toString(), loop, "a loop"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
import "gorm.io/gorm"
4+
5+
func getUsersGood(db *gorm.DB, names []string) []User {
6+
res := make([]User, 0, len(names))
7+
db.Where("name IN ?", names).Find(&res)
8+
return res
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package main
2+
3+
import "os"
4+
5+
func openFiles(filenames []string) {
6+
for _, filename := range filenames {
7+
file, err := os.Open(filename)
8+
defer file.Close()
9+
if err != nil {
10+
// handle error
11+
}
12+
// work on file
13+
}
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>A deferred statement in a loop will not execute until the end of the function. This can lead to unintentionally holding resources open, like file handles or database transactions.</p>
8+
</overview>
9+
<recommendation>
10+
11+
<p>Either run the deferred function manually, or create a subroutine that contains the defer.</p>
12+
13+
</recommendation>
14+
<example>
15+
16+
<p>In the example below, the files opened in the loop are not closed until the end of the function:</p>
17+
18+
<sample src="DeferInLoop.go" />
19+
20+
<p>The corrected version puts the loop body into a function.</p>
21+
22+
<sample src="DeferInLoopGood.go" />
23+
24+
</example>
25+
<references>
26+
27+
<li>
28+
<a href="https://golang.org/ref/spec#Defer_statements">Defer statements</a>.
29+
</li>
30+
31+
</references>
32+
</qhelp>
File renamed without changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import "os"
4+
5+
func openFile(filename string) {
6+
file, err := os.Open(filename)
7+
defer file.Close()
8+
if err != nil {
9+
// handle error
10+
}
11+
// work on file
12+
}
13+
14+
func openFilesGood(filenames []string) {
15+
for _, filename := range filenames {
16+
openFile(filename)
17+
}
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
import "gorm.io/gorm"
4+
5+
func getUserId(db *gorm.DB, name string) int64 {
6+
var user User
7+
db.Where("name = ?", name).First(&user)
8+
return user.Id
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>GORM errors are returned as a field of the return value instead of a separate return value.</p>
8+
9+
<p>It is therefore very easy to miss that an error may occur and omit error handling routines.</p>
10+
</overview>
11+
<recommendation>
12+
13+
<p>Ensure that GORM errors are checked.</p>
14+
15+
</recommendation>
16+
<example>
17+
18+
<p>In the example below, the error from the database query is never checked:</p>
19+
20+
<sample src="GORMErrorNotChecked.go" />
21+
22+
<p>The corrected version checks and handles the error before returning.</p>
23+
24+
<sample src="GORMErrorNotCheckedGood.go" />
25+
26+
</example>
27+
<references>
28+
29+
<li>
30+
<a href="https://gorm.io/docs/error_handling.html">GORM Error Handling</a>.
31+
</li>
32+
33+
</references>
34+
</qhelp>

0 commit comments

Comments
 (0)