CORS Vulnerabilities in Go: Vulnerable Patterns and Lessons

Published: 02 Dec 2024

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...

Play with docker...

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...

Starts with...

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.

Contains...

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.

My favorite one so far

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.

Final Thoughts

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...

Photo of Louis Nyffenegger
Written by Louis Nyffenegger
Founder and CEO @PentesterLab