Back to Blog
critical SEVERITY8 min read

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

O
By Orbis AppSec
Published August 13, 2026Reviewed August 13, 2026

Answer Summary

This is a CRLF Injection vulnerability (CWE-113) in Go's Gin framework where unsanitized user-supplied filenames were directly embedded into HTTP response headers. The vulnerability exists in the `ViewC()`, `ViewCCode()`, and `HeadC()` functions in `server/views/resources.go` where the `re.Name` variable (original filename from uploads) is concatenated directly into Content-Disposition headers without sanitization. The fix uses `strings.NewReplacer()` to strip carriage return (`\r`) and newline (`\n`) characters from filenames before header insertion, preventing attackers from injecting arbitrary HTTP headers or splitting the response.

Vulnerability at a Glance

cweCWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers)
fixStrip CRLF characters from filenames using strings.NewReplacer() before header insertion
riskAttackers can inject arbitrary HTTP headers, enabling cache poisoning, session fixation, XSS, or response splitting attacks
languageGo
root causeUser-controlled filename directly concatenated into HTTP headers without sanitization
vulnerabilityCRLF Injection in HTTP Headers (Header Injection)

How HTTP Header Injection Happens in Go and How to Fix It

Introduction

In the file upload handler of a production Go application, we discovered a critical CRLF Injection vulnerability in server/views/resources.go. The vulnerability existed in three HTTP handler functions—ViewC(), ViewCCode(), and HeadC()—where the original filename from user uploads was directly concatenated into HTTP response headers without any sanitization.

The problematic pattern was simple but dangerous:

c.Header("Content-Disposition", "attachment; filename=\""+re.Name+"\"")

Here, re.Name contains the original filename provided by the user during file upload. Because HTTP headers are delimited by CRLF sequences (\r\n), an attacker could craft a filename containing these characters to inject entirely new headers or even manipulate the response body. This is a textbook example of CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers.

This vulnerability matters because file upload handlers are often publicly accessible endpoints, making this remotely exploitable by any unauthenticated attacker. The threat model is straightforward: an attacker uploads a file with a malicious filename and triggers a response that includes that filename in headers.


The Vulnerability Explained

How CRLF Injection Works in HTTP Headers

HTTP headers are structured as key-value pairs separated by CRLF sequences. A typical response looks like:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Disposition: attachment; filename="document.pdf"

[response body]

The blank line (two consecutive CRLFs) separates headers from the body. If an attacker can inject a CRLF sequence into a header value, they can:

  1. Inject new headers by adding \r\n followed by a new header name and value
  2. Inject response body content by adding \r\n\r\n to terminate headers and start the body
  3. Cache poison by injecting headers that affect how proxies or browsers cache the response
  4. Enable XSS by injecting Content-Type: text/html headers to serve attacker-controlled content as HTML

The Vulnerable Code

Looking at line 119 in the original server/views/resources.go:

func ViewC(c *gin.Context) {
    // ... earlier code ...
    var iv [aes.BlockSize]byte
    stream := cipher.NewCTR(block, iv[:])
    reader := &cipher.StreamReader{S: stream, R: f}
    if conf.C.AlwaysDownload {
        c.Header("Content-Type", "application/octet-stream")
        c.Header("Content-Disposition", "attachment; filename=\""+re.Name+"\"")  // VULNERABLE
    } else {
        c.Header("Content-Disposition", "filename=\""+re.Name+"\"")  // VULNERABLE
    }
    // ... rest of function ...
}

The same pattern appears in ViewCCode() at line 181 and HeadC() at line 245.

The vulnerability is clear: re.Name is user-controlled (it comes from the original filename of an uploaded file), and it's directly concatenated into the header value without any validation or sanitization.

Exploitation Scenario

An attacker could upload a file with this crafted filename:

document.pdf\r\nX-Injected-Header: malicious-value

Or even more dangerously:

document.pdf\r\nContent-Type: text/html\r\n\r\n<script>alert('XSS')</script>

When the application responds with this filename in the Content-Disposition header, the HTTP response would look like:

HTTP/1.1 200 OK
Content-Disposition: attachment; filename="document.pdf
X-Injected-Header: malicious-value"
Content-Type: application/octet-stream

[file content]

The injected header becomes part of the response, potentially:
- Manipulating how the browser handles the response
- Poisoning intermediate caches
- Triggering unintended behavior in security filters
- In severe cases, enabling XSS if Content-Type can be overridden

The real-world impact depends on what an attacker injects, but the potential for abuse is significant because file downloads are often trusted by users and security infrastructure.


The Fix

What Changed

The fix is surgical and focused: sanitize the filename by removing CRLF characters before using it in HTTP headers.

Changes in ViewC() (lines 119-125):

