Crlf Injection in Sinatra (Ruby)
Crlf Injection in Sinatra with Ruby — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject a Carriage Return (CR, \r) and Line Feed (LF, \n) sequence into a header or status-line context. In Sinatra with Ruby, this often arises when user-controlled input is reflected in HTTP response headers without sanitization. Because Sinatra is a lightweight Ruby DSL built on Rack, developers sometimes treat headers as purely strings, inadvertently allowing injection of additional headers or response splitting.
For example, if a route uses headers['X-Redirect'] directly from params and passes it to redirect or sets it via header, an input like example.com\r\nSet-Cookie: session=evil can introduce a second header line. Rack expects headers as a hash, but if you construct raw header strings or use low-level Rack response manipulation, the injected CRLF can cause header injection, cache poisoning, or open the response to response-smuggling-like behaviors in chained proxies.
Ruby’s String handling preserves these characters, so unless you explicitly reject or encode \r and \n, the injected sequence may be interpreted by downstream HTTP components. In Sinatra, common triggers include:
- Using
redirect "http://#{params[:url]}"without validating thatparams[:url]contains no CRLF. - Setting custom headers via
header 'X-Value', params[:input]whereparams[:input]includes\r\n. - Streaming or constructing responses manually with Rack response arrays where newlines are not escaped.
Because Sinatra encourages concise route definitions, developers may overlook sanitization, especially when headers are built dynamically. The framework does not automatically neutralize CRLF in user input, so the burden is on the developer to treat header context as a strict boundary: once user data crosses into header values or status codes, it must be validated and encoded.
An attacker might probe such endpoints using inputs like \r\nSet-Cookie: auth=1 or \r\nLocation: https://evil.com to test for response splitting. In environments with multiple proxies, a successful injection could allow an attacker to inject arbitrary headers or split the response, potentially misleading downstream caches or clients. This maps to the broader OWASP API Top 10 category 'Security Misconfiguration' and can intersect with smuggling-related weaknesses if proxies interpret the injected sequences inconsistently.
Ruby-Specific Remediation in Sinatra — concrete code fixes
Defensive validation and safe abstractions are the primary mitigations. In Ruby, prefer using symbols or symbols for header keys and rely on Rack’s hash-based header handling rather than string concatenation. Below are concrete, Sinatra-specific patterns to eliminate CRLF risk.
1. Validate and sanitize header inputs
Never pass raw user input into header values. Use a denylist or allowlist to reject control characters. For example:
# Reject CRLF in any user-controlled header value
def safe_header(value)
return nil if value.to_s.include?("\r") || value.to_s.include?("\n")
value
end
get '/set-header' do
input = params['input']
safe = safe_header(input)
if safe
header 'X-Custom', safe
else
status 400
{ error: 'Invalid header value' }.to_json
end
end
2. Use symbols for header keys and avoid string interpolation in redirects
When redirecting, validate the target URL to ensure it does not contain CRLF. Prefer URI parsing to enforce a safe schema/host structure:
require 'uri'
get '/redirect' do
url = params['url']
begin
uri = URI.parse(url)
if uri.host.nil? || url.include?("\r") || url.include?("\n")
status 400
{ error: 'Invalid redirect URL' }.to_json
else
redirect uri.to_s
end
rescue URI::InvalidURIError
status 400
{ error: 'Invalid URL' }.to_json
end
end
3. Prefer hash-based header setting over raw strings
Sinatra’s headers helper accepts a hash, which Rack safely serializes. Avoid constructing header lines manually:
# Safe: hash-based headers
get '/profile' do
headers 'Content-Type' => 'application/json',
'X-Request-ID' => SecureRandom.uuid
{ user: 'alice' }.to_json
end
# Unsafe: string-based header construction (do not do this)
# get '/unsafe' do
# header_line = "X-Request-ID: #{params['id']}\r\nX-Extra: injected"
# headers header_line # Risk if params['id'] contains CRLF
# end
4. Leverage middleware or filters for global sanitization
Use a before filter to scrub headers across all routes:
before do
request.headers.each do |key, value|
if value.to_s.include?("\r") || value.to_s.include?("\n")
halt 400, { error: 'Invalid header value' }.to_json
end
end
end
5. Escape output when reflecting user input in responses
If you must include user input in a response body, ensure it does not contain literal CRLF that a downstream proxy might misinterpret. Use encoding or simple replacement:
get '/echo' do
content_type 'text/plain'
input = params['text'] || ''
input.gsub(/[\r\n]/, '') # Remove CR/LF to prevent smuggling in plain text
end
These patterns align with middleBrick’s checks for Input Validation and can complement scans that flag header-injection risks. By validating inputs at the boundary and using Ruby’s hash-based headers, you reduce the chance of response splitting without relying on ad-hoc string manipulation.