Oct 30, 2026·10 min read·77 visits
Versions of go-httpbin prior to v2.18.0 allow unauthenticated callers to set arbitrary Content-Type response headers while reflecting unescaped user input in the response body on the /response-headers and /base64 endpoints, resulting in reflected Cross-Site Scripting (CWE-80).
CVE-2025-45286 is a reflected Cross-Site Scripting (XSS) vulnerability in mccutchen/go-httpbin, a Go implementation of the HTTP request and response inspection service httpbin. In versions prior to v2.18.0, the application allows unauthenticated clients to arbitrarily control the HTTP Content-Type response header via query parameters on the /response-headers, /base64/{payload}, and /base64/decode/{payload} endpoints. Because user-controlled input is reflected in the HTTP response body without HTML entity encoding, specifying an executable media type such as text/html causes client browsers to parse and execute embedded HTML and JavaScript payloads within the security context of the go-httpbin origin.
The mccutchen/go-httpbin library and standalone service is a Go port of Kenneth Reitz's httpbin utility. Software developers and automated testing pipelines deploy it as a microservice or embedded HTTP handler to inspect client requests and simulate server behaviors, including custom status codes, response delays, redirects, and header reflections. Because the service is designed to echo client-supplied parameters directly back to the caller, its endpoints expose a broad attack surface if deployed on a shared domain or accessed directly via a web browser.
CVE-2025-45286 (also tracked as GHSA-528q-4pgm-wvg2 and GO-2025-3554) is a reflected Cross-Site Scripting (XSS) vulnerability classified under CWE-80 (Improper Neutralization of Script-Related HTML Tags in a Web Page), a child weakness of CWE-79. The flaw affects all releases of go-httpbin prior to version v2.18.0. It occurs across three distinct routing endpoints: /response-headers, /base64/{payload}, and /base64/decode/{payload}.
Each of these affected endpoints performs two operations: it reflects user-supplied data into the HTTP response body, and it allows the caller to specify the outgoing Content-Type header via a URL query parameter. When an attacker sets the Content-Type header to an executable document type—such as text/html, application/xhtml+xml, or image/svg+xml—and injects HTML markup into the reflected parameters, a victim's web browser interprets the response body as an active HTML document. Any JavaScript included in the payload executes within the origin context of the go-httpbin instance.
The root cause of CVE-2025-45286 is the combination of unrestricted HTTP response header manipulation and the absence of context-aware output encoding when generating HTTP response bodies. In web applications, returning raw or JSON-serialized user input is safe from XSS only if the browser is instructed to treat the payload as inert data (for example, via Content-Type: text/plain or Content-Type: application/json accompanied by X-Content-Type-Options: nosniff). When the client can override the Content-Type header, the trust boundary protecting the response body format breaks down.
In the /response-headers endpoint, the ResponseHeaders handler iterates over all URL query parameters (r.URL.Query()) and appends each key-value pair directly to the HTTP response headers using w.Header().Add(k, v). If the query string does not explicitly include a Content-Type parameter, the handler defaults to application/json. The handler then serializes the entire query parameter map into the response body using Go's encoding/json package via the internal helper mustMarshalJSON(w, args). If an attacker supplies Content-Type=text/html alongside another parameter containing HTML tags, the response is served with a text/html MIME type, causing the browser to scan the JSON output and render any embedded HTML tags.
A similar architectural flaw exists in the Base64 handler, which backs both the /base64/{payload} and /base64/decode/{payload} routes. This handler extracts a base64-encoded string from the URL path, decodes it into raw bytes, and writes those bytes directly to the HTTP response stream. To support flexible testing, the handler checks for a content-type query parameter (r.URL.Query().Get("content-type")), defaulting to text/plain only when the parameter is omitted.
Because the Base64 handler performs no validation on the requested content-type and writes the decoded byte slice verbatim via writeResponse(w, http.StatusOK, ct, result), an attacker can encode an arbitrary HTML document or script tag into base64 and append ?content-type=text/html to the URL. Upon receiving the response, the browser parses the decoded bytes as standard HTML markup and executes the script.
Prior to version v2.18.0, the vulnerable handlers in httpbin/handlers.go trusted the client-supplied Content-Type without inspecting its media type or escaping the corresponding response body. Below is the vulnerable implementation of ResponseHeaders and Base64:
// Vulnerable implementation in httpbin/handlers.go (prior to v2.18.0)
func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) {
args := r.URL.Query()
for k, vs := range args {
for _, v := range vs {
// Attacker can inject Content-Type: text/html here
w.Header().Add(k, v)
}
}
if contentType := w.Header().Get("Content-Type"); contentType == "" {
w.Header().Set("Content-Type", jsonContentType)
}
// args are marshaled without HTML escaping; unsafe if Content-Type is text/html
mustMarshalJSON(w, args)
}
func (h *HTTPBin) Base64(w http.ResponseWriter, r *http.Request) {
// ... [base64 decoding logic into 'result' byte slice] ...
ct := r.URL.Query().Get("content-type")
if ct == "" {
ct = textContentType
}
// Raw decoded bytes written directly with user-controlled Content-Type
writeResponse(w, http.StatusOK, ct, result)
}In commit 0decfd1a2e88d85ca6bfb8a92421653f647cbc04, the maintainer addressed the vulnerability by introducing a strict allowlist of non-executable media types in httpbin/helpers.go and a conditional escaping mechanism in httpbin/httpbin.go:
// Patched helper in httpbin/helpers.go
var safeContentTypes = map[string]bool{
"text/plain": true,
"application/json": true,
"application/octet-string": true,
}
func isDangerousContentType(ct string) bool {
mediatype, _, err := mime.ParseMediaType(ct)
if err != nil {
// Treat unparseable or malformed MIME types as dangerous by default
return true
}
return !safeContentTypes[mediatype]
}
// Method added in httpbin/httpbin.go
func (h *HTTPBin) mustEscapeResponse(contentType string) bool {
if h.unsafeAllowDangerousResponses {
return false
}
return isDangerousContentType(contentType)
}With mustEscapeResponse in place, the handlers in httpbin/handlers.go were updated to apply html.EscapeString to all reflected data whenever a non-allowlisted Content-Type is requested:
// Patched implementation in httpbin/handlers.go (v2.18.0)
func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) {
args := r.URL.Query()
contentType := args.Get("Content-Type")
for k, vs := range args {
for _, v := range vs {
w.Header().Add(k, v)
}
}
if contentType == "" {
w.Header().Set("Content-Type", jsonContentType)
}
// Escape keys and values if the Content-Type is not on the safe allowlist
if h.mustEscapeResponse(contentType) {
tmp := make(url.Values, len(args))
for k, vs := range args {
for _, v := range vs {
tmp.Add(html.EscapeString(k), html.EscapeString(v))
}
}
args = tmp
}
mustMarshalJSON(w, args)
}
func (h *HTTPBin) Base64(w http.ResponseWriter, r *http.Request) {
// ... [base64 decoding logic] ...
if ct == "" {
ct = textContentType
}
// HTML-escape the raw decoded bytes if Content-Type is dangerous
if h.mustEscapeResponse(ct) {
result = []byte(html.EscapeString(string(result)))
}
writeResponse(w, http.StatusOK, ct, result)
}> [!NOTE]
> Fix Completeness Assessment: In ResponseHeaders, the patched code extracts contentType := args.Get("Content-Type"), which performs a case-sensitive map lookup on url.Values. However, w.Header().Add(k, v) canonicalizes header names using textproto.CanonicalMIMEHeaderKey. If a user passes a lowercase parameter ?content-type=text/html, args.Get("Content-Type") returns an empty string "". Fortunately, this does not create a bypass: when contentType == "", the subsequent check w.Header().Set("Content-Type", jsonContentType) overwrites the lowercase header with application/json, neutralizing the HTML rendering context. Additionally, mime.ParseMediaType properly strips parameters (such as ; charset=utf-8) and converts the media type to lowercase before checking safeContentTypes, preventing parameter-based allowlist bypasses.
Exploiting CVE-2025-45286 requires no authentication or specialized configuration; the default setup of go-httpbin prior to v2.18.0 is vulnerable out of the box. Because this is a reflected XSS vulnerability, successful exploitation requires user interaction (UI:R), meaning an attacker must entice a victim into navigating to a crafted URL in a web browser.
/response-headers EndpointTo exploit the /response-headers endpoint, the attacker constructs a URL containing Content-Type=text/html alongside an arbitrary query parameter holding the HTML/JavaScript payload:
GET /response-headers?Content-Type=text/html&xss=%3Cimg/src/onerror=alert(%27xss%27)%3E HTTP/1.1
Host: vulnerable-httpbin.example.comThe vulnerable server responds with an HTTP 200 status, sets the Content-Type header to text/html, and outputs the JSON representation of the query parameters:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 69
{"Content-Type":["text/html"],"xss":["<img/src/onerror=alert('xss')>"]}When the victim's browser receives this response, it parses the body as an HTML document, encounters the <img> tag, fails to load the invalid src attribute, and immediately executes the onerror JavaScript handler.
/base64/{payload} and /base64/decode/{payload} EndpointsTo exploit the base64 decoding endpoints, the attacker first base64-encodes the desired HTML payload. For example, <img/src/onerror=alert('xss')> encodes to PGltZy9zcmMvb25lcnJvcj1hbGVydCgneHNzJyk+. The attacker then places this encoded string into the URL path and appends ?content-type=text/html:
GET /base64/PGltZy9zcmMvb25lcnJvcj1hbGVydCgneHNzJyk+?content-type=text/html HTTP/1.1
Host: vulnerable-httpbin.example.comThe identical payload also functions against /base64/decode/PGltZy9zcmMvb25lcnJvcj1hbGVydCgneHNzJyk+?content-type=text/html. The server decodes the path segment and returns the exact HTML string with a text/html header:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 30
<img/src/onerror=alert('xss')>CVE-2025-45286 carries a CVSS v3.1 base score of 6.1 (Medium) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N, and a CVSS v4.0 score of 5.1 (Medium). The Scope is marked as Changed (S:C) because although the vulnerable component is the go-httpbin web server, the security impact manifests within the client's web browser environment.
For standalone go-httpbin deployments running on dedicated, isolated origins without authentication state, the practical impact of XSS is limited to client-side phishing, content spoofing, or browser-based request forgery against internal network resources accessible from the victim's machine. Because go-httpbin does not natively manage user accounts or sensitive databases, an attacker cannot extract application-specific credentials from a standalone instance.
However, the severity increases substantially when go-httpbin is deployed under a shared corporate domain (for example, mounted at a subpath like https://api.example.com/httpbin/ or hosted on a subdomain where parent domain cookies are scoped with Domain=.example.com). In such architectures, arbitrary JavaScript execution allows an attacker to read non-HttpOnly session cookies (T1539), extract Cross-Site Request Forgery (CSRF) tokens, access localStorage or sessionStorage data, and issue authenticated asynchronous HTTP requests (fetch or XMLHttpRequest) to other services sharing the same origin.
From a threat intelligence perspective, the Exploit Prediction Scoring System (EPSS) assigns CVE-2025-45286 a score of 0.00267 (0.27%, 18.89th percentile), indicating a low likelihood of widespread automated exploitation. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, though CISA's Vulnrichment SSVC assessment notes that proof-of-concept payloads are publicly documented (poc).
The primary remediation for CVE-2025-45286 is to upgrade mccutchen/go-httpbin to version v2.18.0 or later. Projects embedding go-httpbin as a Go module library should update their go.mod dependency and rebuild the binary:
go get -u github.com/mccutchen/go-httpbin/v2@v2.18.0
go mod tidyVersion v2.18.0 introduces an opt-in backwards-compatibility setting for environments that explicitly depend on unescaped responses for non-browser testing. Operators must verify that the CLI flag -unsafe-allow-dangerous-responses, the environment variable UNSAFE_ALLOW_DANGEROUS_RESPONSES=true, and the functional option httpbin.WithUnsafeAllowDangerousResponses() are disabled in any deployment reachable by web browsers.
If an immediate software upgrade is not feasible, administrators can mitigate the vulnerability at the reverse proxy or Web Application Firewall (WAF) layer. For NGINX deployments fronting go-httpbin, configure the proxy to strip client-controlled Content-Type headers on the vulnerable routes and enforce X-Content-Type-Options: nosniff:
location /response-headers {
proxy_pass http://go-httpbin-backend;
proxy_hide_header Content-Type;
add_header Content-Type "application/json; charset=utf-8" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ ^/base64/ {
proxy_pass http://go-httpbin-backend;
proxy_hide_header Content-Type;
add_header Content-Type "text/plain; charset=utf-8" always;
add_header X-Content-Type-Options "nosniff" always;
}Additionally, deploys of go-httpbin should always be isolated on a dedicated, sandboxed domain (for example, httpbin-sandbox.example.net) rather than hosted on a subpath or subdomain of a primary production domain, preventing any potential cross-origin credential or cookie exposure.
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 |
|---|---|
| Vulnerability Type | Reflected Cross-Site Scripting (CWE-80 / CWE-79) |
| Attack Vector | Network (Crafted URL via HTTP GET) |
| CVSS v3.1 Score | 6.1 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) |
| EPSS Score | 0.27% (18.89th Percentile) |
| Affected Endpoints | /response-headers, /base64/{payload}, /base64/decode/{payload} |
| Exploit Status | Public Proof-of-Concept (PoC) |
| CISA KEV Status | Not Listed |
| Patched Version | go-httpbin v2.18.0 |
The application receives input from an upstream component, but does not neutralize or incorrectly neutralizes script-related HTML tags such as <script>, <img>, or <iframe> before reflecting them back into web content presented to a user.
A credential exposure vulnerability exists in the @sigstore/oci library prior to version 0.7.2. The RegistryClient implementation stores sensitive authentication headers, including Bearer tokens, Basic authentication credentials, and custom Docker configuration headers, as client-wide defaults on its shared HTTP client instance. When uploading binary blobs via the OCI distribution specification, the client extracts the target upload URI from the server-supplied Location header and dispatches an independent HTTP PUT request. If a compromised or untrusted registry returns a cross-origin URI in the Location header, the client transmits the stored authentication credentials directly to the third-party host.
An OS command injection and argument injection vulnerability exists in the Nx build system and its official Maven (@nx/maven) and Gradle (@nx/gradle) plugins. When constructing the project dependency graph, the plugins invoke underlying Java build wrappers via subshells while interpolating unsanitized plugin options directly from nx.json into the command arguments. An attacker who supplies a crafted nx.json file via a repository or pull request can execute arbitrary shell commands with the privileges of the user or CI/CD runner invoking any Nx command that computes the project graph.
AdminNeo versions 4.6.2 through 5.4.1 contain an improper input validation vulnerability (CWE-20) in the version verification endpoint (?script=version). An unauthenticated remote attacker can submit a crafted HTTP POST request containing an array parameter for 'version', which is serialized into a shared server-side temporary cache file without type checking or authentication. On subsequent requests, the application deserializes this cached array and passes it to cookie() and rawurlencode(), triggering an uncaught fatal TypeError in PHP 8.x environments and causing a persistent 24-hour Denial of Service (DoS) for all users.
Charmbracelet Soft Serve versions 0.5.0 through 0.12.2 contain an authorization bypass and observable response discrepancy vulnerability in the HTTP Git server. Remote attackers can determine the existence, exact names, and clone URLs of private repositories by issuing crafted Go remote import path discovery requests (?go-get=1) or by observing differential HTTP status codes across Git Smart HTTP, Dumb HTTP, and Git LFS endpoints.
A Time-of-Check to Time-of-Use (TOCTOU) race condition in Charmbracelet Soft Serve versions 0.6.0 through 0.12.2 allows unauthenticated users to retain read access to repositories after their visibility is changed from public to private. The flaw stems from premature cache invalidation in the SetPrivate method prior to committing the visibility update to the underlying database. Concurrent read requests arriving during the database transaction window repopulate the non-expiring in-memory LRU cache with stale public metadata, bypassing access controls across HTTP, SSH, and Git protocols until the server process is restarted.
A resource consumption vulnerability exists in the Authlib joserfc Python library prior to version 1.7.5. When processing JSON Web Encryption (JWE) objects formatted using General JSON Serialization, the library fails to enforce an upper bound on the number of elements within the recipients array. An unauthenticated attacker can supply a JWE containing thousands of recipient entries targeting an RSA key management algorithm, forcing the server to perform an equivalent number of computationally expensive RSA private-key decryption operations per request and exhausting available CPU resources.