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

Commit f5a8e07

Browse files
authored
Merge pull request #107 from porcupineyhairs/ssrf
Add SSRF query to codeql-go
2 parents 6d93f48 + f2bbbe3 commit f5a8e07

39 files changed

Lines changed: 2117 additions & 49 deletions

File tree

ql/src/go.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ import semmle.go.frameworks.SQL
3434
import semmle.go.frameworks.XPath
3535
import semmle.go.frameworks.Stdlib
3636
import semmle.go.frameworks.Testing
37+
import semmle.go.frameworks.Websocket
3738
import semmle.go.security.FlowSources
3839
import semmle.go.Util

ql/src/semmle/go/Packages.qll

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ class Package extends @package {
2424
/** Gets a textual representation of this element. */
2525
string toString() { result = "package " + getPath() }
2626
}
27+
28+
/**
29+
* Gets the Go import string that may identify a package in module `mod` with the given path,
30+
* possibly modulo semantic import versioning.
31+
*/
32+
bindingset[result, mod, path]
33+
string package(string mod, string path) {
34+
result.regexpMatch("\\Q" + mod + "\\E([/.]v[^/]+)?/\\Q" + path + "\\E")
35+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Contains implementations of some commonly used barrier
3+
* guards for sanitizing untrusted URLs.
4+
*/
5+
6+
import go
7+
8+
/**
9+
* A call to a function called `isLocalUrl`, `isValidRedirect`, or similar, which is
10+
* considered a barrier guard for sanitizing untrusted URLs.
11+
*/
12+
class RedirectCheckBarrierGuard extends DataFlow::BarrierGuard, DataFlow::CallNode {
13+
RedirectCheckBarrierGuard() {
14+
this.getCalleeName().regexpMatch("(?i)(is_?)?(local_?url|valid_?redir(ect)?)")
15+
}
16+
17+
override predicate checks(Expr e, boolean outcome) {
18+
// `isLocalUrl(e)` is a barrier for `e` if it evaluates to `true`
19+
getAnArgument().asExpr() = e and
20+
outcome = true
21+
}
22+
}
23+
24+
/**
25+
* An equality check comparing a data-flow node against a constant string, considered as
26+
* a barrier guard for sanitizing untrusted URLs.
27+
*
28+
* Additionally, a check comparing `url.Hostname()` against a constant string is also
29+
* considered a barrier guard for `url`.
30+
*/
31+
class UrlCheck extends DataFlow::BarrierGuard, DataFlow::EqualityTestNode {
32+
DataFlow::Node url;
33+
34+
UrlCheck() {
35+
exists(this.getAnOperand().getStringValue()) and
36+
(
37+
url = this.getAnOperand()
38+
or
39+
exists(DataFlow::MethodCallNode mc | mc = this.getAnOperand() |
40+
mc.getTarget().getName() = "Hostname" and
41+
url = mc.getReceiver()
42+
)
43+
)
44+
}
45+
46+
override predicate checks(Expr e, boolean outcome) {
47+
e = url.asExpr() and outcome = this.getPolarity()
48+
}
49+
}
50+
51+
/**
52+
* A call to a regexp match function, considered as a barrier guard for sanitizing untrusted URLs.
53+
*
54+
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
55+
*/
56+
class RegexpCheck extends DataFlow::BarrierGuard {
57+
RegexpMatchFunction matchfn;
58+
DataFlow::CallNode call;
59+
60+
RegexpCheck() {
61+
matchfn.getACall() = call and
62+
this = matchfn.getResult().getNode(call).getASuccessor*()
63+
}
64+
65+
override predicate checks(Expr e, boolean branch) {
66+
e = matchfn.getValue().getNode(call).asExpr() and
67+
(branch = false or branch = true)
68+
}
69+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/** Provides classes for working with WebSocket-related APIs. */
2+
3+
import go
4+
5+
/**
6+
* A data-flow node that establishes a new WebSocket connection.
7+
*
8+
* Extend this class to refine existing API models. If you want to model new APIs,
9+
* extend `WebSocketRequestCall::Range` instead.
10+
*/
11+
class WebSocketRequestCall extends DataFlow::CallNode {
12+
WebSocketRequestCall::Range self;
13+
14+
WebSocketRequestCall() { this = self }
15+
16+
/** Gets the URL of the request. */
17+
DataFlow::Node getRequestUrl() { result = self.getRequestUrl() }
18+
}
19+
20+
/** Provides classes for working with WebSocket request functions. */
21+
module WebSocketRequestCall {
22+
/**
23+
* A data-flow node that establishes a new WebSocket connection.
24+
*
25+
* Extend this class to model new APIs. If you want to refine existing
26+
* API models, extend `WebSocketRequestCall` instead.
27+
*/
28+
abstract class Range extends DataFlow::CallNode {
29+
/** Gets the URL of the request. */
30+
abstract DataFlow::Node getRequestUrl();
31+
}
32+
33+
/**
34+
* A WebSocket request expression string used in an API function of the
35+
* `golang.org/x/net/websocket` package.
36+
*/
37+
private class GolangXNetDialFunc extends Range {
38+
GolangXNetDialFunc() {
39+
// func Dial(url_, protocol, origin string) (ws *Conn, err error)
40+
this.getTarget().hasQualifiedName(package("golang.org/x/net", "websocket"), "Dial")
41+
}
42+
43+
override DataFlow::Node getRequestUrl() { result = this.getArgument(0) }
44+
}
45+
46+
/**
47+
* A WebSocket DialConfig expression string used in an API function
48+
* of the `golang.org/x/net/websocket` package.
49+
*/
50+
private class GolangXNetDialConfigFunc extends Range {
51+
GolangXNetDialConfigFunc() {
52+
// func DialConfig(config *Config) (ws *Conn, err error)
53+
this.getTarget().hasQualifiedName(package("golang.org/x/net", "websocket"), "DialConfig")
54+
}
55+
56+
override DataFlow::Node getRequestUrl() {
57+
exists(DataFlow::CallNode cn |
58+
// func NewConfig(server, origin string) (config *Config, err error)
59+
cn.getTarget().hasQualifiedName(package("golang.org/x/net", "websocket"), "NewConfig") and
60+
this.getArgument(0) = cn.getResult(0).getASuccessor*() and
61+
result = cn.getArgument(0)
62+
)
63+
}
64+
}
65+
66+
/**
67+
* A WebSocket request expression string used in an API function
68+
* of the `github.com/gorilla/websocket` package.
69+
*/
70+
private class GorillaWebsocketDialFunc extends Range {
71+
DataFlow::Node url;
72+
73+
GorillaWebsocketDialFunc() {
74+
// func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error)
75+
// func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error)
76+
exists(string name, Method f |
77+
f = this.getTarget() and
78+
f.hasQualifiedName(package("github.com/gorilla", "websocket"), "Dialer", name)
79+
|
80+
name = "Dial" and this.getArgument(0) = url
81+
or
82+
name = "DialContext" and this.getArgument(1) = url
83+
)
84+
}
85+
86+
override DataFlow::Node getRequestUrl() { result = url }
87+
}
88+
89+
/**
90+
* A WebSocket request expression string used in an API function
91+
* of the `github.com/gobwas/ws` package.
92+
*/
93+
private class GobwasWsDialFunc extends Range {
94+
GobwasWsDialFunc() {
95+
// func (d Dialer) Dial(ctx context.Context, urlstr string) (conn net.Conn, br *bufio.Reader, hs Handshake, err error)
96+
exists(Method m |
97+
m.hasQualifiedName(package("github.com/gobwas", "ws"), "Dialer", "Dial") and
98+
m = this.getTarget()
99+
)
100+
or
101+
// func Dial(ctx context.Context, urlstr string) (net.Conn, *bufio.Reader, Handshake, error)
102+
this.getTarget().hasQualifiedName(package("github.com/gobwas", "ws"), "Dial")
103+
}
104+
105+
override DataFlow::Node getRequestUrl() { result = this.getArgument(1) }
106+
}
107+
108+
/**
109+
* A WebSocket request expression string used in an API function
110+
* of the `nhooyr.io/websocket` package.
111+
*/
112+
private class NhooyrWebsocketDialFunc extends Range {
113+
NhooyrWebsocketDialFunc() {
114+
// func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error)
115+
this.getTarget().hasQualifiedName(package("nhooyr.io", "websocket"), "Dial")
116+
}
117+
118+
override DataFlow::Node getRequestUrl() { result = this.getArgument(1) }
119+
}
120+
121+
/**
122+
* A WebSocket request expression string used in an API function
123+
* of the `github.com/sacOO7/gowebsocket` package.
124+
*/
125+
private class SacOO7DialFunc extends Range {
126+
SacOO7DialFunc() {
127+
// func BuildProxy(Url string) func(*http.Request) (*url.URL, error)
128+
// func New(url string) Socket
129+
this.getTarget().hasQualifiedName("github.com/sacOO7/gowebsocket", ["New", "BuildProxy"])
130+
}
131+
132+
override DataFlow::Node getRequestUrl() { result = this.getArgument(0) }
133+
}
134+
}

ql/src/semmle/go/security/OpenUrlRedirectCustomizations.qll

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import go
88
import UrlConcatenation
99
import SafeUrlFlowCustomizations
10+
import semmle.go.dataflow.BarrierGuardUtil
1011

1112
/**
1213
* Provides extension points for customizing the taint-tracking configuration for reasoning about
@@ -104,62 +105,22 @@ module OpenUrlRedirect {
104105

105106
/**
106107
* A call to a function called `isLocalUrl`, `isValidRedirect`, or similar, which is
107-
* considered a barrier for purposes of URL redirection.
108+
* considered a barrier guard for sanitizing untrusted URLs.
108109
*/
109-
class RedirectCheckBarrierGuard extends BarrierGuard, DataFlow::CallNode {
110-
RedirectCheckBarrierGuard() {
111-
this.getCalleeName().regexpMatch("(?i)(is_?)?(local_?url|valid_?redir(ect)?)")
112-
}
113-
114-
override predicate checks(Expr e, boolean outcome) {
115-
// `isLocalUrl(e)` is a barrier for `e` if it evaluates to `true`
116-
getAnArgument().asExpr() = e and
117-
outcome = true
118-
}
119-
}
110+
class RedirectCheckBarrierGuardAsBarrierGuard extends RedirectCheckBarrierGuard, BarrierGuard { }
120111

121112
/**
122-
* A check against a constant value, considered a barrier for redirection.
123-
*/
124-
class EqualityTestGuard extends BarrierGuard, DataFlow::EqualityTestNode {
125-
DataFlow::Node url;
126-
127-
EqualityTestGuard() {
128-
exists(this.getAnOperand().getStringValue()) and
129-
(
130-
url = this.getAnOperand()
131-
or
132-
exists(DataFlow::MethodCallNode mc | mc = this.getAnOperand() |
133-
mc.getTarget().getName() = "Hostname" and
134-
url = mc.getReceiver()
135-
)
136-
)
137-
}
138-
139-
override predicate checks(Expr e, boolean outcome) {
140-
e = url.asExpr() and outcome = this.getPolarity()
141-
}
142-
}
143-
144-
/**
145-
* A call to a regexp match function, considered as a barrier guard for unvalidated URLs.
113+
* A call to a regexp match function, considered as a barrier guard for sanitizing untrusted URLs.
146114
*
147115
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
148116
*/
149-
class RegexpCheck extends BarrierGuard {
150-
RegexpMatchFunction matchfn;
151-
DataFlow::CallNode call;
152-
153-
RegexpCheck() {
154-
matchfn.getACall() = call and
155-
this = matchfn.getResult().getNode(call).getASuccessor*()
156-
}
117+
class RegexpCheckAsBarrierGuard extends RegexpCheck, BarrierGuard { }
157118

158-
override predicate checks(Expr e, boolean branch) {
159-
e = matchfn.getValue().getNode(call).asExpr() and
160-
(branch = false or branch = true)
161-
}
162-
}
119+
/**
120+
* A check against a constant value or the `Hostname` function,
121+
* considered a barrier guard for url flow.
122+
*/
123+
class UrlCheckAsBarrierGuard extends UrlCheck, BarrierGuard { }
163124
}
164125

165126
/** A sink for an open redirect, considered as a sink for safe URL flow. */

ql/src/semmle/go/security/RequestForgeryCustomizations.qll

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import go
66
import UrlConcatenation
77
import SafeUrlFlowCustomizations
8+
import semmle.go.dataflow.BarrierGuardUtil
89

910
/** Provides classes and predicates for the request forgery query. */
1011
module RequestForgery {
@@ -52,13 +53,49 @@ module RequestForgery {
5253
override string getKind() { result = "URL" }
5354
}
5455

56+
/**
57+
* The URL of a WebSocket request, viewed as a sink for request forgery.
58+
*/
59+
class WebSocketCallAsSink extends Sink {
60+
WebSocketRequestCall request;
61+
62+
WebSocketCallAsSink() { this = request.getRequestUrl() }
63+
64+
override DataFlow::Node getARequest() { result = request }
65+
66+
override string getKind() { result = "WebSocket URL" }
67+
}
68+
5569
/**
5670
* A value that is the result of prepending a string that prevents any value from controlling the
5771
* host of a URL.
5872
*/
5973
private class HostnameSanitizer extends SanitizerEdge {
6074
HostnameSanitizer() { hostnameSanitizingPrefixEdge(this, _) }
6175
}
76+
77+
/**
78+
* A call to a function called `isLocalUrl`, `isValidRedirect`, or similar, which is
79+
* considered a barrier guard.
80+
*/
81+
class RedirectCheckBarrierGuardAsBarrierGuard extends RedirectCheckBarrierGuard, SanitizerGuard {
82+
}
83+
84+
/**
85+
* A call to a regexp match function, considered as a barrier guard for sanitizing untrusted URLs.
86+
*
87+
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
88+
*/
89+
class RegexpCheckAsBarrierGuard extends RegexpCheck, SanitizerGuard { }
90+
91+
/**
92+
* An equality check comparing a data-flow node against a constant string, considered as
93+
* a barrier guard for sanitizing untrusted URLs.
94+
*
95+
* Additionally, a check comparing `url.Hostname()` against a constant string is also
96+
* considered a barrier guard for `url`.
97+
*/
98+
class UrlCheckAsBarrierGuard extends UrlCheck, SanitizerGuard { }
6299
}
63100

64101
/** A sink for request forgery, considered as a sink for safe URL flow. */
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
_ "PackageName//v//test" // Not OK
7+
_ "PackageName//v/test" // Not OK
8+
_ "PackageName/test" // OK
9+
_ "PackageName/v//test" // Not OK
10+
_ "PackageName/v/asd/v2/test" // Not OK
11+
_ "PackageName/v/test" // Not OK
12+
13+
_ "PackageName//v2//test" // Not OK
14+
_ "PackageName//v2/test" // Not OK
15+
_ "PackageName/v2//test" // Not OK
16+
_ "PackageName/v2/test" //OK
17+
)
18+
19+
func main() {
20+
pkg.Foo()
21+
fmt.Println("")
22+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| package PackageName/test | PackageName/test |
2+
| package PackageName/v2/test | PackageName/v2/test |
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import go
2+
3+
from Package pkg, string mod, string path
4+
where
5+
packages(pkg, _, package(mod, path), _) and
6+
mod = "PackageName" and
7+
path = "test"
8+
select pkg, pkg.getPath()

0 commit comments

Comments
 (0)