Oxeye’s security research team has found a security vulnerability in Golang-based applications. Under certain conditions, it lets a threat actor bypass validations based on HTTP request parameters due to the use of unsafe URL parsing.
Each one representing the internet address of a disparate web asset, uniform resource locators (URLs) are essential. A browser initiates an HTTP request to fetch a given resource identified by its URL. The targeted web server translates the textual URL to a manner it understands—referred to as URL parsing.
This process is usually done according to RFC 3986, which provides instructions on proper URL parsing. Here is an excerpt that illustrates URL components:
Every language has its own implementation of this logic. For example, in Python, you can use the urllib.parse module. In Golang, it’s the net/url module, and so on.
It would be reasonable to assume that all implementations are the same, right? Sorry… in reality, each follows the same guidelines but contains minor differences. This can result in parsing inconsistencies of the same input.
Recently Golang’s core team released a patch that fundamentally changes how that language parses URLs. Before version 1.17, Golang considered semicolons within a URL query portion as a valid delimiter—like how an ampersand is assessed. This behavior originates from the parseQuery method:
For example, the following URL yields two query parameters, name and company, rather than a single name parameter:
Golang has changed this behavior starting with version 1.17. Now its parseQuery method returns an error if the URL query contains a semicolon:
Although the parseQuery method was fixed to properly return an error when input contains a semicolon, one of the methods responsible for getting the parsed query string blithely ignores the returned error:
Moreover, Golang offers a built-in function to proxy HTTP traffic using its httputil/reverseproxy.go module. To create a new reverse proxy, it provides the NewSingleHostReverseProxy method:
This takes the raw query string from the request rather than the parsed one. But rather than parse it, it sends it as is.
You’ve now seen the behavioral difference between Golang versions, but how do the points connect?
Consider the following two Golang-based services. The first is a user-facing application running on a version 1.17 or later, while the second, a backend service, runs on an earlier version.
A user makes an HTTP request to the first service, supplying a name query parameter. This first service decides whether to pass through the request based on the supplied parameter.
Suppose a user appends a semicolon to the name parameter when parsing the query string. In that case, the first service ignores its existence; rather than make a logic decision based on it, the request gets forwarded to the second (backend) service. The latter receives the transaction and treats the parameter without the semicolon.
This means miscreants are able to smuggle requests containing query parameters that normally would be rejected. During our research, Oxeye found multiple instances of this vulnerability in several open source projects.
From the official website of Harbor – “Harbor is an open source registry that secures artifacts with policies and role-based access control… Harbor, a CNCF Graduated project, delivers compliance, performance, and interoperability to help you consistently and securely manage artifacts across cloud-native compute platforms like Kubernetes and Docker.”
In other words, Harbor lets you manage your application artifacts. One of its many features is that you can centralize multiple image registries under one roof.
During our research, we found that under specific deployments, an authenticated user (even with the lowest permission level) can issue a special request to read image layers of restricted projects they don’t have access to.
Harbor uses the distribution container registry to store and manage Docker images.
While reading the Docker registry V2 API, we saw an endpoint that allows Cross Repository Blob Mount. This function saves disk space and network bandwidth while storing/uploading Docker images pointing to the same file system layers.
Here you see a request that attempts to mount a blob from a different repository:
To mount a blob from a foreign repository, the endpoint expects to receive one URL path parameter—name (specifying the target repository)—and two query parameters:
When Harbor receives such a request, it attempts to perform two permission validations:
Oxeye looked for Harbor deployments that have this Golang version discrepancy. We discovered that Bitnami (a company acquired by VMWare that holds Helm charts and Docker images for various projects) has this exact setup. Its core Harbor microservice runs on Golang 1.17, while the Docker registry microservice runs on version 1.15.
Twice we attempted to send the cross-repository blob mount request through Harbor—once with a valid from value and once with that same parameter that included a trailing semicolon.
Original request – Blocked due to Harbor authorization checks:
Modified request – Bypassing Harbor authorization checks, the response confirms the blob (received from the registry) mounted successfully:
To successfully exploit this vulnerability, an attacker would need to know a blob digest hash to mount it. This challenge will be discussed in the next blog post.
During our research, Oxeye discovered other projects that parse query strings using the same unsafe method we’ve examined.
Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. One feature permits the blocking of specific requests based on query parameters. For example, the following configuration doesn’t forward requests that have the foo parameter:
Behind the scenes, Traefik uses the vulnerable query string parsing method. Proxying a request to a backend server passes both the original request and query string. A request with the foo parameter containing a semicolon would bypass this rule matcher.
Skipper is an HTTP router and reverse proxy for service composition that also uses vulnerable query parsing. For example, requests having the foo parameter will be rejected when using the following configuration:
A request with the foo parameter and trailing semicolon would bypass the rule matcher, resulting in Skipper not rejecting this request.
Golang offers other methods to parse query strings, so consider using those. For example, the ParseQuery method lets developers consider the returned error:
If you still wish to use the vulnerable method, consider sanitizing the raw query such that input that includes a semicolon is rejected before the method call (although there are other ways to masquerade input, e.g., URL encoding).
To help the community find such vulnerabilities, Oxeye wrote a Semgrep rule that alerts you if your codebase uses the vulnerable method. It suggests an auto-fix using the more secure method:
All vulnerabilities mentioned above have been disclosed to their respective vendors and are fixed in the latest versions.
Oxeye provides a cloud-native application security solution designed specifically for modern architectures. The company enables customers to quickly identify and resolve all application-layer risks as an integral part of the software development lifecycle by offering a seamless, comprehensive, and effective solution that ensures touchless assessment, focus on the exploitable risks, and actionable remediation guidance. Built for Dev and AppSec teams, Oxeye helps to shift security to the left while accelerating development cycles, reducing friction, and eliminating risks. To learn more, please visit www.oxeye.io.