Skip to content

Commit 3d62447

Browse files
committed
all: detect diverse nested http.Error + misuse of http.NotFound too
With this change, we simplify the code radically, no need to construct an incidence matrix, but instead, on failing to find a terminating statement within a block, we'll now walk each block's successors, and if there is any statement that's not a defer nor a terminating statement, report that error.
1 parent b1f5b06 commit 3d62447

2 files changed

Lines changed: 56 additions & 56 deletions

File tree

httperroryzer.go

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ return after a call to http.Error
3939
// Code that assumes the error was properly handled.
4040
slurp, _ := ioutil.ReadAll(res.Body)
4141
42-
This checker helps uncover latent nil dereference bugs by reporting a
42+
This checker helps uncover latent nil dereference bugs, or security problems by reporting a
4343
diagnostic for such mistakes.`
4444

4545
var Analyzer = &analysis.Analyzer{
@@ -88,22 +88,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
8888
// Let's inspect its control flow graph.
8989
acfg := cfgs.FuncDecl(fnDecl)
9090

91-
explorable := make(map[int32]bool)
92-
for _, block := range acfg.Blocks {
93-
explorable[block.Index] = true
94-
}
95-
// Partition the graph, by deleting the roots accessible by the
96-
// entry block, so that the two roots can never be connected.
97-
for _, block := range acfg.Blocks[0].Succs {
98-
delete(explorable, block.Index)
99-
}
100-
101-
// Now that the roots are deleted, we can build the incidence graph with no more problems.
102-
partitionedButConnectedBlocks := make(incidence)
103-
for _, block := range acfg.Blocks[0].Succs {
104-
buildIncidence(block, explorable, partitionedButConnectedBlocks)
105-
}
106-
10791
for _, block := range acfg.Blocks {
10892
if retStmt := block.Return(); retStmt != nil {
10993
continue
@@ -129,7 +113,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
129113
ident = t.Fun.(*ast.Ident)
130114
}
131115

132-
if ident != nil && identMatches(pass, ident, "http.Error") {
116+
if ident != nil && identMatches(pass, ident, responseWriterRepliers...) {
133117
firstHTTPErrorIndex = i
134118
break
135119
}
@@ -140,6 +124,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
140124
continue
141125
}
142126

127+
var succs []*cfg.Block
143128
tillEndOfBlock := block.Nodes[firstHTTPErrorIndex+1:]
144129
// First attempt is to try to find any terminating statements in the same block as the
145130
// http.Error statement, for example:
@@ -155,38 +140,29 @@ func run(pass *analysis.Pass) (interface{}, error) {
155140
}
156141
}
157142

158-
// The last attempt is to find partitioned but connected blocks that
159-
// might have return statements or terminating statements in them.
160-
// In this case, let's retrieve all the block indices accessible after we fall through
161-
// this block or in the next/larger scope e.g.
162-
// if errors.Is(err, os.ErrNotExist) {
163-
// http.NotFound(rw, req)
164-
// } else {
165-
// http.Error(rw, "cannot load archive", 500)
166-
// }
167-
// return
143+
// Check if the successors don't have terminating statements.
144+
succs = append(succs, block.Succs...)
145+
for len(succs) > 0 {
146+
succ := succs[0]
147+
succs = succs[1:]
168148

169-
for _, index := range partitionedButConnectedBlocks[block.Index] {
170-
// Does the block have a return statement.
171-
blocksToExplore := []*cfg.Block{acfg.Blocks[index]}
172-
if false {
173-
for _, subIndex := range partitionedButConnectedBlocks[index] {
174-
blocksToExplore = append(blocksToExplore, acfg.Blocks[subIndex])
175-
}
176-
}
177-
for _, cBlock := range blocksToExplore {
178-
if cBlock.Return() != nil {
149+
for _, node := range succ.Nodes {
150+
switch node.(type) {
151+
case *ast.ReturnStmt:
179152
goto done
180-
}
181-
// Now check if any of the nodes in there have terminating statements.
182-
for _, node := range cBlock.Nodes {
153+
case *ast.DeferStmt:
154+
continue
155+
default:
183156
if isTerminatingStmt(pass, node) {
184157
goto done
185158
}
159+
goto failed
186160
}
187161
}
162+
succs = append(succs, succ.Succs...)
188163
}
189164

165+
failed:
190166
// We did not find a terminating statement in this block.
191167
pass.ReportRangef(block.Nodes[firstHTTPErrorIndex], "call to http.Error without a terminating statement below it")
192168
done:
@@ -195,6 +171,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
195171
return nil, nil
196172
}
197173

174+
var responseWriterRepliers = []string{"http.Error", "http.NotFound"}
175+
198176
// Check that the function arguments contain:
199177
// http.ResponseWriter
200178
func responseWriterInParams(pass *analysis.Pass, fnDecl *ast.FuncDecl) bool {
@@ -208,19 +186,6 @@ func responseWriterInParams(pass *analysis.Pass, fnDecl *ast.FuncDecl) bool {
208186
return false
209187
}
210188

211-
type incidence map[int32][]int32
212-
213-
// buildIncidence traverses a block's successive neighbors, using explorable as a guide
214-
// for a well partitioned directed graph.
215-
func buildIncidence(discover *cfg.Block, explorable map[int32]bool, ind incidence) {
216-
for _, succ := range discover.Succs {
217-
if explorable[succ.Index] {
218-
ind[discover.Index] = append(ind[discover.Index], succ.Index)
219-
buildIncidence(succ, explorable, ind)
220-
}
221-
}
222-
}
223-
224189
func isTerminatingStmt(pass *analysis.Pass, n ast.Node) bool {
225190
if n == nil {
226191
return false

testdata/src/b/b.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func proxyHandleWithoutReturn(rw http.ResponseWriter, req *http.Request) {
1414
fmt.Fprintf(os.Stderr, "go proxy: no archive: %v\n", err)
1515
}
1616
if errors.Is(err, os.ErrNotExist) {
17-
http.NotFound(rw, req)
17+
http.NotFound(rw, req) // want "call to http.Error without a terminating statement below it"
1818
} else {
1919
http.Error(rw, "cannot load archive", 500) // want "call to http.Error without a terminating statement below it"
2020
}
@@ -40,7 +40,7 @@ func proxyHandleWithReturn(rw http.ResponseWriter, req *http.Request) {
4040
fmt.Fprintf(os.Stderr, "go proxy: no archive: %v\n", err)
4141
}
4242
if errors.Is(err, os.ErrNotExist) {
43-
http.NotFound(rw, req)
43+
http.NotFound(rw, req) // want "call to http.Error without a terminating statement below it"
4444
} else {
4545
http.Error(rw, "cannot load archive", 500) // want "call to http.Error without a terminating.+"
4646
}
@@ -61,6 +61,41 @@ func proxyHandleWithReturn(rw http.ResponseWriter, req *http.Request) {
6161
}
6262
}
6363

64+
func notFoundReplier(rw http.ResponseWriter, req *http.Request) {
65+
if req.Header.Get("failalways") != "" {
66+
http.NotFound(rw, req) // want "call to http.Error without a terminating.+"
67+
}
68+
rw.Write([]byte("FOO"))
69+
}
70+
71+
func notFoundReplierWithReturn(rw http.ResponseWriter, req *http.Request) {
72+
if req.Header.Get("failalways") != "" {
73+
http.NotFound(rw, req)
74+
return
75+
}
76+
rw.Write([]byte("FOO"))
77+
}
78+
79+
func foo(rw http.ResponseWriter, req *http.Request) {
80+
http.Error(rw, "msg", 404)
81+
}
82+
83+
func notFoundReplierWithoutReturn(rw http.ResponseWriter, req *http.Request) {
84+
if req.Header.Get("failalways") != "" {
85+
http.NotFound(rw, req)
86+
}
87+
}
88+
89+
func notFoundReplierWithBranch(rw http.ResponseWriter, req *http.Request) {
90+
if req.Header.Get("failalways") != "" {
91+
rw.WriteHeader(200)
92+
http.NotFound(rw, req)
93+
rw.Write([]byte("fizz buzz"))
94+
} else {
95+
http.Error(rw, "ditto", http.StatusBadRequest)
96+
}
97+
}
98+
6499
func do() error {
65100
return errors.New("foo")
66101
}

0 commit comments

Comments
 (0)