Exploiting Parameter Pollution in Golang Web Apps

Authorization Vulnerabilities in Concourse CI

Rick Ramgattie
4 min readFeb 22, 2023

tl;dr

Priority differences in common golang HTTP request argument fetchers can result in parameter pollution. Security checks based on these parameter fetching priority differences granted me the ability to bypass horizontal authorization checks and access resources outside of my team’s scope in Concourse CI. This issue was disclosed to VMWare and was remediated here. You can skip to Why are we getting the “team_name” with two different functions? for the parameter pollution.

Intro:

A couple months ago I was reading Assetnote’s blog and was inspired to find vulnerabilities in an open source project. I had just finished Let’s Go! by Alex Edwards so I decided to target a Golang web app, which led me to Concourse CI. I cloned the repo and did a quick scan with gokart, gosec, and semgrep but nothing stuck out to me. Concourse CI has a need for both vertical and horizontal authorization verification to support its concept of “teams”.

Figure 1. Horizontal vs. Vertical Authorization

Reversing Authorization Flow:

Since Concourse CI is an open source project I cloned the repo and set it up with some users, teams, and pipelines. After turning on verbose mode on the CLI tool I issued some requests and saw that all of the parameters were being passed in the URL. In the example below you can see that I am exposing “team1pipe1” and that I am issuing that request as someone who belongs to “team1”.

PUT /api/v1/teams/team1/pipelines/team1pipe1/expose HTTP/1.1
Host: localhost:8080
User-Agent: <USER_AGENT>
Authorization: bearer <AUTH_TOKEN>
Connection: close

I searched for that route and traced the code to find that there were x2 authorization checks. The first one is “checkAuthorizationHandler” which verifies the user’s ability to access a team, and the second one is “pipelineScopedHandler” which verifies the team’s access to the pipeline.

Figure 2. checkAuthorizationHandler code snippet
Figure 3. pipelineScopedHandler code snippet

Why are we getting the “team_name” with two different functions?

In checkAuthorizationHandler we are using r.URL.Query().Get(“:team_name”) and in pipelineScopedHandler we are using r.FormValue(“:team_name”). I looked both of those up in the docs and read them side-by-side to see if there were any noticeable differences.

func (u *URL) Query() Values

Query parses RawQuery and returns the corresponding values. It silently discards malformed value pairs. To check errors use ParseQuery.

func (r *Request) FormValue(key string) string

FormValue returns the first value for the named component of the query. POST and PUT body parameters take precedence over URL query string values. FormValue calls ParseMultipartForm and ParseForm if necessary and ignores any errors returned by these functions. If key is not present, FormValue returns the empty string. To access multiple values of the same key, call ParseForm and then inspect Request.Form directly.

According to the docs, FormValue will prioritize POST and PUT body parameters over URL query parameters. The difference in priority means that we can provide different values to the corresponding functions, this is called parameter pollution.

Exploiting Parameter Pollution

Based on what we learned about request parameter priority we should be able to pass a different team_name to checkAuthorizationHandler and pipelineScopedHandler.

We know that the team_name in the URL is used to the check the “AUTH_TOKEN” owner’s authorization, but the team_name used to check the team’s access to a pipeline uses r.FormValue which means that it can be sent in the PUT body.

If we add a colon to the parameter name (it was auto added by the framework for the URL path parameter) we can bypass the authorization check in pipelineScopedHandler and expose a pipeline that belongs to another team. Note that in order for the body parameter to be picked up you will need to pass the corresponding content-type and content-length.

PUT /api/v1/teams/team1/pipelines/team2pipe1/expose HTTP/1.1
Host: localhost:8080
User-Agent: <USER_AGENT>
Authorization: bearer <AUTH_TOKEN>
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 16

:team_name=team2

Responsible Disclosure

After I exploited this on my local setup I began the responsible disclosure process. It took me a while to find someone, but eventually I realized that there was a Concourse team at VMWare. Shortly after I reached out to them they responded and confirmed that this was a valid vulnerability. They remediated the vulnerability by removing the usage of r.FormValue to ensure body parameters couldn’t pollute the values used in the secondary authorization check. You can read about in the writeup they did when they assigned it CVE-2022–31683.

Conclusion

I was able to bypass horizontal authorization controls by exploiting parameter pollution. The inconsistency in how the application sourced, parsed, and prioritized request parameters allowed us to pass different values to security critical functionality that were dependent on each other. To ensure your application isn’t subject to parameter pollution you should stick to the same functions throughout your application.

--

--