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:
- Inject new headers by adding
\r\nfollowed by a new header name and value - Inject response body content by adding
\r\n\r\nto terminate headers and start the body - Cache poison by injecting headers that affect how proxies or browsers cache the response
- Enable XSS by injecting
Content-Type: text/htmlheaders 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\nsequence is the HTTP delimiter, so including it in header values allows attackers to inject new headers or response body content. -
The
ViewC(),ViewCCode(), andHeadC()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.
References
- CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers
- OWASP: HTTP Response Splitting
- OWASP: Injection Prevention Cheat Sheet
- Go strings package documentation
- Gin Web Framework - Setting Headers
- Semgrep rule for HTTP header injection
- fix: the application stores the original filename fr... in resources.go