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

Commit 25e4245

Browse files
authored
Merge pull request #291 from smowton/smowton/admin/oauth2-query-polish
Promote OAuth2-misuse query to mainline
2 parents c7b4db8 + b487799 commit 25e4245

File tree

19 files changed

+416
-164
lines changed

19 files changed

+416
-164
lines changed

change-notes/2020-08-18-oauth2.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* The query "Use of constant `state` value in OAuth 2.0 URL" (`go/constant-oauth2-state`) has been promoted from experimental status. This checks for use of a constant state value in generating an OAuth2 redirect URL, which may open the way for a CSRF attack.

ql/src/Security/CWE-327/InsecureTLS.ql

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,6 @@ int getASecureTlsVersion() {
5050
*/
5151
int getATlsVersion() { result = getASecureTlsVersion() or isInsecureTlsVersion(result, _, _) }
5252

53-
/**
54-
* Holds if `node` refers to a value returned alongside a non-nil error value.
55-
*
56-
* For example, `0` in `func tryGetInt() (int, error) { return 0, errors.New("no good") }`
57-
*/
58-
predicate isReturnedWithError(DataFlow::Node node) {
59-
exists(ReturnStmt ret |
60-
ret.getExpr(0) = node.asExpr() and
61-
ret.getNumExpr() = 2 and
62-
ret.getExpr(1).getType() instanceof ErrorType
63-
// That last condition implies ret.getExpr(1) is non-nil, since nil doesn't implement `error`
64-
)
65-
}
66-
6753
/**
6854
* Flow of TLS versions into a `tls.Config` struct, to the `MinVersion` and `MaxVersion` fields.
6955
*/
@@ -76,7 +62,7 @@ class TlsVersionFlowConfig extends TaintTracking::Configuration {
7662
predicate isSource(DataFlow::Node source, int val) {
7763
val = source.getIntValue() and
7864
val = getATlsVersion() and
79-
not isReturnedWithError(source)
65+
not DataFlow::isReturnedWithError(source)
8066
}
8167

8268
/**

ql/src/experimental/CWE-352/ConstantOauth2State.qhelp renamed to ql/src/Security/CWE-352/ConstantOauth2State.qhelp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<overview>
66
<p>
77
OAuth 2.0 clients must implement CSRF protection for the redirection URI, which is typically accomplished by including a "state" value that binds the request to
8-
the user's authenticated state. The Go OAuth 2.0 library allows to specify a "state" value which is then included in the auth code URL, and then provided back by the remote authentication server in the redirect callback, from where it must be validated; failure to do so makes the client susceptible to an CSRF attack.
8+
the user's authenticated state. The Go OAuth 2.0 library allows you to specify a "state" value which is then included in the auth code URL. That state is then provided back by the remote authentication server in the redirect callback, from where it must be validated. Failure to do so makes the client susceptible to an CSRF attack.
99
</p>
1010
</overview>
1111
<recommendation>
@@ -23,4 +23,8 @@
2323
</p>
2424
<sample src="ConstantOauth2StateBetter.go" />
2525
</example>
26+
<references>
27+
<li>IETF: <a href="https://tools.ietf.org/html/rfc6749#section-10.12">The OAuth 2.0 Authorization Framework</a></li>
28+
<li>IETF: <a href="https://tools.ietf.org/html/draft-ietf-oauth-security-topics-15#section-2.1">OAuth 2.0 Security Best Current Practice</a></li>
29+
</references>
2630
</qhelp>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* @name Use of constant `state` value in OAuth 2.0 URL
3+
* @description Using a constant value for the `state` in the OAuth 2.0 URL makes the application
4+
* susceptible to CSRF attacks.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id go/constant-oauth2-state
9+
* @tags security
10+
* external/cwe/cwe-352
11+
*/
12+
13+
import go
14+
import DataFlow::PathGraph
15+
16+
/**
17+
* A method that creates a new URL that will send the user
18+
* to the OAuth 2.0 authorization dialog of the provider.
19+
*/
20+
class AuthCodeURL extends Method {
21+
AuthCodeURL() { this.hasQualifiedName("golang.org/x/oauth2", "Config", "AuthCodeURL") }
22+
}
23+
24+
/**
25+
* A flow of a constant string value to a call to `AuthCodeURL` as the
26+
* `state` parameter.
27+
*/
28+
class ConstantStateFlowConf extends DataFlow::Configuration {
29+
ConstantStateFlowConf() { this = "ConstantStateFlowConf" }
30+
31+
predicate isSink(DataFlow::Node sink, DataFlow::CallNode call) {
32+
exists(AuthCodeURL m | call = m.getACall() | sink = call.getArgument(0))
33+
}
34+
35+
override predicate isSource(DataFlow::Node source) {
36+
source.isConst() and
37+
not DataFlow::isReturnedWithError(source) and
38+
// Avoid duplicate paths by not considering reads from constants as sources themselves:
39+
(
40+
source.asExpr() instanceof StringLit
41+
or
42+
source.asExpr() instanceof AddExpr
43+
)
44+
}
45+
46+
override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
47+
}
48+
49+
/**
50+
* Holds if `pred` writes a URL to the `RedirectURL` field of the `succ` `Config` object.
51+
*
52+
* This propagates flow from the RedirectURL field to the whole Config object.
53+
*/
54+
predicate isUrlTaintingConfigStep(DataFlow::Node pred, DataFlow::Node succ) {
55+
exists(Write w, Field f | f.hasQualifiedName("golang.org/x/oauth2", "Config", "RedirectURL") |
56+
w.writesField(succ.(DataFlow::PostUpdateNode).getPreUpdateNode(), f, pred)
57+
)
58+
}
59+
60+
/**
61+
* Gets a URL or pseudo-URL that suggests an out-of-band OAuth2 flow or use of a transient
62+
* local listener to receive an OAuth2 redirect.
63+
*/
64+
bindingset[result]
65+
string getAnOobOauth2Url() {
66+
// The following are pseudo-URLs seen in the wild to indicate the authenticating site
67+
// should display a code for the user to manually convey, rather than directing:
68+
result in ["urn:ietf:wg:oauth:2.0:oob", "urn:ietf:wg:oauth:2.0:oob:auto", "oob", "code"] or
69+
// Alternatively some non-web tools will create a temporary local webserver to handle the
70+
// OAuth2 redirect:
71+
result.matches("%://localhost%") or
72+
result.matches("%://127.0.0.1%")
73+
}
74+
75+
/**
76+
* A flow of a URL indicating the OAuth redirect doesn't point to a publicly
77+
* accessible address, to the receiver of an `AuthCodeURL` call.
78+
*
79+
* Note we accept localhost and 127.0.0.1 on the assumption this is probably a transient
80+
* listener; if it actually is a persistent server then that really is vulnerable to CSRF.
81+
*/
82+
class PrivateUrlFlowsToAuthCodeUrlCall extends DataFlow::Configuration {
83+
PrivateUrlFlowsToAuthCodeUrlCall() { this = "PrivateUrlFlowsToConfig" }
84+
85+
override predicate isSource(DataFlow::Node source) {
86+
source.getStringValue() = getAnOobOauth2Url() and
87+
// Avoid duplicate paths by excluding constant variable references from
88+
// themselves being sources:
89+
(
90+
source.asExpr() instanceof StringLit
91+
or
92+
source.asExpr() instanceof AddExpr
93+
)
94+
}
95+
96+
override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
97+
// Propagate from a RedirectURL field to a whole Config
98+
isUrlTaintingConfigStep(pred, succ)
99+
or
100+
// Propagate across deref and address-taking steps
101+
TaintTracking::referenceStep(pred, succ)
102+
or
103+
// Propagate across Sprintf and similar calls
104+
TaintTracking::functionModelStep(any(Fmt::Sprinter s), pred, succ)
105+
}
106+
107+
predicate isSink(DataFlow::Node sink, DataFlow::CallNode call) {
108+
exists(AuthCodeURL m | call = m.getACall() | sink = call.getReceiver())
109+
}
110+
111+
override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
112+
}
113+
114+
/**
115+
* Holds if a URL indicating the OAuth redirect doesn't point to a publicly
116+
* accessible address, to the receiver of an `AuthCodeURL` call.
117+
*
118+
* Note we accept localhost and 127.0.0.1 on the assumption this is probably a transient
119+
* listener; if it actually is a persistent server then that really is vulnerable to CSRF.
120+
*/
121+
predicate privateUrlFlowsToAuthCodeUrlCall(DataFlow::CallNode call) {
122+
exists(PrivateUrlFlowsToAuthCodeUrlCall flowConfig, DataFlow::Node receiver |
123+
flowConfig.hasFlowTo(receiver) and
124+
flowConfig.isSink(receiver, call)
125+
)
126+
}
127+
128+
/** A flow from `golang.org/x/oauth2.Config.AuthCodeURL`'s result to a logging function. */
129+
class FlowToPrint extends DataFlow::Configuration {
130+
FlowToPrint() { this = "FlowToPrint" }
131+
132+
predicate isSink(DataFlow::Node sink, DataFlow::CallNode call) {
133+
exists(LoggerCall logCall | call = logCall | sink = logCall.getAMessageComponent())
134+
}
135+
136+
override predicate isSource(DataFlow::Node source) {
137+
source = any(AuthCodeURL m).getACall().getResult()
138+
}
139+
140+
override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
141+
}
142+
143+
/** Holds if the provided `CallNode`'s result flows to an argument of a printer call. */
144+
predicate resultFlowsToPrinter(DataFlow::CallNode authCodeURLCall) {
145+
exists(FlowToPrint cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
146+
cfg.hasFlowPath(source, sink) and
147+
authCodeURLCall.getResult() = source.getNode()
148+
)
149+
}
150+
151+
/** Get a data-flow node that reads the value of `os.Stdin`. */
152+
DataFlow::Node getAStdinNode() {
153+
exists(ValueEntity v |
154+
v.hasQualifiedName("os", "Stdin") and result = globalValueNumber(v.getARead()).getANode()
155+
)
156+
}
157+
158+
/**
159+
* Gets a call to a scanner function that reads from `os.Stdin`, or which creates a scanner
160+
* instance wrapping `os.Stdin`.
161+
*/
162+
DataFlow::CallNode getAScannerCall() {
163+
result = any(Fmt::Scanner f).getACall()
164+
or
165+
exists(Fmt::FScanner f |
166+
result = f.getACall() and f.getReader().getNode(result) = getAStdinNode()
167+
)
168+
or
169+
exists(Bufio::NewScanner f |
170+
result = f.getACall() and f.getReader().getNode(result) = getAStdinNode()
171+
)
172+
}
173+
174+
/**
175+
* Holds if the provided `CallNode` is within the same root as a call
176+
* to a scanner that reads from `os.Stdin`.
177+
*/
178+
predicate containsCallToStdinScanner(FuncDef funcDef) { getAScannerCall().getRoot() = funcDef }
179+
180+
/**
181+
* Holds if the `authCodeURLCall` seems to be done within a terminal
182+
* because there are calls to a printer (`fmt.Println` and similar),
183+
* and a call to a scanner (`fmt.Scan` and similar),
184+
* all of which are typically done within a terminal session.
185+
*/
186+
predicate seemsLikeDoneWithinATerminal(DataFlow::CallNode authCodeURLCall) {
187+
resultFlowsToPrinter(authCodeURLCall) and
188+
containsCallToStdinScanner(authCodeURLCall.getRoot())
189+
}
190+
191+
from
192+
ConstantStateFlowConf cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
193+
DataFlow::CallNode sinkCall
194+
where
195+
cfg.hasFlowPath(source, sink) and
196+
cfg.isSink(sink.getNode(), sinkCall) and
197+
// Exclude cases that seem to be oauth flows done from within a terminal:
198+
not seemsLikeDoneWithinATerminal(sinkCall) and
199+
not privateUrlFlowsToAuthCodeUrlCall(sinkCall)
200+
select sink.getNode(), source, sink, "Using a constant $@ to create oauth2 URLs.", source.getNode(),
201+
"state string"

ql/src/experimental/CWE-352/ConstantOauth2State.ql

Lines changed: 0 additions & 101 deletions
This file was deleted.

ql/src/semmle/go/dataflow/internal/DataFlowUtil.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,20 @@ Node extractTupleElement(Node t, int i) {
875875
)
876876
}
877877

878+
/**
879+
* Holds if `node` refers to a value returned alongside a non-nil error value.
880+
*
881+
* For example, `0` in `func tryGetInt() (int, error) { return 0, errors.New("no good") }`
882+
*/
883+
predicate isReturnedWithError(Node node) {
884+
exists(ReturnStmt ret, int nodeArg, int errorArg |
885+
ret.getExpr(nodeArg) = node.asExpr() and
886+
nodeArg != errorArg and
887+
ret.getExpr(errorArg).getType() instanceof ErrorType
888+
// That last condition implies ret.getExpr(errorArg) is non-nil, since nil doesn't implement `error`
889+
)
890+
}
891+
878892
/**
879893
* Holds if data flows from `nodeFrom` to `nodeTo` in exactly one local
880894
* (intra-procedural) step.

0 commit comments

Comments
 (0)