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.


References

Frequently Asked Questions

What is CRLF Injection in HTTP headers?

CRLF (Carriage Return + Line Feed) injection occurs when an attacker includes `\r\n` sequences in data that becomes part of HTTP headers. Since headers are separated by CRLF, injecting these characters allows attackers to insert entirely new headers or even a blank line to inject response body content.

How do you prevent CRLF Injection in Go?

Validate and sanitize all user-controlled data before using it in HTTP headers. Remove or reject CRLF characters using functions like `strings.NewReplacer()` or regex validation. Better yet, use framework APIs that handle encoding automatically rather than manual string concatenation.

What CWE is CRLF Injection?

CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting'). Related CWEs include CWE-74 (Improper Neutralization of Special Elements in Output) and CWE-22 (Path Traversal).

Is URL encoding enough to prevent CRLF Injection in headers?

No. While URL encoding helps for query parameters, HTTP headers use a different encoding context. CRLF characters must be explicitly removed or rejected in header values. URL encoding won't prevent `%0D%0A` from being decoded back to `\r\n` in some contexts.

Can static analysis detect CRLF Injection in headers?

Yes. Static analysis tools can detect when user-controlled variables are concatenated into HTTP header values without sanitization. The Orbis AppSec scanner identified this exact pattern by tracking the flow from `re.Name` (untrusted source) to the `Content-Disposition` header (sink).

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #432

Related Articles

high

How Unicode Normalization Infinite Loops Happen in Go and How to Fix CVE-2026-56852

CVE-2026-56852 is a high-severity vulnerability in golang.org/x/text that allows the Unicode normalization iterator to enter an infinite loop when processing specially crafted input. This fix upgrades the dependency from v0.37.0 to v0.39.0, tightening input validation and preventing denial-of-service attacks in applications that process untrusted Unicode text.

critical

How command injection happens in Go ffmpeg-go and how to fix it

A critical command injection vulnerability (CVE-2026-41179, CWE-78) was discovered in `drivers/local/util.go` of a Go media processing service, where user-controlled file paths were passed to `ffmpeg.Input()` without filtering shell metacharacters. Although a `sanitizeFilePath()` function existed to validate paths, it failed to reject characters like `;`, `|`, and backticks that could be weaponized if the underlying ffmpeg-go library constructs shell commands internally. The fix adds a targeted

high

How improper handling of case sensitivity happens in Go MCP SDK and how to fix it

A high-severity vulnerability (CVE-2026-27896) in the Model Context Protocol Go SDK v1.3.0 allowed attackers to bypass security controls through improper handling of case sensitivity. The fix upgrades the dependency from v1.3.0 to v1.3.1, which correctly normalizes case comparisons. This vulnerability was particularly concerning for CLI tools where attackers could manipulate input to evade validation logic.

high

How Denial of Service in SSH Key Exchange happens in Go golang.org/x/crypto and how to fix it

A high-severity denial of service vulnerability (CVE-2025-22869) was discovered in the SSH key exchange implementation of Go's `golang.org/x/crypto` library. The `cpdaemon` service depended on the vulnerable version v0.32.0, which could allow an attacker to exhaust server resources during the SSH handshake phase. The fix upgrades the dependency to v0.35.0, which includes the upstream patch for this vulnerability.

critical

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.