If you read this blog regularly, you know that I like looking at CVE. I do that to create labs and security code review content for PentesterLab and for my Web Security Code Review Training. This article covers my latest discoveries...
It all started with play with docker and CVE-2023-28109. A classic example of vulnerable CORS policy. You can find a copy of the commit fixing the issue below (ed82247c9ab7990ad76ec2bf1498c2b2830b6f1a):
--- a/handlers/bootstrap.go
+++ b/handlers/bootstrap.go
@@ -70,10 +70,10 @@ func Register(extend HandlerExtender) {
corsHandler := gh.CORS(gh.AllowCredentials(), gh.AllowedHeaders([]string{"x-requested-with", "content-type"}), gh.AllowedMethods([]string{"GET", "POST", "HEAD", "DELETE"}), gh.AllowedOriginValidator(func(origin string) bool {
if strings.Contains(origin, "localhost") ||
- strings.HasSuffix(origin, "play-with-docker.com") ||
- strings.HasSuffix(origin, "play-with-kubernetes.com") ||
- strings.HasSuffix(origin, "docker.com") ||
- strings.HasSuffix(origin, "play-with-go.dev") {
+ strings.HasSuffix(origin, ".play-with-docker.com") ||
+ strings.HasSuffix(origin, ".play-with-kubernetes.com") ||
+ strings.HasSuffix(origin, ".docker.com") ||
+ strings.HasSuffix(origin, ".play-with-go.dev") {
return true
}
return false
We can see that the vulnerable code allowed someone with a domain ending in play-with-docker.com
, play-with-kubernetes.com
, docker.com
, or play-with-go.dev
to bypass the security check. For example, the domain penetesterlab-docker.com
could be used since it ends with docker.com
. This is a classic example of vulnerable CORS policy.
[As a side note, using strings.HasSuffix(...)
and only validating the hostname or domain should be improved by also checking the scheme to enforce the use of TLS (e.g., forcing https instead of also allowing http).]
BUT, wait a minute...
Now if you look at the line before the vulnerable code, you may spot another issue that wasn't fixed: strings.Contains(origin, "localhost")
. Basically, any domain that contains the string localhost
can be used to bypass this check. For example, localhost.pentesterlab.com
can be used.
That got me interested, and I decided to go down the rabbit hole...
From there, I looked at a few more codebases on GitHub and found other examples of vulnerable code.
Another example of a vulnerable check can be found in this snippet:
if strings.HasPrefix(origin, "http://localhost") {
This only checks that the origin starts with http://localhost
. We can bypass this check using http://localhost.pentesterlab.com
, for example.
Another example of a vulnerable check can be found in this snippet:
return strings.Contains(origin, "example.com")
This only checks that the origin contains example.com
. We can bypass this check using example.com.pentesterlab.com
, for example.
I really like this last one:
config.AllowOriginFunc = func(origin string) bool {
// get allowed origin domains from env
originsFromEnv := os.Getenv("ALLOW_ORIGIN_DOMAINS")
origins := []string{"localhost", "example.com", "127.0.0.1"}
isAllowedThisOrigin := false
origins = append(origins, strings.Split(originsFromEnv, ",")...)
for _, allowedOrigin := range origins {
if strings.Contains(origin, allowedOrigin) {
isAllowedThisOrigin = true
break
}
}
return isAllowedThisOrigin
}
We can see that the code allows a hardcoded list of origins origins := []string{"localhost", "example.com", "127.0.0.1"}
as well as an environment variable ALLOW_ORIGIN_DOMAINS
. The check is done using strings.Contains(...)
, which is not secure as we saw before. We can bypass this check using example.com.pentesterlab.com
, for example.
What makes this snippet my favorite is how the environment variable is handled... If the environment variable ALLOW_ORIGIN_DOMAINS
is not set, strings.Split(originsFromEnv, ",")
will return an empty string. The list of allowed origins origins
will contain an empty string. When matching the origin origin
, the code will check if origin
contains an empty string, which will always return true, effectively allowing all origins.
I recently gave a talk on "What developers get for free," explaining how important it is to provide functions/methods at the language level to make developers' lives easier. One of my final points was that languages should provide a built-in way of checking if a hostname or an origin is part of a domain. I think this article highlights why...