+ safeName := strings.NewReplacer("\r", "", "\n", "").Replace(re.Name)
  if conf.C.AlwaysDownload {
      c.Header("Content-Type", "application/octet-stream")
-     c.Header("Content-Disposition", "attachment; filename=\""+re.Name+"\"")
+     c.Header("Content-Disposition", "attachment; filename=\""+safeName+"\"")
  } else {
-     c.Header("Content-Disposition", "filename=\""+re.Name+"\"")
+     c.Header("Content-Disposition", "filename=\""+safeName+"\"")
  }

Changes in ViewCCode() (lines 181-182):

+ safeName := strings.NewReplacer("\r", "", "\n", "").Replace(re.Name)
- c.Header("Content-Disposition", "filename=\""+re.Name+"\"")
+ c.Header("Content-Disposition", "filename=\""+safeName+"\"")

Changes in HeadC() (lines 245-251):

+ safeName := strings.NewReplacer("\r", "", "\n", "").Replace(re.Name)
  if conf.C.AlwaysDownload {
      c.Header("Content-Type", "application/octet-stream")
-     c.Header("Content-Disposition", "attachment; filename=\""+re.Name+"\"")
+     c.Header("Content-Disposition", "attachment; filename=\""+safeName+"\"")
  } else {
-     c.Header("Content-Disposition", "filename=\""+re.Name+"\"")
+     c.Header("Content-Disposition", "filename=\""+safeName+"\"")
  }

How the Fix Works

The fix uses Go's strings.NewReplacer() function to create a replacer that removes both carriage return (\r, ASCII 13) and newline (\n, ASCII 10) characters:

safeName := strings.NewReplacer("\r", "", "\n", "").Replace(re.Name)

This approach:
1. Prevents header injection by removing the exact characters that would split HTTP headers
2. Preserves filename usability by keeping all other characters intact (spaces, dashes, dots, etc.)
3. Maintains backward compatibility because legitimate filenames rarely contain CRLF sequences
4. Is applied consistently across all three vulnerable functions

The sanitized safeName is then used in all Content-Disposition header assignments, eliminating the attack surface.

Why This Specific Approach

The fix uses character removal rather than rejection because:
- Removal is more user-friendly: If a user accidentally uploads a file with a CRLF in the name, it's silently cleaned rather than rejected
- It's specific to the threat: Only CRLF characters are removed; all other special characters in filenames are preserved
- It's performant: strings.NewReplacer() is optimized for this exact use case
- It's Go-idiomatic: This is the standard pattern in Go for simple character replacement

An alternative approach would be to use regex validation to reject filenames containing CRLF, but removal is less disruptive and equally secure.


Prevention & Best Practices

1. Never Concatenate User Input into HTTP Headers

Always treat HTTP headers as a security boundary. Even if you think you're only using the data in a "safe" way, concatenation is error-prone:

// BAD: Direct concatenation
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")

// GOOD: Sanitize first
safeName := strings.NewReplacer("\r", "", "\n", "").Replace(filename)
c.Header("Content-Disposition", "attachment; filename=\""+safeName+"\"")

// EVEN BETTER: Use framework helpers if available
// Some frameworks provide header-safe functions

2. Validate All User-Controlled Data at Entry Points

For file uploads, implement validation at the earliest point:

func validateFilename(filename string) error {
    // Reject files with CRLF
    if strings.Contains(filename, "\r") || strings.Contains(filename, "\n") {
        return fmt.Errorf("filename contains invalid characters")
    }

    // Additional validations
    if len(filename) > 255 {
        return fmt.Errorf("filename too long")
    }

    if strings.Contains(filename, "..") {
        return fmt.Errorf("filename contains path traversal")
    }

    return nil
}

3. Use Allowlists for Filenames

Consider restricting filenames to a safe character set:

// Only allow alphanumeric, dots, dashes, and underscores
validChars := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
if !validChars.MatchString(filename) {
    return fmt.Errorf("filename contains invalid characters")
}

4. Implement Content-Disposition Properly

RFC 6266 specifies how to handle non-ASCII characters in Content-Disposition:

// For ASCII-only filenames (after sanitization)
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, safeName))

// For filenames with Unicode characters, use RFC 5987 encoding
// This is more complex but handles international filenames safely

5. Use Static Analysis to Catch These Issues

Enable linters and static analysis tools that can detect:
- User input flowing into HTTP headers
- String concatenation in security-sensitive contexts
- Missing input validation

Tools like:
- Semgrep: Can write rules to detect user input in headers
- gosec: Go security checker that catches some header injection patterns
- SonarQube: Comprehensive static analysis for Go
- Orbis AppSec: Automated security scanning (see below)

6. Apply Defense in Depth

