Skip to content

Commit 8206734

Browse files
authored
Merge pull request github#13119 from porcupineyhairs/goTiming
Go : Add query to detect potential timing attacks
2 parents 4885e58 + 99f4eef commit 8206734

File tree

8 files changed

+178
-1
lines changed

8 files changed

+178
-1
lines changed

go/ql/lib/semmle/go/security/SensitiveActions.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module HeuristicNames {
3535
*/
3636
string maybePassword() {
3737
result = "(?is).*pass(wd|word|code|phrase)(?!.*question).*" or
38-
result = "(?is).*(auth(entication|ori[sz]ation)?)key.*"
38+
result = "(?is).*(auth(entication|ori[sz]ation)?|api)key.*"
3939
}
4040

4141
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Using a non-constant time comparision to compare sensitive information can lead to auth
6+
vulnerabilities.
7+
</p>
8+
</overview>
9+
10+
<recommendation>
11+
<p>Use of a constant time comparision function such as <code>crypto/subtle</code> package's <code>
12+
ConstantTimeCompare</code> function can prevent this vulnerability. </p>
13+
</recommendation>
14+
15+
<example>
16+
<p>In the following examples, the code accepts a secret via a HTTP header in variable <code>
17+
secretHeader</code> and a secret from the user in the <code>headerSecret</code> variable, which
18+
are then compared with a system stored secret to perform authentication.</p>
19+
20+
21+
<sample src="timingBad.go" />
22+
23+
<p>In the following example, the input provided by the user is compared using the <code>
24+
ConstantTimeComapre</code> function. This ensures that timing attacks are not possible in this
25+
case.</p>
26+
27+
<sample src="timingGood.go" />
28+
</example>
29+
30+
<references>
31+
<li>National Vulnerability Database: <a href="https://nvd.nist.gov/vuln/detail/CVE-2022-24912">
32+
CVE-2022-24912</a>.</li>
33+
<li>Verbose Logging:<a href="https://verboselogging.com/2012/08/20/a-timing-attack-in-action"> A
34+
timing attack in action </a></li>
35+
</references>
36+
</qhelp>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @name Timing attacks due to comparison of sensitive secrets
3+
* @description using a non-constant time comparison method to compare secrets can lead to authoriztion vulnerabilities
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @id go/timing-attack
7+
* @tags security
8+
* experimental
9+
* external/cwe/cwe-203
10+
*/
11+
12+
import go
13+
import DataFlow::PathGraph
14+
import semmle.go.security.SensitiveActions
15+
16+
private predicate isBadResult(DataFlow::Node e) {
17+
exists(string path | path = e.asExpr().getFile().getAbsolutePath().toLowerCase() |
18+
path.matches(["%fake%", "%dummy%", "%test%", "%example%"]) and not path.matches("%ql/test%")
19+
)
20+
}
21+
22+
/**
23+
* A data flow sink for timing attack vulnerabilities.
24+
*/
25+
abstract class Sink extends DataFlow::Node { }
26+
27+
/** A taint-tracking sink which models comparisons of sensitive variables. */
28+
private class SensitiveCompareSink extends Sink {
29+
ComparisonExpr c;
30+
31+
SensitiveCompareSink() {
32+
// We select a comparison where a secret or password is tested.
33+
exists(SensitiveVariableAccess op1, Expr op2 |
34+
op1.getClassification() = [SensitiveExpr::secret(), SensitiveExpr::password()] and
35+
// exclude grant to avoid FP from OAuth
36+
not op1.getClassification().matches("%grant%") and
37+
op1 = c.getAnOperand() and
38+
op2 = c.getAnOperand() and
39+
not op1 = op2 and
40+
not (
41+
// Comparisons with `nil` should be excluded.
42+
op2 = Builtin::nil().getAReference()
43+
or
44+
// Comparisons with empty string should also be excluded.
45+
op2.getStringValue().length() = 0
46+
)
47+
|
48+
// It is important to note that the name of both the operands need not be
49+
// `sensitive`. Even if one of the operands appears to be sensitive, we consider it a potential sink.
50+
c.getAnOperand() = this.asExpr()
51+
)
52+
}
53+
54+
DataFlow::Node getOtherOperand() { result.asExpr() = c.getAnOperand() and not result = this }
55+
}
56+
57+
class SecretTracking extends TaintTracking::Configuration {
58+
SecretTracking() { this = "SecretTracking" }
59+
60+
override predicate isSource(DataFlow::Node source) {
61+
source instanceof UntrustedFlowSource and not isBadResult(source)
62+
}
63+
64+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink and not isBadResult(sink) }
65+
}
66+
67+
from SecretTracking cfg, DataFlow::PathNode source, DataFlow::PathNode sink
68+
where
69+
cfg.hasFlowPath(source, sink) and
70+
not cfg.hasFlowTo(sink.getNode().(SensitiveCompareSink).getOtherOperand())
71+
select sink.getNode(), source, sink, "$@ may be vulnerable to timing attacks.", source.getNode(),
72+
"Hardcoded String"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
func bad(w http.ResponseWriter, req *http.Request, []byte secret) (interface{}, error) {
2+
3+
secretHeader := "X-Secret"
4+
5+
headerSecret := req.Header.Get(secretHeader)
6+
secretStr := string(secret)
7+
if len(secret) != 0 && headerSecret != secretStr {
8+
return nil, fmt.Errorf("header %s=%s did not match expected secret", secretHeader, headerSecret)
9+
}
10+
return nil, nil
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
func good(w http.ResponseWriter, req *http.Request, []byte secret) (interface{}, error) {
2+
3+
secretHeader := "X-Secret"
4+
5+
headerSecret := req.Header.Get(secretHeader)
6+
if len(secret) != 0 && subtle.ConstantTimeCompare(secret, []byte(headerSecret)) != 1 {
7+
return nil, fmt.Errorf("header %s=%s did not match expected secret", secretHeader, headerSecret)
8+
}
9+
return nil, nil
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
edges
2+
| timing.go:14:18:14:27 | selection of Header | timing.go:14:18:14:45 | call to Get |
3+
| timing.go:14:18:14:45 | call to Get | timing.go:16:25:16:36 | headerSecret |
4+
nodes
5+
| timing.go:14:18:14:27 | selection of Header | semmle.label | selection of Header |
6+
| timing.go:14:18:14:45 | call to Get | semmle.label | call to Get |
7+
| timing.go:16:25:16:36 | headerSecret | semmle.label | headerSecret |
8+
subpaths
9+
#select
10+
| timing.go:16:25:16:36 | headerSecret | timing.go:14:18:14:27 | selection of Header | timing.go:16:25:16:36 | headerSecret | $@ may be vulnerable to timing attacks. | timing.go:14:18:14:27 | selection of Header | Hardcoded String |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/CWE-203/Timing.ql
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"crypto/subtle"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
func bad(w http.ResponseWriter, req *http.Request) (interface{}, error) {
10+
11+
secret := "MySuperSecretPasscode"
12+
secretHeader := "X-Secret"
13+
14+
headerSecret := req.Header.Get(secretHeader)
15+
secretStr := string(secret)
16+
if len(secret) != 0 && headerSecret != secretStr {
17+
return nil, fmt.Errorf("header %s=%s did not match expected secret", secretHeader, headerSecret)
18+
}
19+
return nil, nil
20+
}
21+
22+
func good(w http.ResponseWriter, req *http.Request) (interface{}, error) {
23+
24+
secret := []byte("MySuperSecretPasscode")
25+
secretHeader := "X-Secret"
26+
27+
headerSecret := req.Header.Get(secretHeader)
28+
if len(secret) != 0 && subtle.ConstantTimeCompare(secret, []byte(headerSecret)) != 1 {
29+
return nil, fmt.Errorf("header %s=%s did not match expected secret", secretHeader, headerSecret)
30+
}
31+
return nil, nil
32+
}
33+
34+
func main() {
35+
bad(nil, nil)
36+
good(nil, nil)
37+
}

0 commit comments

Comments
 (0)