From f261f34f57aa5f36812a03c50ac8487de8b4995c Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Sat, 22 May 2021 18:14:13 +0200
Subject: [PATCH 01/14] Add query to detect CORS misconfiguration
---
.../CWE-942/CorsMisconfiguration.qhelp | 42 +++++++
.../CWE-942/CorsMisconfiguration.ql | 84 +++++++++++++
.../CWE-942/CorsMisconfigurationBad.go | 34 ++++++
.../CWE-942/CorsMisconfigurationGood.go | 32 +++++
.../CWE-942/CorsMisconfiguration.expected | 7 ++
.../CWE-942/CorsMisconfiguration.go | 110 ++++++++++++++++++
.../CWE-942/CorsMisconfiguration.qlref | 1 +
7 files changed, 310 insertions(+)
create mode 100644 ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
create mode 100644 ql/src/experimental/CWE-942/CorsMisconfiguration.ql
create mode 100644 ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
create mode 100644 ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
create mode 100644 ql/test/experimental/CWE-942/CorsMisconfiguration.expected
create mode 100755 ql/test/experimental/CWE-942/CorsMisconfiguration.go
create mode 100755 ql/test/experimental/CWE-942/CorsMisconfiguration.qlref
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
new file mode 100644
index 00000000000..50a27f669f6
--- /dev/null
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
@@ -0,0 +1,42 @@
+
+
+
+
+ When configuring the allowed origins for CORS requests that allow passing cookies
+ (i.e. the Access-Control-Allow-Credentials is set to true),
+ if the Access-Control-Allow-Origin header is set to a user-provided value (and not correctly validated),
+ or is set to special values such as * or null, then the users of that application might be
+ vulnerable to attacks where the attacker impersonates the user and sends request on their behalf.
+
+
+
+
+ When configuring CORS that allow credentials passing,
+ it's best not to use user-provided values for the allowed origins response header,
+ especially if the cookies grant session permissions on the user's account.
+
+
+ It also can be very dangerous to set the allowed origins to * or null (which can be bypassed).
+
+
+
+
+ The first example shows a few possible CORS misconfiguration cases:
+
+
+
+ The second example show better configurations:
+
+
+
+
+
+ Reference 1: PortSwigger Web Security Academy on CORS.
+
+
+ Reference 2: AppSec EU 2017 Exploiting CORS Misconfigurations For Bitcoins And Bounties by James Kettle.
+
+
+
\ No newline at end of file
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
new file mode 100644
index 00000000000..5c34eaf4fa5
--- /dev/null
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -0,0 +1,84 @@
+/**
+ * @name CORS misconfiguration
+ * @description If a CORS policy is configured to accept an origin value obtained from the request data,
+ * or is set to `*` or `null`, and it allows credential sharing, then the users of the
+ * application are vulnerable to the same range of attacks as in XSS (credential stealing, etc.).
+ * @kind problem
+ * @problem.severity warning
+ * @id go/cors-misconfiguration
+ * @tags security
+ * external/cwe/cwe-942
+ * external/cwe/cwe-346
+ */
+
+import go
+
+/**
+ * Provides the name of the `Access-Control-Allow-Origin` header key.
+ */
+string headerAllowOrigin() { result = "Access-Control-Allow-Origin".toLowerCase() }
+
+/**
+ * Provides the name of the `Access-Control-Allow-Credentials` header key.
+ */
+string headerAllowCredentials() { result = "Access-Control-Allow-Credentials".toLowerCase() }
+
+/**
+ * A taint-tracking configuration for reasoning about when an UntrustedFlowSource
+ * flows to a HeaderWrite that writes an `Access-Control-Allow-Origin` header's value.
+ */
+class FlowsUntrustedToAllowOriginHeader extends TaintTracking::Configuration {
+ FlowsUntrustedToAllowOriginHeader() { this = "from-untrusted-to-allow-origin-header-value" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
+
+ predicate isSink(DataFlow::Node sink, HTTP::HeaderWrite hw) {
+ hw.getHeaderName() = headerAllowOrigin() and sink = hw.getValue()
+ }
+
+ override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
+}
+
+/**
+ * Holds if the provided `allowOriginHW` HeaderWrite's parent ResponseWriter
+ * also has another HeaderWrite that sets a `Access-Control-Allow-Credentials`
+ * header to `true`.
+ */
+predicate allowCredentialsIsSetToTrue(HTTP::HeaderWrite allowOriginHW) {
+ exists(HTTP::HeaderWrite allowCredentialsHW |
+ allowCredentialsHW.getHeaderName() = headerAllowCredentials() and
+ allowCredentialsHW.getHeaderValue() = "true"
+ |
+ allowOriginHW.getResponseWriter() = allowCredentialsHW.getResponseWriter()
+ )
+}
+
+/**
+ * Holds if the provided `allowOriginHW` HeaderWrite's value is set using an
+ * UntrustedFlowSource.
+ * The `message` parameter is populated with the warning message to be returned by the query.
+ */
+predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, string message) {
+ exists(FlowsUntrustedToAllowOriginHeader cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
+ cfg.hasFlowPath(source, sink) and
+ cfg.isSink(sink.getNode(), allowOriginHW)
+ |
+ message =
+ headerAllowOrigin() + " header is set to a user-defined value, and " +
+ headerAllowCredentials() + " is set to `true`"
+ )
+}
+
+from HTTP::HeaderWrite allowOriginHW, string message
+where
+ (
+ flowsFromUntrustedToAllowOrigin(allowOriginHW, message)
+ or
+ allowOriginHW.getHeaderName() = headerAllowOrigin() and
+ allowOriginHW.getHeaderValue() = ["*", "null"] and
+ message =
+ headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
+ headerAllowCredentials() + " is set to `true`"
+ ) and
+ allowCredentialsIsSetToTrue(allowOriginHW)
+select allowOriginHW, message
diff --git a/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go b/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
new file mode 100644
index 00000000000..2c54da4730d
--- /dev/null
+++ b/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
@@ -0,0 +1,34 @@
+package main
+
+import "net/http"
+
+func main() {}
+
+// bad is an example of a bad implementation
+func bad1() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: all origins are allowed,
+ // and Access-Control-Allow-Credentials is set to 'true'.
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+}
+
+func bad2() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: 'null' origin is allowed,
+ // and Access-Control-Allow-Credentials is set to 'true'.
+ w.Header().Set("Access-Control-Allow-Origin", "null")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+}
+
+func bad3() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ origin := req.Header.Get("origin")
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+}
diff --git a/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go b/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
new file mode 100644
index 00000000000..24fcdd56d80
--- /dev/null
+++ b/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
@@ -0,0 +1,32 @@
+package main
+
+import "net/http"
+
+// good1 is an example of a good implementation
+func good1() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: all origins are allowed,
+ // but Access-Control-Allow-Credentials is not set.
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ })
+}
+
+func good2() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: all origins are allowed,
+ // and some write methods are allowed,
+ // BUT `Access-Control-Allow-Credentials` is not set:
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
+ })
+}
+
+func good3() {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // BUT `Access-Control-Allow-Credentials` is set to 'false':
+ origin := req.Header.Get("origin")
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "false")
+ })
+}
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
new file mode 100644
index 00000000000..c1bb884c613
--- /dev/null
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
@@ -0,0 +1,7 @@
+| CorsMisconfiguration.go:15:4:15:53 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:21:4:21:41 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:41:4:41:56 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:47:4:47:42 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:68:4:68:53 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:86:4:86:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:93:4:93:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.go b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
new file mode 100755
index 00000000000..96517d42a4b
--- /dev/null
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
@@ -0,0 +1,110 @@
+package main
+
+import "net/http"
+
+const (
+ HeaderAllowOrigin = "Access-Control-Allow-Origin"
+ HeaderAllowCredentials = "Access-Control-Allow-Credentials"
+)
+
+func main() {
+ {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: all origins are allowed,
+ // and Access-Control-Allow-Credentials is set to 'true'.
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: all origins are allowed,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ w.Header().Set(HeaderAllowOrigin, "*")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK: all origins are allowed,
+ // but Access-Control-Allow-Credentials is set to 'false'.
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Credentials", "false")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK: all origins are allowed,
+ // but Access-Control-Allow-Credentials is not set.
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ })
+ }
+ {
+ const Null = "null"
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: 'null' origin is allowed,
+ // and Access-Control-Allow-Credentials is set to 'true'.
+ w.Header().Set("Access-Control-Allow-Origin", "null")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: 'null' origin is allowed,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ w.Header().Set(HeaderAllowOrigin, Null)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK: 'null' origin is allowed,
+ // but Access-Control-Allow-Credentials is set to 'false'.
+ w.Header().Set("Access-Control-Allow-Origin", Null)
+ w.Header().Set("Access-Control-Allow-Credentials", "false")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK: 'null' origin is allowed,
+ // but Access-Control-Allow-Credentials is not set.
+ w.Header().Set("Access-Control-Allow-Origin", Null)
+ })
+ }
+
+ {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: all origins are allowed,
+ // and some write methods are allowed,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK: all origins are allowed,
+ // and some write methods are allowed,
+ // BUT `Access-Control-Allow-Credentials` is not set:
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
+ })
+ }
+
+ {
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ origin := req.Header.Get("origin")
+ w.Header().Set(HeaderAllowOrigin, origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ origin := req.Header.Get("origin")
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set(HeaderAllowCredentials, "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // BUT `Access-Control-Allow-Credentials` is set to 'false':
+ origin := req.Header.Get("origin")
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "false")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // BUT `Access-Control-Allow-Credentials` is not set:
+ origin := req.Header.Get("origin")
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ })
+ }
+}
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.qlref b/ql/test/experimental/CWE-942/CorsMisconfiguration.qlref
new file mode 100755
index 00000000000..f23401c1091
--- /dev/null
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.qlref
@@ -0,0 +1 @@
+experimental/CWE-942/CorsMisconfiguration.ql
From 924e445ce991ca4d4e5a7de66408a58a4026ea2e Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Sat, 22 May 2021 18:19:44 +0200
Subject: [PATCH 02/14] Add missing newline
---
ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
index 50a27f669f6..c6493645c63 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
@@ -39,4 +39,4 @@
Reference 2: AppSec EU 2017 Exploiting CORS Misconfigurations For Bitcoins And Bounties by James Kettle.
-
\ No newline at end of file
+
From 9d1f13fe9be5d425bf88cdc60e5de1d7d2b0cb03 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Sat, 22 May 2021 18:32:48 +0200
Subject: [PATCH 03/14] Add `allowOriginIsWildcardOrNull` predicate
---
.../CWE-942/CorsMisconfiguration.ql | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 5c34eaf4fa5..8aa06faccce 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -69,16 +69,24 @@ predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, strin
)
}
+/**
+ * Holds if the provided `allowOriginHW` HeaderWrite is for a `Access-Control-Allow-Origin`
+ * header and the value is set to `*` or `null`.
+ */
+predicate allowOriginIsWildcardOrNull(HTTP::HeaderWrite allowOriginHW, string message) {
+ allowOriginHW.getHeaderName() = headerAllowOrigin() and
+ allowOriginHW.getHeaderValue() = ["*", "null"] and
+ message =
+ headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
+ headerAllowCredentials() + " is set to `true`"
+}
+
from HTTP::HeaderWrite allowOriginHW, string message
where
(
flowsFromUntrustedToAllowOrigin(allowOriginHW, message)
or
- allowOriginHW.getHeaderName() = headerAllowOrigin() and
- allowOriginHW.getHeaderValue() = ["*", "null"] and
- message =
- headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
- headerAllowCredentials() + " is set to `true`"
+ allowOriginIsWildcardOrNull(allowOriginHW, message)
) and
allowCredentialsIsSetToTrue(allowOriginHW)
select allowOriginHW, message
From 74f8f1dcdb2b6418efd1a6b24ad1da16ad5429f4 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Mon, 24 May 2021 15:19:35 +0200
Subject: [PATCH 04/14] Cleanup
---
ql/src/experimental/CWE-942/CorsMisconfiguration.ql | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 8aa06faccce..eb9cc52d8fe 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -83,10 +83,10 @@ predicate allowOriginIsWildcardOrNull(HTTP::HeaderWrite allowOriginHW, string me
from HTTP::HeaderWrite allowOriginHW, string message
where
+ allowCredentialsIsSetToTrue(allowOriginHW) and
(
flowsFromUntrustedToAllowOrigin(allowOriginHW, message)
or
allowOriginIsWildcardOrNull(allowOriginHW, message)
- ) and
- allowCredentialsIsSetToTrue(allowOriginHW)
+ )
select allowOriginHW, message
From 8525c58e1ae34f0a813ac17cdfae34d1fe9f0619 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Mon, 24 May 2021 15:19:50 +0200
Subject: [PATCH 05/14] Improve qhelp doc
---
.../CWE-942/CorsMisconfiguration.qhelp | 32 ++++++++++++++++---
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
index c6493645c63..cbcf206c01a 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
@@ -4,11 +4,33 @@
- When configuring the allowed origins for CORS requests that allow passing cookies
- (i.e. the Access-Control-Allow-Credentials is set to true),
- if the Access-Control-Allow-Origin header is set to a user-provided value (and not correctly validated),
- or is set to special values such as * or null, then the users of that application might be
- vulnerable to attacks where the attacker impersonates the user and sends request on their behalf.
+ Web browsers, by default, disallow cross-origin resource sharing via direct HTTP requests (i.e. using a JavaScript HTTP client).
+ Still, to satisfy some needs that arose with the growth of the web, an expedient was created to make exceptions possible.
+ CORS (Cross-origin resource sharing) is a mechanism that allows resources of a web endpoint (let's call it "Peer A")
+ to be accessed from another web page belonging to a different domain ("Peer B").
+
+
+ For that to happen, Peer A needs to make available its CORS configuration via special headers on the desired endpoint
+ via the OPTIONS method.
+
+
+ This configuration can also allow the inclusion of cookies on the cross-origin request,
+ (i.e. when the Access-Control-Allow-Credentials header is set to true)
+ meaning that Peer B can send a request to Peer A that will include the cookies as if the request was executed by the user.
+
+
+ That can have dangerous effects if Peer B origin is not restricted correctly.
+ An example of a dangerous scenario is when Access-Control-Allow-Origin header is set to a value gotten from the Peer B's request
+ (and not correctly validated), or is set to special values such as * or null.
+ The above values can allow any Peer B to send requests to the misconfigured Peer A on behalf of the user.
+
+
+ Example scenario:
+ User is client of a bank that has its API misconfigured to accept CORS requests from any domain.
+ When the user loads an evil page, the evil page sends a request to the bank's API to transfer all funds
+ to evil party's account.
+ Given that the user was already logged in to the bank website, and had its session cookies set,
+ the evil party's request succeeds.
From 56e99b6efb9b9f0161df52a461977e5d948c53cd Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Thu, 3 Jun 2021 10:50:50 +0200
Subject: [PATCH 06/14] Convert header values to lowercase before comparing
---
ql/src/experimental/CWE-942/CorsMisconfiguration.ql | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index eb9cc52d8fe..9099a6685a9 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -47,7 +47,7 @@ class FlowsUntrustedToAllowOriginHeader extends TaintTracking::Configuration {
predicate allowCredentialsIsSetToTrue(HTTP::HeaderWrite allowOriginHW) {
exists(HTTP::HeaderWrite allowCredentialsHW |
allowCredentialsHW.getHeaderName() = headerAllowCredentials() and
- allowCredentialsHW.getHeaderValue() = "true"
+ allowCredentialsHW.getHeaderValue().toLowerCase() = "true"
|
allowOriginHW.getResponseWriter() = allowCredentialsHW.getResponseWriter()
)
@@ -75,7 +75,7 @@ predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, strin
*/
predicate allowOriginIsWildcardOrNull(HTTP::HeaderWrite allowOriginHW, string message) {
allowOriginHW.getHeaderName() = headerAllowOrigin() and
- allowOriginHW.getHeaderValue() = ["*", "null"] and
+ allowOriginHW.getHeaderValue().toLowerCase() = ["*", "null"] and
message =
headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
headerAllowCredentials() + " is set to `true`"
From 4662358b8d58624f598bfee9a5a361b9e7364d2a Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Thu, 3 Jun 2021 12:53:52 +0200
Subject: [PATCH 07/14] Add flag checks
---
.../CWE-942/CorsMisconfiguration.ql | 26 ++++++++
.../CWE-942/CorsMisconfiguration.expected | 1 +
.../CWE-942/CorsMisconfiguration.go | 62 +++++++++++++++++++
3 files changed, 89 insertions(+)
mode change 100755 => 100644 ql/test/experimental/CWE-942/CorsMisconfiguration.go
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 9099a6685a9..a2c6bd3ef08 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -12,6 +12,19 @@
*/
import go
+import semmle.go.security.InsecureFeatureFlag::InsecureFeatureFlag
+
+/**
+ * A flag indicating a check for satisfied permissions or test configuration.
+ */
+class AllowedFlag extends FlagKind {
+ AllowedFlag() { this = "allowed" }
+
+ bindingset[result]
+ override string getAFlagName() {
+ result.regexpMatch("(?i).*(allow|match|check|debug|devel|insecure).*")
+ }
+}
/**
* Provides the name of the `Access-Control-Allow-Origin` header key.
@@ -36,6 +49,14 @@ class FlowsUntrustedToAllowOriginHeader extends TaintTracking::Configuration {
hw.getHeaderName() = headerAllowOrigin() and sink = hw.getValue()
}
+ override predicate isSanitizer(DataFlow::Node node) {
+ exists(ControlFlow::ConditionGuardNode cgn |
+ cgn.ensures(any(AllowedFlag f).getAFlag().getANode(), _)
+ |
+ cgn.dominates(node.getBasicBlock())
+ )
+ }
+
override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
}
@@ -88,5 +109,10 @@ where
flowsFromUntrustedToAllowOrigin(allowOriginHW, message)
or
allowOriginIsWildcardOrNull(allowOriginHW, message)
+ ) and
+ not exists(ControlFlow::ConditionGuardNode cgn |
+ cgn.ensures(any(AllowedFlag f).getAFlag().getANode(), _)
+ |
+ cgn.dominates(allowOriginHW.getBasicBlock())
)
select allowOriginHW, message
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
index c1bb884c613..40ab35a0d87 100644
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
@@ -5,3 +5,4 @@
| CorsMisconfiguration.go:68:4:68:53 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
| CorsMisconfiguration.go:86:4:86:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
| CorsMisconfiguration.go:93:4:93:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:100:5:100:57 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.go b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
old mode 100755
new mode 100644
index 96517d42a4b..5570f3c243e
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.go
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
@@ -93,6 +93,14 @@ func main() {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set(HeaderAllowCredentials, "true")
})
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true':
+ if origin := req.Header.Get("Origin"); origin != "" {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+ w.Header().Set(HeaderAllowCredentials, "true")
+ })
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
// BUT `Access-Control-Allow-Credentials` is set to 'false':
@@ -106,5 +114,59 @@ func main() {
origin := req.Header.Get("origin")
w.Header().Set("Access-Control-Allow-Origin", origin)
})
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true',
+ // BUT there is a check on the origin header:
+ origin := req.Header.Get("origin")
+ if allowedOrigin(origin) {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+ w.Header().Set(HeaderAllowCredentials, "true")
+ })
+ allowedOrigins := map[string]bool{
+ "example.com": true,
+ }
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true',
+ // BUT there is a check on the origin header:
+ origin := req.Header.Get("origin")
+ isAllowed := allowedOrigins[origin]
+ if isAllowed {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+ w.Header().Set(HeaderAllowCredentials, "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
+ // and `Access-Control-Allow-Credentials` is set to 'true',
+ // BUT there is a check on the origin header:
+ origin := req.Header.Get("origin")
+ allowAllHosts := true
+ allowedHost := isAllowedHost([]string{"example.com"}, origin)
+ if allowAllHosts {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ } else if allowedHost {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+ w.Header().Set(HeaderAllowCredentials, "true")
+ })
}
}
+
+func isAllowedHost(allowed []string, try string) bool {
+ for _, v := range allowed {
+ if try == v {
+ return true
+ }
+ }
+ return false
+}
+
+func allowedOrigin(origin string) bool {
+ if origin == "example.com" {
+ return true
+ }
+ return false
+}
From 824b5a4b52b92859b4963f702b3c117e729b815e Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Sat, 5 Jun 2021 10:40:28 +0200
Subject: [PATCH 08/14] Wildcard origin does not allow
`Access-Control-Allow-Credentials: true`
---
.../CWE-942/CorsMisconfiguration.qhelp | 2 +-
.../CWE-942/CorsMisconfiguration.ql | 8 ++--
.../CWE-942/CorsMisconfigurationBad.go | 12 +-----
.../CWE-942/CorsMisconfigurationGood.go | 19 ----------
.../CWE-942/CorsMisconfiguration.expected | 13 +++----
.../CWE-942/CorsMisconfiguration.go | 38 +------------------
6 files changed, 12 insertions(+), 80 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
index cbcf206c01a..eafd34cd6c5 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.qhelp
@@ -40,7 +40,7 @@
especially if the cookies grant session permissions on the user's account.
- It also can be very dangerous to set the allowed origins to * or null (which can be bypassed).
+ It also can be very dangerous to set the allowed origins to null (which can be bypassed).
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index a2c6bd3ef08..44f4848b574 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -92,11 +92,11 @@ predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, strin
/**
* Holds if the provided `allowOriginHW` HeaderWrite is for a `Access-Control-Allow-Origin`
- * header and the value is set to `*` or `null`.
+ * header and the value is set to `null`.
*/
-predicate allowOriginIsWildcardOrNull(HTTP::HeaderWrite allowOriginHW, string message) {
+predicate allowOriginIsNull(HTTP::HeaderWrite allowOriginHW, string message) {
allowOriginHW.getHeaderName() = headerAllowOrigin() and
- allowOriginHW.getHeaderValue().toLowerCase() = ["*", "null"] and
+ allowOriginHW.getHeaderValue().toLowerCase() = "null" and
message =
headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
headerAllowCredentials() + " is set to `true`"
@@ -108,7 +108,7 @@ where
(
flowsFromUntrustedToAllowOrigin(allowOriginHW, message)
or
- allowOriginIsWildcardOrNull(allowOriginHW, message)
+ allowOriginIsNull(allowOriginHW, message)
) and
not exists(ControlFlow::ConditionGuardNode cgn |
cgn.ensures(any(AllowedFlag f).getAFlag().getANode(), _)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go b/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
index 2c54da4730d..470735eb027 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
+++ b/ql/src/experimental/CWE-942/CorsMisconfigurationBad.go
@@ -4,17 +4,7 @@ import "net/http"
func main() {}
-// bad is an example of a bad implementation
func bad1() {
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // BAD: all origins are allowed,
- // and Access-Control-Allow-Credentials is set to 'true'.
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Credentials", "true")
- })
-}
-
-func bad2() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// BAD: 'null' origin is allowed,
// and Access-Control-Allow-Credentials is set to 'true'.
@@ -23,7 +13,7 @@ func bad2() {
})
}
-func bad3() {
+func bad2() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
// and `Access-Control-Allow-Credentials` is set to 'true':
diff --git a/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go b/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
index 24fcdd56d80..1924b004bdf 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
+++ b/ql/src/experimental/CWE-942/CorsMisconfigurationGood.go
@@ -2,26 +2,7 @@ package main
import "net/http"
-// good1 is an example of a good implementation
func good1() {
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // OK-ish: all origins are allowed,
- // but Access-Control-Allow-Credentials is not set.
- w.Header().Set("Access-Control-Allow-Origin", "*")
- })
-}
-
-func good2() {
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // OK-ish: all origins are allowed,
- // and some write methods are allowed,
- // BUT `Access-Control-Allow-Credentials` is not set:
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
- })
-}
-
-func good3() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// OK-ish: the `Access-Control-Allow-Origin` header is set using a user-defined value,
// BUT `Access-Control-Allow-Credentials` is set to 'false':
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
index 40ab35a0d87..6eedef35678 100644
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
@@ -1,8 +1,5 @@
-| CorsMisconfiguration.go:15:4:15:53 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:21:4:21:41 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:41:4:41:56 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:47:4:47:42 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:68:4:68:53 | call to Set | access-control-allow-origin header is set to `*`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:86:4:86:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:93:4:93:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:100:5:100:57 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:23:4:23:56 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:29:4:29:42 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:50:4:50:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:57:4:57:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:64:5:64:57 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.go b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
index 5570f3c243e..20d90e0a251 100644
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.go
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
@@ -9,24 +9,6 @@ const (
func main() {
{
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // BAD: all origins are allowed,
- // and Access-Control-Allow-Credentials is set to 'true'.
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Credentials", "true")
- })
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // BAD: all origins are allowed,
- // and `Access-Control-Allow-Credentials` is set to 'true':
- w.Header().Set(HeaderAllowOrigin, "*")
- w.Header().Set("Access-Control-Allow-Credentials", "true")
- })
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // OK: all origins are allowed,
- // but Access-Control-Allow-Credentials is set to 'false'.
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Credentials", "false")
- })
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// OK: all origins are allowed,
// but Access-Control-Allow-Credentials is not set.
@@ -60,24 +42,6 @@ func main() {
})
}
- {
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // BAD: all origins are allowed,
- // and some write methods are allowed,
- // and `Access-Control-Allow-Credentials` is set to 'true':
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
- w.Header().Set("Access-Control-Allow-Credentials", "true")
- })
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- // OK: all origins are allowed,
- // and some write methods are allowed,
- // BUT `Access-Control-Allow-Credentials` is not set:
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT")
- })
- }
-
{
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// BAD: the `Access-Control-Allow-Origin` header is set using a user-defined value,
@@ -149,8 +113,8 @@ func main() {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else if allowedHost {
w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set(HeaderAllowCredentials, "true")
}
- w.Header().Set(HeaderAllowCredentials, "true")
})
}
}
From c0f195ba164df16410583c27c37d0f1e496d6b73 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Sat, 19 Jun 2021 22:25:51 +0200
Subject: [PATCH 09/14] Reduce false positives
---
.../CWE-942/CorsMisconfiguration.ql | 78 ++++++++++++
.../CWE-942/CorsMisconfiguration.expected | 10 +-
.../CWE-942/CorsMisconfiguration.go | 113 +++++++++++++++++-
3 files changed, 195 insertions(+), 6 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 44f4848b574..b05e90a6e62 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -102,6 +102,83 @@ predicate allowOriginIsNull(HTTP::HeaderWrite allowOriginHW, string message) {
headerAllowCredentials() + " is set to `true`"
}
+/**
+ * A read on a map type.
+ */
+class MapRead extends DataFlow::ElementReadNode {
+ MapRead() { this.getBase().getType() instanceof MapType }
+}
+
+/**
+ * A taint-tracking configuration for reasoning about when an UntrustedFlowSource
+ * flows somewhere.
+ */
+class FlowsFromUntrusted extends TaintTracking::Configuration {
+ FlowsFromUntrusted() { this = "from-untrusted" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) { any() }
+}
+
+/**
+ * Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
+ */
+predicate untrustedFlowsToExpr(Expr dst) {
+ exists(FlowsFromUntrusted cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
+ cfg.hasFlowPath(source, sink) and
+ sink.getNode().asExpr() = dst
+ )
+}
+
+/**
+ * Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
+ */
+predicate untrustedFlowsTo(DataFlow::Node dst) { untrustedFlowsToExpr(dst.asExpr()) }
+
+/**
+ * Holds if the provided `allowOriginHW` is guarded by a check on an `UntrustedFlowSource`
+ * which (supposedly) is an `Origin` header.
+ */
+predicate isGuardedByCheckOnUntrusted(HTTP::HeaderWrite allowOriginHW) {
+ exists(ControlFlow::ConditionGuardNode cgn, IfStmt ifs |
+ exists(Expr child, Expr operand |
+ child = ifs.getCond().getAChildExpr*() and
+ (
+ operand = child or
+ operand = child.(LorExpr).getAnOperand() or
+ operand = child.(LandExpr).getAnOperand()
+ ) and
+ (
+ exists(DataFlow::CallExpr call | call = operand |
+ call.getTarget().hasQualifiedName("strings", "HasSuffix") and
+ untrustedFlowsToExpr(call.getArgument(0))
+ )
+ or
+ exists(MapRead mapRead |
+ operand = mapRead.asExpr() and
+ untrustedFlowsTo(mapRead.getIndex().getAPredecessor*())
+ // TODO: add _, ok : map[untrusted]; ok
+ )
+ or
+ exists(EqlExpr comp |
+ operand = comp and
+ (
+ untrustedFlowsToExpr(comp.getLeftOperand()) and
+ not comp.getRightOperand().(StringLit).getStringValue() = ""
+ or
+ untrustedFlowsToExpr(comp.getRightOperand()) and
+ not comp.getLeftOperand().(StringLit).getStringValue() = ""
+ )
+ )
+ )
+ )
+ |
+ cgn.getCondition() = ifs.getCond() and
+ cgn.dominates(allowOriginHW.getBasicBlock())
+ )
+}
+
from HTTP::HeaderWrite allowOriginHW, string message
where
allowCredentialsIsSetToTrue(allowOriginHW) and
@@ -110,6 +187,7 @@ where
or
allowOriginIsNull(allowOriginHW, message)
) and
+ not isGuardedByCheckOnUntrusted(allowOriginHW) and
not exists(ControlFlow::ConditionGuardNode cgn |
cgn.ensures(any(AllowedFlag f).getAFlag().getANode(), _)
|
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
index 6eedef35678..2ab9324fe8c 100644
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.expected
@@ -1,5 +1,5 @@
-| CorsMisconfiguration.go:23:4:23:56 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:29:4:29:42 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:50:4:50:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:57:4:57:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
-| CorsMisconfiguration.go:64:5:64:57 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:26:4:26:56 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:32:4:32:42 | call to Set | access-control-allow-origin header is set to `null`, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:53:4:53:44 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:60:4:60:56 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
+| CorsMisconfiguration.go:67:5:67:57 | call to Set | access-control-allow-origin header is set to a user-defined value, and access-control-allow-credentials is set to `true` |
diff --git a/ql/test/experimental/CWE-942/CorsMisconfiguration.go b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
index 20d90e0a251..cac752dbcb2 100644
--- a/ql/test/experimental/CWE-942/CorsMisconfiguration.go
+++ b/ql/test/experimental/CWE-942/CorsMisconfiguration.go
@@ -1,6 +1,9 @@
package main
-import "net/http"
+import (
+ "net/http"
+ "strings"
+)
const (
HeaderAllowOrigin = "Access-Control-Allow-Origin"
@@ -116,9 +119,117 @@ func main() {
w.Header().Set(HeaderAllowCredentials, "true")
}
})
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the input origin header is validated agains a whitelist.
+ responseHeader := w.Header()
+ {
+ origin := req.Header.Get("origin")
+ if origin != "" && origin != "null" {
+ if len(AccessControlAllowOrigins) == 0 ||
+ AccessControlAllowOrigins[origin] {
+ responseHeader.Set("Access-Control-Allow-Origin", origin)
+ responseHeader.Set("Access-Control-Allow-Credentials", "true")
+ }
+ } else {
+ responseHeader.Set("Access-Control-Allow-Origin", "*")
+ }
+ }
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ originSuffix := ".example.com"
+ // OK-ish: the input origin header is validated agains a suffix.
+ origin := req.Header.Get("Origin")
+ if origin != "" && (originSuffix == "" || strings.HasSuffix(origin, originSuffix)) {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, x-requested-by, *")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS")
+ if req.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ }
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ originSuffix := ".example.com"
+ // OK-ish: the input origin header is validated agains a whitelist.
+ origin := req.Header.Get("Origin")
+ if origin != "" && (originSuffix == "" || AccessControlAllowOrigins[origin]) {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, x-requested-by, *")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS")
+ if req.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ }
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the input origin header is validated agains a whitelist.
+ origin := req.Header.Get("origin")
+ if origin != "" && origin != "null" {
+ if len(AccessControlAllowOrigins) == 0 || AccessControlAllowOrigins[origin] {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ }
+ } else {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ }
+ })
+ // http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // // OK-ish: the input origin header is validated agains a whitelist.
+ // origin := req.Header.Get("origin")
+ // if origin != "" && origin != "null" {
+ // if _, ok := AccessControlAllowOrigins[origin]; ok {
+ // w.Header().Set("Access-Control-Allow-Origin", origin)
+ // w.Header().Set("Access-Control-Allow-Credentials", "true")
+ // }
+ // } else {
+ // w.Header().Set("Access-Control-Allow-Origin", "*")
+ // }
+ // })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the input origin header is validated agains a whitelist.
+ if origin := req.Header.Get("Origin"); cors[origin] {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ } else if len(origin) > 0 && cors["*"] {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+
+ w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-Token, X-Client")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ // OK-ish: the input origin header is validated agains a whitelist.
+ origin := req.Header.Get("origin")
+ for _, v := range GetAllowOrigin() {
+ if v == origin {
+ w.Header().Add("Access-Control-Allow-Origin", origin)
+ break
+ }
+ }
+
+ w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-Token, X-Client")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ })
}
}
+var (
+ cors = map[string]bool{"*": true}
+)
+
+func GetAllowOrigin() []string {
+ return []string{
+ "example.com",
+ }
+}
+
+var AccessControlAllowOrigins map[string]bool
+
func isAllowedHost(allowed []string, try string) bool {
for _, v := range allowed {
if try == v {
From 66bd56f444e48371a2333f84f6edbcf5beee3ae2 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Mon, 5 Jul 2021 13:14:56 +0200
Subject: [PATCH 10/14] Don't use any() as sink
---
.../CWE-942/CorsMisconfiguration.ql | 99 +++++++++----------
1 file changed, 47 insertions(+), 52 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index b05e90a6e62..8140dc2d74a 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -118,63 +118,58 @@ class FlowsFromUntrusted extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
- override predicate isSink(DataFlow::Node sink) { any() }
-}
+ override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
-/**
- * Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
- */
-predicate untrustedFlowsToExpr(Expr dst) {
- exists(FlowsFromUntrusted cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
- cfg.hasFlowPath(source, sink) and
- sink.getNode().asExpr() = dst
- )
-}
-
-/**
- * Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
- */
-predicate untrustedFlowsTo(DataFlow::Node dst) { untrustedFlowsToExpr(dst.asExpr()) }
-
-/**
- * Holds if the provided `allowOriginHW` is guarded by a check on an `UntrustedFlowSource`
- * which (supposedly) is an `Origin` header.
- */
-predicate isGuardedByCheckOnUntrusted(HTTP::HeaderWrite allowOriginHW) {
- exists(ControlFlow::ConditionGuardNode cgn, IfStmt ifs |
- exists(Expr child, Expr operand |
- child = ifs.getCond().getAChildExpr*() and
- (
- operand = child or
- operand = child.(LorExpr).getAnOperand() or
- operand = child.(LandExpr).getAnOperand()
- ) and
- (
- exists(DataFlow::CallExpr call | call = operand |
- call.getTarget().hasQualifiedName("strings", "HasSuffix") and
- untrustedFlowsToExpr(call.getArgument(0))
- )
- or
- exists(MapRead mapRead |
- operand = mapRead.asExpr() and
- untrustedFlowsTo(mapRead.getIndex().getAPredecessor*())
- // TODO: add _, ok : map[untrusted]; ok
- )
- or
- exists(EqlExpr comp |
- operand = comp and
- (
- untrustedFlowsToExpr(comp.getLeftOperand()) and
- not comp.getRightOperand().(StringLit).getStringValue() = ""
- or
- untrustedFlowsToExpr(comp.getRightOperand()) and
- not comp.getLeftOperand().(StringLit).getStringValue() = ""
+ predicate isSink(DataFlow::Node sink, ControlFlow::ConditionGuardNode cgn) {
+ exists(IfStmt ifs |
+ exists(Expr child, Expr operand |
+ child = ifs.getCond().getAChildExpr*() and
+ (
+ operand = child or
+ operand = child.(LorExpr).getAnOperand() or
+ operand = child.(LandExpr).getAnOperand()
+ ) and
+ (
+ //
+ exists(DataFlow::CallExpr call | call = operand |
+ call.getTarget().hasQualifiedName("strings", "HasSuffix") and
+ sink.asExpr() = call.getArgument(0)
+ )
+ or
+ exists(MapRead mapRead |
+ operand = mapRead.asExpr() and
+ sink = mapRead.getIndex().getAPredecessor*()
+ // TODO: add _, ok : map[untrusted]; ok
+ )
+ or
+ exists(EqlExpr comp |
+ operand = comp and
+ (
+ sink.asExpr() = comp.getLeftOperand() and
+ not comp.getRightOperand().(StringLit).getStringValue() = ""
+ or
+ sink.asExpr() = comp.getRightOperand() and
+ not comp.getLeftOperand().(StringLit).getStringValue() = ""
+ )
)
)
)
+ |
+ cgn.getCondition() = ifs.getCond()
)
+ }
+}
+
+/**
+ * Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
+ */
+predicate flowsToGuardedByCheckOnUntrusted(HTTP::HeaderWrite allowOriginHW) {
+ exists(
+ FlowsFromUntrusted cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
+ ControlFlow::ConditionGuardNode cgn
+ |
+ cfg.hasFlowPath(source, sink) and cfg.isSink(sink.getNode(), cgn)
|
- cgn.getCondition() = ifs.getCond() and
cgn.dominates(allowOriginHW.getBasicBlock())
)
}
@@ -187,7 +182,7 @@ where
or
allowOriginIsNull(allowOriginHW, message)
) and
- not isGuardedByCheckOnUntrusted(allowOriginHW) and
+ not flowsToGuardedByCheckOnUntrusted(allowOriginHW) and
not exists(ControlFlow::ConditionGuardNode cgn |
cgn.ensures(any(AllowedFlag f).getAFlag().getANode(), _)
|
From 92e0f02d2a2908baa0f28c3faecbddb37aaf5e6d Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Thu, 15 Jul 2021 15:06:28 +0200
Subject: [PATCH 11/14] Remove special cases inside if
---
ql/src/experimental/CWE-942/CorsMisconfiguration.ql | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 8140dc2d74a..bd6297afb92 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -124,11 +124,7 @@ class FlowsFromUntrusted extends TaintTracking::Configuration {
exists(IfStmt ifs |
exists(Expr child, Expr operand |
child = ifs.getCond().getAChildExpr*() and
- (
- operand = child or
- operand = child.(LorExpr).getAnOperand() or
- operand = child.(LandExpr).getAnOperand()
- ) and
+ operand = child and
(
//
exists(DataFlow::CallExpr call | call = operand |
From e92738a93fa493a1245f7acd8a524bb254a04277 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Fri, 16 Jul 2021 00:42:36 +0300
Subject: [PATCH 12/14] Apply suggestions from code review
Co-authored-by: Chris Smowton
---
ql/src/experimental/CWE-942/CorsMisconfiguration.ql | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index bd6297afb92..17f9dd54753 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -122,11 +122,9 @@ class FlowsFromUntrusted extends TaintTracking::Configuration {
predicate isSink(DataFlow::Node sink, ControlFlow::ConditionGuardNode cgn) {
exists(IfStmt ifs |
- exists(Expr child, Expr operand |
- child = ifs.getCond().getAChildExpr*() and
- operand = child and
+ exists(Expr operand |
+ operand = ifs.getCond().getAChildExpr*() and
(
- //
exists(DataFlow::CallExpr call | call = operand |
call.getTarget().hasQualifiedName("strings", "HasSuffix") and
sink.asExpr() = call.getArgument(0)
From 52b650a1be5f1e3ba755ce45dda42f65a4665bc5 Mon Sep 17 00:00:00 2001
From: Slavomir
Date: Fri, 16 Jul 2021 00:01:55 +0200
Subject: [PATCH 13/14] Add AllowOriginHeaderWrite and
AllowCredentialsHeaderWrite classes
---
.../CWE-942/CorsMisconfiguration.ql | 32 ++++++++++++-------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 17f9dd54753..6efb24c852a 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -36,6 +36,20 @@ string headerAllowOrigin() { result = "Access-Control-Allow-Origin".toLowerCase(
*/
string headerAllowCredentials() { result = "Access-Control-Allow-Credentials".toLowerCase() }
+/**
+ * An `Access-Control-Allow-Origin` header write.
+ */
+class AllowOriginHeaderWrite extends HTTP::HeaderWrite {
+ AllowOriginHeaderWrite() { this.getHeaderName() = headerAllowOrigin() }
+}
+
+/**
+ * An `Access-Control-Allow-Credentials` header write.
+ */
+class AllowCredentialsHeaderWrite extends HTTP::HeaderWrite {
+ AllowCredentialsHeaderWrite() { this.getHeaderName() = headerAllowCredentials() }
+}
+
/**
* A taint-tracking configuration for reasoning about when an UntrustedFlowSource
* flows to a HeaderWrite that writes an `Access-Control-Allow-Origin` header's value.
@@ -45,9 +59,7 @@ class FlowsUntrustedToAllowOriginHeader extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
- predicate isSink(DataFlow::Node sink, HTTP::HeaderWrite hw) {
- hw.getHeaderName() = headerAllowOrigin() and sink = hw.getValue()
- }
+ predicate isSink(DataFlow::Node sink, AllowOriginHeaderWrite hw) { sink = hw.getValue() }
override predicate isSanitizer(DataFlow::Node node) {
exists(ControlFlow::ConditionGuardNode cgn |
@@ -65,9 +77,8 @@ class FlowsUntrustedToAllowOriginHeader extends TaintTracking::Configuration {
* also has another HeaderWrite that sets a `Access-Control-Allow-Credentials`
* header to `true`.
*/
-predicate allowCredentialsIsSetToTrue(HTTP::HeaderWrite allowOriginHW) {
- exists(HTTP::HeaderWrite allowCredentialsHW |
- allowCredentialsHW.getHeaderName() = headerAllowCredentials() and
+predicate allowCredentialsIsSetToTrue(AllowOriginHeaderWrite allowOriginHW) {
+ exists(AllowCredentialsHeaderWrite allowCredentialsHW |
allowCredentialsHW.getHeaderValue().toLowerCase() = "true"
|
allowOriginHW.getResponseWriter() = allowCredentialsHW.getResponseWriter()
@@ -79,7 +90,7 @@ predicate allowCredentialsIsSetToTrue(HTTP::HeaderWrite allowOriginHW) {
* UntrustedFlowSource.
* The `message` parameter is populated with the warning message to be returned by the query.
*/
-predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, string message) {
+predicate flowsFromUntrustedToAllowOrigin(AllowOriginHeaderWrite allowOriginHW, string message) {
exists(FlowsUntrustedToAllowOriginHeader cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
cfg.hasFlowPath(source, sink) and
cfg.isSink(sink.getNode(), allowOriginHW)
@@ -94,8 +105,7 @@ predicate flowsFromUntrustedToAllowOrigin(HTTP::HeaderWrite allowOriginHW, strin
* Holds if the provided `allowOriginHW` HeaderWrite is for a `Access-Control-Allow-Origin`
* header and the value is set to `null`.
*/
-predicate allowOriginIsNull(HTTP::HeaderWrite allowOriginHW, string message) {
- allowOriginHW.getHeaderName() = headerAllowOrigin() and
+predicate allowOriginIsNull(AllowOriginHeaderWrite allowOriginHW, string message) {
allowOriginHW.getHeaderValue().toLowerCase() = "null" and
message =
headerAllowOrigin() + " header is set to `" + allowOriginHW.getHeaderValue() + "`, and " +
@@ -157,7 +167,7 @@ class FlowsFromUntrusted extends TaintTracking::Configuration {
/**
* Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
*/
-predicate flowsToGuardedByCheckOnUntrusted(HTTP::HeaderWrite allowOriginHW) {
+predicate flowsToGuardedByCheckOnUntrusted(AllowOriginHeaderWrite allowOriginHW) {
exists(
FlowsFromUntrusted cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
ControlFlow::ConditionGuardNode cgn
@@ -168,7 +178,7 @@ predicate flowsToGuardedByCheckOnUntrusted(HTTP::HeaderWrite allowOriginHW) {
)
}
-from HTTP::HeaderWrite allowOriginHW, string message
+from AllowOriginHeaderWrite allowOriginHW, string message
where
allowCredentialsIsSetToTrue(allowOriginHW) and
(
From 87afdae1c751b34ca5ec9c2245b11a3b9bc5df38 Mon Sep 17 00:00:00 2001
From: Chris Smowton
Date: Fri, 16 Jul 2021 10:47:06 +0100
Subject: [PATCH 14/14] use hasFlowTo where possible
---
ql/src/experimental/CWE-942/CorsMisconfiguration.ql | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
index 6efb24c852a..fa07af5914f 100644
--- a/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
+++ b/ql/src/experimental/CWE-942/CorsMisconfiguration.ql
@@ -91,9 +91,9 @@ predicate allowCredentialsIsSetToTrue(AllowOriginHeaderWrite allowOriginHW) {
* The `message` parameter is populated with the warning message to be returned by the query.
*/
predicate flowsFromUntrustedToAllowOrigin(AllowOriginHeaderWrite allowOriginHW, string message) {
- exists(FlowsUntrustedToAllowOriginHeader cfg, DataFlow::PathNode source, DataFlow::PathNode sink |
- cfg.hasFlowPath(source, sink) and
- cfg.isSink(sink.getNode(), allowOriginHW)
+ exists(FlowsUntrustedToAllowOriginHeader cfg, DataFlow::Node sink |
+ cfg.hasFlowTo(sink) and
+ cfg.isSink(sink, allowOriginHW)
|
message =
headerAllowOrigin() + " header is set to a user-defined value, and " +
@@ -168,11 +168,8 @@ class FlowsFromUntrusted extends TaintTracking::Configuration {
* Holds if the provided `dst` is also destination of a `UntrustedFlowSource`.
*/
predicate flowsToGuardedByCheckOnUntrusted(AllowOriginHeaderWrite allowOriginHW) {
- exists(
- FlowsFromUntrusted cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
- ControlFlow::ConditionGuardNode cgn
- |
- cfg.hasFlowPath(source, sink) and cfg.isSink(sink.getNode(), cgn)
+ exists(FlowsFromUntrusted cfg, DataFlow::Node sink, ControlFlow::ConditionGuardNode cgn |
+ cfg.hasFlowTo(sink) and cfg.isSink(sink, cgn)
|
cgn.dominates(allowOriginHW.getBasicBlock())
)