Even with sanitization, consider:
- Content Security Policy headers to limit what injected headers can do
- X-Content-Type-Options: nosniff to prevent MIME type confusion
- Strict-Transport-Security to prevent downgrade attacks
- Input validation at multiple layers


Key Takeaways

  • CRLF characters in HTTP headers enable header injection attacks: The \r\n sequence is the HTTP delimiter, so including it in header values allows attackers to inject new headers or response body content.

  • The ViewC(), ViewCCode(), and HeadC() functions all had the same vulnerability: User-supplied filenames (re.Name) were concatenated directly into Content-Disposition headers without sanitization, making all three functions exploitable.

  • Sanitization is simpler than validation for this use case: Using strings.NewReplacer("\r", "", "\n", "").Replace() removes the dangerous characters while preserving legitimate filenames, avoiding user frustration from rejected uploads.

  • File upload handlers are high-value attack targets: Because they're often publicly accessible and handle user-supplied data, they deserve extra scrutiny during code review and security testing.

  • This vulnerability is detectable by automated security scanning: The pattern of user input flowing to HTTP headers without sanitization is a clear signal that static analysis tools can identify before code reaches production.


How Orbis AppSec Detected This

Source: The original filename from user file uploads, stored in the re.Name variable from the file resource object retrieved from the database.

Sink: The c.Header("Content-Disposition", ...) calls in the Gin framework's Header() method at lines 119, 125, 181, and 245 of server/views/resources.go.

Missing control: No sanitization or validation of the re.Name variable before it's concatenated into the header value string. The filename is treated as a trusted value despite originating from user input during file upload.

CWE: CWE-113 - Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')

Fix: Added strings.NewReplacer("\r", "", "\n", "").Replace(re.Name) to strip carriage return and newline characters from the filename before inserting it into Content-Disposition headers, preventing CRLF injection.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

HTTP header injection through CRLF sequences is a subtle but serious vulnerability that can lead to cache poisoning, response splitting, and even XSS attacks. The fix in this case was straightforward—sanitize filenames before using them in headers—but it demonstrates a critical security principle: never trust user-supplied data in security-sensitive contexts like HTTP headers.

The three functions fixed in server/views/resources.go now properly sanitize filenames, eliminating the injection surface. This is a good reminder that security vulnerabilities often hide in plain sight, in code that seems simple and harmless. A file download handler looks innocuous, but concatenating user input into headers is a classic mistake.

For developers working with file uploads, HTTP headers, or any user-controlled data that becomes part of infrastructure responses: validate early, sanitize appropriately for the context, and use static analysis tools to catch these patterns before they reach production.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #432

Related Articles

high

How JavaScript Injection via String Interpolation Happens in Go Wails Applications and How to Fix It

A high-severity JavaScript injection vulnerability in `internal/clusterconfigs/input.go` allowed arbitrary code execution through malicious kubeconfig filenames. The `saveClusterConfigFile` function at line 20 constructed JavaScript code by directly interpolating unsanitized filenames into `window.ExecJS()` calls, enabling attackers to break out of string literals and execute arbitrary JavaScript in the Webview context.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

high

How Infinite Loop Denial of Service happens in Go's golang.org/x/text and how to fix it

A high-severity denial-of-service flaw (CVE-2026-56852) in golang.org/x/text's Unicode normalization iterator (`norm.Iter`) could cause an infinite loop when processing specially crafted input. The `mcp` module's `go.mod`/`go.sum` pinned a vulnerable v0.14.0 release; upgrading to v0.39.0 closes the hole.

high

How containerd CRI plugin command injection happens in Go and how to fix it

A critical vulnerability in containerd v1.7.32 allowed attackers to execute arbitrary commands as root on the host by manipulating image configuration labels processed by the CRI plugin. Upgrading to containerd v1.7.33 eliminates this attack vector through improved input validation.

high

How Information Disclosure happens in Go dependency management and how to fix it

CVE-2026-42151 is a high-severity information disclosure vulnerability in the Prometheus monitoring library (github.com/prometheus/prometheus) that exposed Azure OAuth client secrets through the Prometheus configuration API endpoint. Applications depending on versions prior to v0.311.3 were at risk of leaking sensitive Azure credentials to anyone with access to the config API. The fix involves upgrading the dependency in go.mod from v0.310.0 to v0.311.3.

high

How Privilege Escalation via Incorrect User ID Handling Happens in Go and How to Fix It

A high-severity privilege escalation vulnerability (CVE-2026-46680) was discovered in containerd v1.7.31, where incorrect user ID handling could allow an attacker to escalate privileges within container environments. The fix upgrades the `github.com/containerd/containerd` dependency from v1.7.31 to v1.7.32, which corrects the UID handling logic and introduces additional transitive dependencies for secure path resolution.