go-httpbin < 2.18.0 contains a Reflected XSS vulnerability. By manipulating the 'Content-Type' query parameter on specific endpoints, attackers can force the server to render arbitrary JSON or Base64 input as HTML, executing malicious JavaScript in the victim's browser.
A classic tale of a feature becoming a bug. The popular debugging tool go-httpbin allowed users to control both the reflected content and the Content-Type header, creating a trivial path to Cross-Site Scripting (XSS).
In the world of API development, httpbin is sacred ground. It is the ultimate sanity check—a server that simply echoes back whatever you throw at it. Need to verify your headers? Check httpbin. Need to see if your JSON body is formatting correctly? httpbin. It is designed to be a mirror.
The Golang port, go-httpbin, is widely used in containerized environments and CI/CD pipelines because it's fast, lightweight, and compiles to a single binary. But here lies the irony: the very purpose of the tool is to reflect input.
Security engineers often joke that an echo server is just an XSS vulnerability waiting to happen. Usually, frameworks prevent this by strictly enforcing Content-Type: application/json or sanitizing output. But go-httpbin made a critical error in its quest for flexibility: it handed the keys to the Content-Type header directly to the user. It’s like a funhouse mirror that, if you look at it from the wrong angle, reaches out and punches you in the face.
The vulnerability resides in the core philosophy of the application: "Give the user what they want." Specifically, the endpoints /response-headers and /base64 were designed to let developers test how their clients handle specific server responses.
If a developer wanted to test how their app handles a text/html response, they could simply append ?Content-Type=text/html to the URL. The server would dutifully set the response header to text/html.
Simultaneously, the /response-headers endpoint takes all other query parameters and reflects them back in the response body as a JSON object. Do you see the train wreck coming?
If you tell the server "Set the content type to HTML" and also "Put this <script> tag in the JSON body," the browser receives a payload that it thinks is HTML (because the server said so). It parses the reflected JSON, encounters the script tag, and executes it. The browser doesn't care that the body looks like JSON; the Content-Type header is the ultimate authority here.
The fix, implemented in commit 0decfd1a2e88d85ca6bfb8a92421653f647cbc04, is a lesson in defensive programming. The maintainers realized they couldn't just trust the user input anymore.
The Vulnerable Logic: Previously, the code simply took the query parameter and wrote it to the response header writer. It was a direct pipe from user input to HTTP headers.
The Fix:
The patch introduces a whitelist (or allowlist) strategy. They defined a map of safeContentTypes:
var safeContentTypes = map[string]bool{
"text/plain": true,
"application/json": true,
"application/octet-stream": true,
}They then implemented a helper function, mustEscapeResponse, which returns true if the requested content type is not in this safe list.
In the handler for /response-headers, the logic now branches. If the content type is deemed "dangerous" (e.g., text/html), the application forcibly HTML-escapes every key and value in the JSON response before sending it:
// heavily simplified pseudocode of the fix
if mustEscapeResponse(contentType) {
// Create a new map where every string is HTML escaped
safeHeaders := make(map[string]string)
for k, v := range headers {
safeHeaders[html.EscapeString(k)] = html.EscapeString(v)
}
writeJSON(safeHeaders)
}This effectively neutralizes the attack. Even if the browser tries to render the JSON as HTML, the <script> tags are converted to <script>, rendering them harmless text.
Let's construct the attack. We have two primary vectors here: reflected headers and base64 decoding.
We target /response-headers. We need to do two things: set the MIME type to HTML and inject a script payload.
The Payload:
GET /response-headers?Content-Type=text/html¶m=<img src=x onerror=alert(1)>The Server Response:
HTTP/1.1 200 OK
Content-Type: text/html
{
"Content-Type": "text/html",
"param": "<img src=x onerror=alert(1)>"
}The browser sees text/html, ignores the curly braces, parses the <img> tag, fails to load src=x, and triggers onerror=alert(1). Game over.
The /base64 endpoint is even more direct. It decodes whatever you send it.
<script>alert(document.domain)</script> to Base64: PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+.GET /base64/PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+?content-type=text/htmlThe server decodes the string and serves it back as text/html. The browser executes the script immediately. This is a "pure" XSS—no JSON wrapping to worry about.
I hear this argument constantly: "But httpbin is just for testing! Who cares if it has XSS?"
This mindset is dangerous. go-httpbin is frequently deployed in:
If you can execute Javascript in the context of the domain hosting httpbin, you own that session. If httpbin is hosted on subdomain.corp.com and the company uses wildcard cookies for *.corp.com, the attacker potentially compromises the entire corporate session.
The primary fix is simple: Upgrade to version 2.18.0.
However, the maintainers recognized that some users might actually rely on the dangerous behavior for legitimate (albeit terrifying) testing scenarios. They included an escape hatch: the environment variable UNSAFE_ALLOW_DANGEROUS_RESPONSES=true.
[!WARNING] Do not enable
UNSAFE_ALLOW_DANGEROUS_RESPONSESin any environment accessible by untrusted users. It effectively reverts the patch and re-opens the XSS hole.
Defense in Depth:
Beyond patching, ensure your deployment includes the X-Content-Type-Options: nosniff header globally. While the exploit specifically sets the Content-Type, nosniff is a good general practice to prevent browsers from second-guessing MIME types in other edge cases.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
go-httpbin mccutchen | < 2.18.0 | 2.18.0 |
| Attribute | Detail |
|---|---|
| GHSA ID | GHSA-528q-4pgm-wvg2 |
| CVSS | 6.1 (Medium) |
| CWE | CWE-79 (XSS) |
| Attack Vector | Network |
| Privileges Required | None |
| User Interaction | Required (Phishing) |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Get the latest CVE analysis reports delivered to your inbox.