Crlf Injection in Rails with Mutual Tls
Crlf Injection in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject a CRLF sequence (\r\n) into a header or the start of a body, causing the header to split and enabling injection of additional headers or response splitting. In Rails, this typically surfaces when user input is placed into HTTP response headers without strict validation or sanitization. Common patterns include passing unchecked parameters to redirect_to, location headers, or methods that ultimately set headers via response.headers.
Mutual Tls (mTLS) changes the trust boundaries but does not remove header injection logic. With mTLS, the server authenticates the client using a client certificate, which can lead developers to assume the request is inherently trusted. This may reduce scrutiny on input validation because the channel is encrypted and authenticated. However, Crlf Injection is a protocol-level issue about header formatting, not about transport-layer authentication. Even with mTLS, a validated client can supply malicious payloads if the application reflects or forwards client data into HTTP headers.
In a Rails app using mTLS, the request environment will contain client certificate details (e.g., in env["SSL_CLIENT_CERT"] or via request.ssl?). If the application uses these details in building responses—such as echoing a certificate serial into a custom header or using client-supplied values in redirects—an attacker can supply a crafted header containing \r\n to inject new headers like Set-Cookie or to split the response and perform HTTP response splitting or cache poisoning. Because mTLS is often used in internal or high-assurance environments, the impact can be more severe: attackers may be able to hijack sessions or poison intermediaries that rely on header integrity.
The vulnerability is not specific to Rails internals but to how Rails applications handle and forward data. For example, using redirect_to with a user-controlled host or path without validation can lead to header splitting if the input contains line breaks. Similarly, setting custom headers via response.headers['X-Custom'] = params[:value] without normalization allows injection. With mTLS, the added assurance of client authentication can mask the need for output encoding, making it easier for insecure code patterns to reach production.
Real-world test patterns for Crlf Injection in Rails include submitting \r\nSet-Cookie: injected=1 as a parameter that gets reflected in a header, or providing a redirect target like http://example.com\r\nX-Injected: true. When the response is inspected, injected headers or a split response indicate a successful injection. These tests remain valid regardless of mTLS, but the presence of mTLS may reduce logging or alerting on suspicious client behavior, delaying detection.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
Remediation centers on strict input validation, output encoding, and avoiding the direct use of client-controlled data in HTTP headers. Below are concrete, safe patterns for Rails applications that use mTLS.
1. Sanitizing inputs before using them in headers
Never pass raw user input to header-setting methods. Strip or reject CRLF characters explicitly. A helper method can be used across the app:
# app/lib/header_sanitizer.rb
class HeaderSanitizer
CRLF = /[\r\n]/
def self.sanitize(value)
return nil if value.blank?
value.gsub(CRLF, '')
end
end
# Usage in a controller
class ProfilesController < ApplicationController
def update
raw = params[:display_name]
safe = HeaderSanitizer.sanitize(raw)
response.headers['X-Display-Name'] = safe if safe
# ...
end
end
2. Safe redirects with validated hosts
When using redirect_to, restrict allowed hosts or use path-only redirects. Avoid direct use of user input in the redirect location:
# Safe: path-only redirect
redirect_to dashboard_path
# Safe: validated host
allowed_hosts = ['app.example.com', 'cdn.example.com']
url = params[:next]
if url.present? && allowed_hosts.include?(URI.parse(url).host)
redirect_to url
else
redirect_to root_path
end
3. mTLS-aware header handling with certificate metadata
If you use client certificate details, ensure they are sanitized before being reflected in headers:
class MtlsController < ApplicationController
before_action :verify_client_cert
def show
# Example: using certificate serial safely
if request.ssl? && (cert = request.client_cert)
serial = cert.serial.to_s
safe_serial = serial.gsub(/[\r\n]/, '')
response.headers['X-Client-Serial'] = safe_serial
end
# ... render logic
end
private
def verify_client_cert
unless request.ssl? && request.client_cert
head :unauthorized
end
end
end
4. Framework-level protections
Rails provides some built-in protections, but they should be augmented. For instance, Rails does not automatically reject headers containing newlines when setting via response.headers[]=. Always treat headers as an output channel that requires encoding. Additionally, consider using strong parameter filtering to remove or transform CRLF-bearing fields early in the request lifecycle.
5. Testing and verification
Verify fixes by sending requests with CRLF sequences in candidate fields and inspecting response headers for unexpected line splits. Tools like curl can be used with crafted payloads:
# Example curl with injected header attempt
curl -k --cert client.crt --key client.key \
-H 'Accept: text/html' \
'https://localhost:3000/profile?display_name=test%0D%0ASet-Cookie:%20foo=bar'
Check that the response does not contain the injected header and that the status code remains as expected.