September 1, 2020·4 min read·engineeringgosecuritycgihttp

Go 1.15.1 stops labeling unspecified CGI responses as HTML

CGI and FastCGI response writers now detect an omitted Content-Type from the first body write. The repair removes a dangerous HTML default without making detection an application security policy.

The same HTTP handler can acquire different security behavior depending on the response writer behind it. Before today's update, Go's CGI and FastCGI implementations supplied an HTML content type when a handler omitted one. Ordinary data could consequently reach a browser labeled for interpretation as HTML. If an attacker controlled part of that response, the default could create a cross-site scripting vulnerability.

The Go 1.15.1 release entry identifies fixes to net/http/cgi and net/http/fcgi. The repair replaces their fixed default with content detection. It also changes when the implementation finalizes headers, because it needs the first body bytes before it can choose the omitted type. That sequencing change is the mechanism behind the short release description.

I prefer Go interfaces when they let the same handler retain understandable behavior across implementations. Here, the shared http.ResponseWriter interface concealed a consequential difference in defaults. The fix reduces that difference, while leaving explicit content policy with the application.

The missing header became an HTML decision

A handler that writes a body without setting Content-Type leaves a decision to its response writer. The old CGI path selected text/html; charset=utf-8. FastCGI also defaulted to HTML. That choice did more than describe the encoding of some text. It told the recipient to interpret the representation as a document format that can contain active content.

The security announcement assigns the problem CVE-2020-24553. Its important exposure condition is attacker influence over response contents when the handler has not explicitly selected a type. The disclosure credits RedTeam Pentesting GmbH and announces the corresponding fixes in Go 1.15.1 and Go 1.14.8.

The replacement uses http.DetectContentType on the first Write. That aligns the behavior with the ordinary HTTP package. However, the announcement also says applications should explicitly set a type for attacker-controlled files. Detection can recognize content that the application did not intend to serve as active content. Correcting the library default does not establish what formats a particular endpoint should allow.

A plain-text endpoint can make its policy explicit before writing its body.

func showText(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    io.WriteString(w, "This response is plain text.\n")
}

This fragment uses net/http and io. Its header states the endpoint's intended representation independently of detection. An endpoint that intentionally serves HTML needs a different representation and appropriate construction of that HTML. The runtime cannot infer that application intent from the omission of a header.

Finalization must wait for body bytes

The security patch introduces separate state for choosing a status and writing the CGI header block. Previously, WriteHeader sent the header block and supplied the fixed default. The updated implementation records the status code and marks it chosen. A later helper emits the CGI header block after it can inspect the first body chunk.

This distinction is necessary even when the application explicitly calls WriteHeader. Choosing a status does not supply body bytes for detection. The implementation therefore stores the code separately from the flag that records whether it has actually emitted the CGI headers.

When Write runs, it selects status OK if no status has yet been selected. If the CGI header block remains unwritten, it calls the new helper with the body chunk. The helper writes the status and headers, choosing a detected content type only when the header map lacks an explicit entry. It then permits the body write to proceed.

The helper itself does not write the supplied body chunk. That argument exists so it can detect the representation. Keeping detection separate from body output avoids accidentally emitting the first bytes twice. The surrounding Write method remains responsible for returning the result of writing those bytes.

A separate flag prevents the helper from writing the header block repeatedly. Without that state, subsequent body writes could introduce another status and header block into the output. The patch thus connects content detection with a clear one-time finalization point.

FastCGI has an additional empty-response path

The FastCGI repair follows the same separation between stored status and emitted headers. It preserves special treatment for a not-modified response, removing body-related headers and avoiding automatic content-type detection for that status. A general rule for omitted content types must still respect a response that is not meant to carry a body.

The patch also calls Write(nil) after the handler returns. That makes the response produce headers even when the handler did not write any body bytes. A detection design based only on the first body write otherwise needs a defined outcome for the case where no such write occurs.

Tests in the patch exercise empty content, HTML, ordinary text, and JPEG data. The expected types demonstrate that detection now distinguishes representations where the old unconditional HTML choice could not. The CGI changes include an integration test that runs a child process, so the test checks the actual CGI output path as well as the response object directly.

The tests also check that the body remains intact and that the reported write length matches the supplied body length. Those properties matter because delaying header finalization changes the order of internal operations. A security correction should not silently duplicate or drop the bytes whose type it detects.

The empty-response case is particularly useful because it tests the control flow without relying on meaningful content. Header emission must terminate correctly even when there is nothing to classify beyond an empty byte slice. That requirement belongs to the protocol adapter, not to each handler.

The application still owns representation policy

I regard the most useful lesson as the distinction between a fallback and a policy. Go 1.15.1 supplies a more appropriate fallback for these response writers. An application serving user-controlled files still needs to decide which representations it intends browsers to interpret.

The concrete repair occurs before the first body bytes are emitted, when the CGI implementation can finally inspect them. Separating status selection from header output makes that inspection possible. Explicit headers continue to state application intent, while omitted headers now receive content-based handling instead of the old unconditional HTML label.