Skip to content
This repository was archived by the owner on Sep 21, 2021. It is now read-only.

Commit 8c0f803

Browse files
authored
Merge pull request #16 from codyl-stripe/codyl/add_ignore_comments
Adds ability to ignore false positives
2 parents cddf355 + 200faa8 commit 8c0f803

File tree

9 files changed

+341
-6
lines changed

9 files changed

+341
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,21 @@ constant, but this code has yet to be written.
7272
The second sort of false positive is based on a limitation in the sort of
7373
analysis SafeSQL performs: there are many safe SQL statements which are not
7474
feasible (or not possible) to represent as compile-time constants. More advanced
75-
static analysis techniques (such as taint analysis) or user-provided safety
76-
annotations would be able to reduce the number of false positives, but this is
77-
expected to be a significant undertaking.
75+
static analysis techniques (such as taint analysis).
7876

77+
In order to ignore false positives, add the following comment to the line before
78+
or the same line as the statement:
79+
```
80+
//nolint:safesql
81+
```
82+
83+
Even if a statement is ignored it will still be logged, but will not cause
84+
safesql to exit with a status code of 1 if all found statements are ignored.
85+
86+
Adding tests
87+
---------------
88+
To add a test create a new director in `testdata` and add a go program in the
89+
folder you created, for an example look at `testdata/multiple_files`.
90+
91+
After adding a new directory and go program, add an entry to the tests map in
92+
`safesql_test.go`, which will run the tests against the program added.

safesql.go

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77
"flag"
88
"fmt"
99
"go/build"
10+
"go/token"
1011
"go/types"
12+
"io/ioutil"
1113
"os"
14+
"sort"
1215

1316
"path/filepath"
1417
"strings"
@@ -20,6 +23,8 @@ import (
2023
"golang.org/x/tools/go/ssa/ssautil"
2124
)
2225

26+
const IgnoreComment = "//nolint:safesql"
27+
2328
type sqlPackage struct {
2429
packageName string
2530
paramNames []string
@@ -133,16 +138,37 @@ func main() {
133138
fmt.Printf("Found %d potentially unsafe SQL statements:\n", len(bad))
134139
}
135140

141+
potentialBadStatements := []token.Position{}
136142
for _, ci := range bad {
137-
pos := p.Fset.Position(ci.Pos())
138-
fmt.Printf("- %s\n", pos)
143+
potentialBadStatements = append(potentialBadStatements, p.Fset.Position(ci.Pos()))
144+
}
145+
146+
issues, err := CheckIssues(potentialBadStatements)
147+
if err != nil {
148+
fmt.Printf("error when checking for ignore comments: %v\n", err)
149+
os.Exit(2)
139150
}
151+
140152
if verbose {
141153
fmt.Println("Please ensure that all SQL queries you use are compile-time constants.")
142154
fmt.Println("You should always use parameterized queries or prepared statements")
143155
fmt.Println("instead of building queries from strings.")
144156
}
145-
os.Exit(1)
157+
158+
hasNonIgnoredUnsafeStatement := false
159+
160+
for _, issue := range issues {
161+
if issue.ignored {
162+
fmt.Printf("- %s is potentially unsafe but ignored by comment\n", issue.statement)
163+
} else {
164+
fmt.Printf("- %s\n", issue.statement)
165+
hasNonIgnoredUnsafeStatement = true
166+
}
167+
}
168+
169+
if hasNonIgnoredUnsafeStatement {
170+
os.Exit(1)
171+
}
146172
}
147173

148174
// QueryMethod represents a method on a type which has a string parameter named
@@ -154,6 +180,59 @@ type QueryMethod struct {
154180
Param int
155181
}
156182

