Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit c8a2d30

Browse files
authored
Merge pull request #360 from smowton/smowton/feature/stack-trace-exposure
Add stack-trace exposure query
2 parents d7dcf27 + 0eb7ac9 commit c8a2d30

File tree

7 files changed

+183
-0
lines changed

7 files changed

+183
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* A new query `go/stack-trace-exposure` has been added. The query flags exposure of a stack trace to a remote party.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package example
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"runtime"
7+
)
8+
9+
func handlePanic(w http.ResponseWriter, r *http.Request) {
10+
buf := make([]byte, 2<<16)
11+
buf = buf[:runtime.Stack(buf, true)]
12+
// BAD: printing a stack trace back to the response
13+
w.Write(buf)
14+
// GOOD: logging the response to the server and sending
15+
// a more generic message.
16+
log.Printf("Panic: %s", buf)
17+
w.Write([]byte("An unexpected runtime error occurred"))
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Software developers often add stack traces to error messages, as a
7+
debugging aid. Whenever that error message occurs for an end user, the
8+
developer can use the stack trace to help identify how to fix the
9+
problem. In particular, stack traces can tell the developer more about
10+
the sequence of events that led to a failure, as opposed to merely the
11+
final state of the software when the error occurred.</p>
12+
13+
<p>Unfortunately, the same information can be useful to an attacker.
14+
The sequence of class names in a stack trace can reveal the structure
15+
of the application as well as any internal components it relies on.</p>
16+
</overview>
17+
18+
<recommendation>
19+
<p>Send the user a more generic error message that reveals less information.
20+
Either suppress the stack trace entirely, or log it only on the server.</p>
21+
</recommendation>
22+
23+
<example>
24+
<p>In the following example, a panic is handled in two different
25+
ways. In the first version, labeled BAD, a detailed stack trace is
26+
written to a user-facing HTTP response object, which may disclose
27+
sensitive information. In the second version, the error message is
28+
logged only on the server. That way, the developers can still access
29+
and use the error log, but remote users will not see the
30+
information.</p>
31+
32+
<sample src="StackTraceExposure.go" />
33+
</example>
34+
35+
<references>
36+
<li>OWASP: <a href="https://owasp.org/www-community/Improper_Error_Handling">Improper Error Handling</a>.</li>
37+
</references>
38+
</qhelp>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @name Information exposure through a stack trace
3+
* @description Information from a stack trace propagates to an external user.
4+
* Stack traces can unintentionally reveal implementation details
5+
* that are useful to an attacker for developing a subsequent exploit.
6+
* @kind path-problem
7+
* @problem.severity error
8+
* @precision high
9+
* @id go/stack-trace-exposure
10+
* @tags security
11+
* external/cwe/cwe-209
12+
* external/cwe/cwe-497
13+
*/
14+
15+
import go
16+
import semmle.go.security.InsecureFeatureFlag::InsecureFeatureFlag
17+
import DataFlow::PathGraph
18+
19+
/**
20+
* A flag indicating the program is in debug or development mode, or that stack
21+
* dumps have been specifically enabled.
22+
*/
23+
class DebugModeFlag extends FlagKind {
24+
DebugModeFlag() { this = "debugMode" }
25+
26+
bindingset[result]
27+
override string getAFlagName() {
28+
result.regexpMatch("(?i).*(trace|debug|devel|(enable|disable|print)stack).*")
29+
}
30+
}
31+
32+
/**
33+
* The function `runtime.Stack`, which emits a stack trace.
34+
*/
35+
class StackFunction extends Function {
36+
StackFunction() { this.hasQualifiedName("runtime", "Stack") }
37+
}
38+
39+
/**
40+
* The function `runtime/debug.Stack`, which emits a stack trace.
41+
*/
42+
class DebugStackFunction extends Function {
43+
DebugStackFunction() { this.hasQualifiedName("runtime/debug", "Stack") }
44+
}
45+
46+
/**
47+
* A taint-tracking configuration that looks for stack traces being written to
48+
* an HTTP response body without an intervening debug- or development-mode conditional.
49+
*/
50+
class StackTraceExposureConfig extends TaintTracking::Configuration {
51+
StackTraceExposureConfig() { this = "StackTraceExposureConfig" }
52+
53+
override predicate isSource(DataFlow::Node node) {
54+
node.(DataFlow::PostUpdateNode).getPreUpdateNode() =
55+
any(StackFunction f).getACall().getArgument(0) or
56+
node = any(DebugStackFunction f).getACall().getResult()
57+
}
58+
59+
override predicate isSink(DataFlow::Node node) { node instanceof HTTP::ResponseBody }
60+
61+
override predicate isSanitizer(DataFlow::Node node) {
62+
// Sanitise everything controlled by an is-debug-mode check.
63+
// Imprecision: I don't try to guess which arm of a branch is intended
64+
// to mean debug mode, and which is production mode.
65+
exists(ControlFlow::ConditionGuardNode cgn |
66+
cgn.ensures(any(DebugModeFlag f).getAFlag().getANode(), _)
67+
|
68+
cgn.dominates(node.getBasicBlock())
69+
)
70+
}
71+
}
72+
73+
from StackTraceExposureConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
74+
where cfg.hasFlowPath(source, sink)
75+
select source.getNode(), source, sink, "This stack trace is exposed to a remote user $@.",
76+
sink.getNode(), "here"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
edges
2+
| test.go:14:2:14:4 | definition of buf : slice type | test.go:17:10:17:12 | buf |
3+
nodes
4+
| test.go:14:2:14:4 | definition of buf : slice type | semmle.label | definition of buf : slice type |
5+
| test.go:17:10:17:12 | buf | semmle.label | buf |
6+
#select
7+
| test.go:14:2:14:4 | definition of buf | test.go:14:2:14:4 | definition of buf : slice type | test.go:17:10:17:12 | buf | This stack trace is exposed to a remote user $@. | test.go:17:10:17:12 | buf | here |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-209/StackTraceExposure.ql
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package test
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"runtime"
7+
)
8+
9+
var debug, development, trace, enableStackTrace, disableStackTrace, printStackTrace bool
10+
11+
var logger log.Logger
12+
13+
func handlePanic(w http.ResponseWriter, r *http.Request) {
14+
buf := make([]byte, 2<<16)
15+
buf = buf[:runtime.Stack(buf, true)]
16+
// BAD: printing a stack trace back to the response
17+
w.Write(buf)
18+
// GOOD: logging the response to the server and sending
19+
// a more generic message.
20+
logger.Printf("Panic: %s", buf)
21+
w.Write([]byte("An unexpected runtime error occurred"))
22+
// GOOD: guarding remote stack dumps with tests that suggest an opt-in debug mode:
23+
if debug {
24+
w.Write(buf)
25+
}
26+
if development {
27+
w.Write(buf)
28+
}
29+
if trace {
30+
w.Write(buf)
31+
}
32+
if enableStackTrace {
33+
w.Write(buf)
34+
}
35+
if !disableStackTrace {
36+
w.Write(buf) // Note our analysis doesn't actually check this branch goes the right way
37+
}
38+
if printStackTrace {
39+
w.Write(buf)
40+
}
41+
}

0 commit comments

Comments
 (0)