go-httpbin versions <= 2.17.1 contain a Reflected XSS vulnerability. Endpoints like /response-headers and /base64 allow users to define the response Content-Type while simultaneously reflecting input. Attackers can force the server to serve text/html containing malicious JavaScript.
A classic tale of a debugging tool working exactly as intended, right up until it wasn't. The popular go-httpbin service allowed attackers to coerce the server into serving HTML payloads by manipulating the Content-Type header, turning a harmless echo server into an XSS launchpad.
We all love httpbin. It’s the Swiss Army knife of HTTP debugging. Need to test how your client handles a 418 I'm a Teapot? It’s got you. Need to verify your headers are sending correctly? Just ask it to echo them back. It is, by design, an obedient servant that does exactly what you tell it to do.
But here is the problem with obedient servants: they don't ask questions. In the security world, we call this a "confused deputy," but in this specific case, it’s more like a "gullible parrot." The go-httpbin library (a Go port of the original Python httpbin) implemented features allowing clients to control the response headers and the response body simultaneously.
Imagine you tell a server: "Hey, please reply to me with this specific script, and oh, by the way, tell my browser it's definitely an HTML page, not JSON." The server, trying to be helpful, obliges. The result? Your browser executes the script. This isn't a complex buffer overflow or a heap spray; it's a logic flaw born from the assumption that the user knows best.
The vulnerability lies in the intersection of two features: Header Control and Data Reflection. The library exposes endpoints like /response-headers and /base64. These endpoints are designed to let developers test their HTTP clients by simulating various server responses.
In the vulnerable code, the application iterates over the query parameters provided by the user. If it finds a parameter named Content-Type, it dutifully overrides the response header. Simultaneously, it takes other parameters (or decoded base64 data) and writes them directly to the response body.
This is the critical failure: Output Encoding. The application assumed that because it usually returns JSON (application/json), it didn't need to escape HTML characters like < or >. But since the user controls the Content-Type, they can switch the context to text/html. The browser sees the header, ignores the fact that the body looks like JSON, and parses any HTML tags it finds inside the reflected string.
Let's look at the smoking gun in handlers.go before the patch. The logic was deceptively simple—too simple.
// Vulnerable logic (simplified)
func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) {
// 1. Iterate over query params
for k, v := range r.URL.Query() {
// 2. Set headers directly from input
w.Header().Set(k, v[0])
}
// 3. Reflect the headers back in the body as JSON
json.NewEncoder(w).Encode(r.URL.Query())
}See the issue? If k is Content-Type and v[0] is text/html, the header is set. Then, the json.Encoder writes the query parameters into the body. While JSON encoders escape quotes, they often do not escape HTML characters like < and > by default unless specifically configured to be HTML-safe.
The fix, introduced in commit 0decfd1a2e88d85ca6bfb8a92421653f647cbc04, adds a sanity check. It implements a "safe list" of content types (application/json, text/plain, etc.).
// The Fix: Check for danger
func (h *HTTPBin) mustEscapeResponse(contentType string) bool {
if h.unsafeAllowDangerousResponses {
return false
}
// Returns true if Content-Type is NOT in the safe list
return isDangerousContentType(contentType)
}
// Inside the handler
if h.mustEscapeResponse(contentType) {
// Force escape the output if the type is dangerous (e.g., text/html)
encodedData = html.EscapeString(encodedData)
}They didn't just remove the feature; they neutered it. If you ask for text/html now, you get it, but the body is HTML-escaped, rendering the script inert.
Exploiting this requires nothing more than a web browser and a creative URL. We don't need fancy C2 servers or shellcode. We just need to construct a URL that sets the MIME type and injects the payload.
The /response-headers endpoint reflects query parameters into a JSON object. We inject Content-Type=text/html and our XSS payload into a dummy parameter.
GET /response-headers?Content-Type=text/html&xss=<img src=x onerror=alert(document.domain)> HTTP/1.1
Host: vulnerable-httpbin.comThe server responds with:
HTTP/1.1 200 OK
Content-Type: text/html
{
"Content-Type": "text/html",
"xss": "<img src=x onerror=alert(document.domain)>"
}The browser ignores the curly braces, sees the <img> tag, and fires the alert.
The /base64/{data} endpoint is even cleaner. It decodes the input and writes it raw. If we combine this with a query param to set the header, it's game over.
<script>alert('Pwned')</script>PHNjcmlwdD5hbGVydCgnUHduZWQnKTwvc2NyaXB0Pg==/base64/PHNjcmlwdD5hbGVydCgnUHduZWQnKTwvc2NyaXB0Pg==?Content-Type=text/htmlThe response is a pure HTML page containing our script, executed immediately upon load.
You might be thinking, "It's a debugging tool, who cares?" But httpbin instances are frequently deployed in internal developer networks, sometimes even exposed to the public internet for convenience.
If an attacker targets a developer using a self-hosted go-httpbin instance:
httpbin. The developer clicks it.It is a perfect example of how non-production tools can introduce production-level risks.
The remediation is straightforward: stop trusting users with the Content-Type header if you are going to reflect their input. The maintainer patched this in version v2.18.0.
To fix this in your environment:
go.mod to use github.com/mccutchen/go-httpbin/v2 v2.18.0.UNSAFE_ALLOW_DANGEROUS_RESPONSES enabled. If you do, you are re-enabling the vulnerability. Turn it off unless you have a very specific, isolated use case.For security teams, this serves as a reminder to inventory "dev tools" running in your environment. They often lack the hardening of production apps because everyone assumes "it's just for testing."
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
go-httpbin mccutchen | <= 2.17.1 | 2.18.0 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (Web) |
| CVSS | 5.3 (Medium) |
| CWE | CWE-79 (XSS) |
| Exploit Status | Functional PoC Available |
| Component | go-httpbin |
| Impact | Code Execution (Browser Context) |
Get the latest CVE analysis reports delivered to your inbox.