183+
type Issue struct {
184+
statement token.Position
185+
ignored bool
186+
}
187+
188+
// CheckIssues checks lines to see if the line before or the current line has an ignore comment and marks those
189+
// statements that have the ignore comment on the current line or the line before
190+
func CheckIssues(lines []token.Position) ([]Issue, error) {
191+
files := make(map[string][]token.Position)
192+
193+
for _, line := range lines {
194+
files[line.Filename] = append(files[line.Filename], line)
195+
}
196+
197+
issues := []Issue{}
198+
199+
for file, linesInFile := range files {
200+
// ensure we have the lines in ascending order
201+
sort.Slice(linesInFile, func(i, j int) bool { return linesInFile[i].Line < linesInFile[j].Line })
202+
203+
data, err := ioutil.ReadFile(file)
204+
if err != nil {
205+
return nil, err
206+
}
207+
fileLines := strings.Split(string(data), "\n")
208+
209+
for _, line := range linesInFile {
210+
// check the line before the problematic statement first
211+
potentialCommentLine := line.Line - 2
212+
213+
// check only if the previous line is strictly a line that begins with
214+
// the ignore comment
215+
if 0 <= potentialCommentLine && BeginsWithComment(fileLines[potentialCommentLine]) {
216+
issues = append(issues, Issue{statement: line, ignored: true})
217+
continue
218+
}
219+
220+
isIgnored := HasIgnoreComment(fileLines[line.Line-1])
221+
issues = append(issues, Issue{statement: line, ignored: isIgnored})
222+
}
223+
}
224+
225+
return issues, nil
226+
}
227+
228+
func BeginsWithComment(line string) bool {
229+
return strings.HasPrefix(strings.TrimSpace(line), IgnoreComment)
230+
}
231+
232+
func HasIgnoreComment(line string) bool {
233+
return strings.HasSuffix(strings.TrimSpace(line), IgnoreComment)
234+
}
235+
157236
// FindQueryMethods locates all methods in the given package (assumed to be
158237
// package database/sql) with a string parameter named "query".
159238
func FindQueryMethods(sqlPackages sqlPackage, sql *types.Package, ssa *ssa.Program) []*QueryMethod {

safesql_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package main
2+
3+
import (
4+
"go/token"
5+
"path"
6+
"reflect"
7+
"sort"
8+
"testing"
9+
)
10+
11+
const testDir = "./testdata"
12+
13+
// TestCheckIssues attempts to see if issues are ignored or not and annotates the issues if they are ignored
14+
func TestCheckIssues(t *testing.T) {
15+
tests := map[string]struct{
16+
tokens []token.Position
17+
expected []Issue
18+
}{
19+
"all_ignored": {
20+
tokens: []token.Position{
21+
token.Position{Filename:"main.go", Line: 23, Column: 5 },
22+
token.Position{Filename:"main.go", Line: 29, Column: 5 },
23+
},
24+
expected: []Issue{
25+
Issue{statement: token.Position{Filename:"main.go", Line: 23, Column: 5 }, ignored: true},
26+
Issue{statement: token.Position{Filename:"main.go", Line: 29, Column: 5 }, ignored: true},
27+
},
28+
},
29+
"ignored_back_to_back": {
30+
tokens: []token.Position{
31+
token.Position{Filename:"main.go", Line: 22, Column: 5 },
32+
token.Position{Filename:"main.go", Line: 23, Column: 5 },
33+
},
34+
expected: []Issue{
35+
Issue{statement: token.Position{Filename:"main.go", Line: 22, Column: 5 }, ignored: true},
36+
Issue{statement: token.Position{Filename:"main.go", Line: 23, Column: 5 }, ignored: false},
37+
},
38+
},
39+
"single_ignored": {
40+
tokens: []token.Position{
41+
token.Position{Filename:"main.go", Line: 23, Column: 5 },
42+
token.Position{Filename:"main.go", Line: 29, Column: 5 },
43+
},
44+
expected: []Issue{
45+
Issue{statement: token.Position{Filename:"main.go", Line: 23, Column: 5 }, ignored: true},
46+
Issue{statement: token.Position{Filename:"main.go", Line: 29, Column: 5 }, ignored: false},
47+
},
48+
},
49+
"multiple_files": {
50+
tokens: []token.Position{
51+
token.Position{Filename:"main.go", Line: 23, Column: 5 },
52+
token.Position{Filename:"main.go", Line: 24, Column: 5 },
53+
token.Position{Filename:"helpers.go", Line: 16, Column: 5 },
54+
token.Position{Filename:"helpers.go", Line: 17, Column: 5 },
55+
},
56+
expected: []Issue{
57+
Issue{statement: token.Position{Filename:"main.go", Line: 23, Column: 5 }, ignored: true},
58+
Issue{statement: token.Position{Filename:"main.go", Line: 24, Column: 5 }, ignored: true},
59+
Issue{statement: token.Position{Filename:"helpers.go", Line: 16, Column: 5 }, ignored: true},
60+
Issue{statement: token.Position{Filename:"helpers.go", Line: 17, Column: 5 }, ignored: true},
61+
},
62+
},
63+
}
64+
65+
for name, expectations := range tests {
66+
t.Run(name, func(t *testing.T) {
67+
for idx, pos := range expectations.tokens {
68+
expectations.tokens[idx].Filename = path.Join(testDir, name, pos.Filename)
69+
}
70+
for idx, issue := range expectations.expected {
71+
expectations.expected[idx].statement.Filename = path.Join(testDir, name, issue.statement.Filename)
72+
}
73+
74+
issues, err := CheckIssues(expectations.tokens)
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
79+
if len(issues) != len(expectations.expected) {
80+
t.Fatal("The expected number of issues was not found")
81+
}
82+
83+
// sort to ensure we have the same issue order
84+
sort.Slice(expectations.expected, func(i, j int) bool {return expectations.expected[i].statement.Filename < expectations.expected[j].statement.Filename })
85+
sort.Slice(issues, func(i, j int) bool {return issues[i].statement.Filename < issues[j].statement.Filename })
86+
87+
for idx, expected := range expectations.expected {
88+
actual := issues[idx]
89+
if !reflect.DeepEqual(actual, expected) {
90+
t.Errorf("The actual value of %v did not match the expected %v", actual, expected)
91+
}
92+
}
93+
})
94+
}
95+
}

testdata/all_ignored/main.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func main() {
9+
fmt.Println(query("'test' OR 1=1"))
10+
}
11+
12+
const GetAllQuery = "SELECT COUNT(*) FROM t WHERE arg=%s"
13+
14+
// All issues are ignored in this test
15+
func query(arg string) error {
16+
db, err := sql.Open("postgres", "postgresql://test:test@test")
17+
if err != nil {
18+
return err
19+
}
20+
21+
query := fmt.Sprintf(GetAllQuery, arg)
22+
//nolint:safesql
23+
row := db.QueryRow(query)
24+
var count int
25+
if err := row.Scan(&count); err != nil {
26+
return err
27+
}
28+
29+
row = db.QueryRow(fmt.Sprintf(GetAllQuery, "Catch me please?")) //nolint:safesql
30+
if err := row.Scan(&count); err != nil {
31+
return err
32+
}
33+
34+
return nil
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func main() {
9+
fmt.Println(query("'test' OR 1=1"))
10+
}
11+
12+
const GetAllQuery = "SELECT COUNT(*) FROM t WHERE arg=%s"
13+
14+
// For this test we expect the second QueryRow to be an issue even though the line before has a comment
15+
func query(arg string) error {
16+
db, err := sql.Open("postgres", "postgresql://test:test@test")
17+
if err != nil {
18+
return err
19+
}
20+
21+
query := fmt.Sprintf(GetAllQuery, arg)
22+
_ := db.QueryRow(query) //nolint:safesql
23+
_ := db.QueryRow(fmt.Sprintf(GetAllQuery, "Catch me please?"))
24+
25+
26+
return nil
27+
}

testdata/multiple_files/helpers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
// For this test we expect the second QueryRow to be an issue even though the line before has a comment
9+
func query(arg string) error {
10+
db, err := sql.Open("postgres", "postgresql://test:test@test")
11+
if err != nil {
12+
return err
13+
}
14+
15+
query := fmt.Sprintf(GetAllQuery, arg)
16+
_ := db.QueryRow(query) //nolint:safesql
17+
_ := db.QueryRow(fmt.Sprintf(GetAllQuery, "Catch me please?")) //nolint:safesql
18+
19+
20+
return nil
21+
}

testdata/multiple_files/main.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func main() {
9+
fmt.Println(query("'test' OR 1=1"))
10+
fmt.Println(query2("'test' OR 1=1"))
11+
}
12+
13+
const GetAllQuery = "SELECT COUNT(*) FROM t WHERE arg=%s"
14+
15+
// For this test we expect the second QueryRow to be an issue even though the line before has a comment
16+
func query2(arg string) error {
17+
db, err := sql.Open("postgres", "postgresql://test:test@test")
18+
if err != nil {
19+
return err
20+
}
21+
22+
query := fmt.Sprintf(GetAllQuery, arg)
23+
_ := db.QueryRow(query) //nolint:safesql
24+
_ := db.QueryRow(fmt.Sprintf(GetAllQuery, "Catch me please?")) //nolint:safesql
25+
26+
27+
return nil
28+
}

0 commit comments

Comments
 (0)