Crlf Injection in Sinatra with Basic Auth
Crlf Injection in Sinatra with Basic Auth — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when user-controlled data is reflected into HTTP headers without sanitization, allowing an attacker to inject newline characters (CRLF = \r\n) and append or overwrite headers. In Sinatra, this risk is heightened when Basic Auth is used because the Authorization header is parsed and often echoed or logged in a way that can be influenced by an attacker-controlled value, such as a username or password parameter that is improperly validated.
Consider a Sinatra endpoint that accepts credentials via Basic Auth and then returns a custom header based on the provided username:
require 'sinatra'
require 'base64'
get '/profile' do
auth = request.env['HTTP_AUTHORIZATION']
if auth && auth.start_with?('Basic ')
decoded = Base64.decode64(auth.split(' ').last)
username, password = decoded.split(':', 2)
# Vulnerable: username directly used in a custom header
response['X-User'] = username
"Welcome, #{username}"
else
response.status = 401
'Unauthorized'
end
end
If an attacker sends a request with a crafted Basic Auth credential such as user%0d%0aX-Injected: true (URL-encoded CRLF), the decoded username becomes user\r\nX-Injected: true. When this value is assigned to response['X-User'], Sinatra reflects the raw newline characters into the HTTP response headers. This can lead to response splitting, header injection, or cache poisoning, depending on how downstream proxies or clients handle the malformed response.
The combination of Basic Auth and header reflection is particularly dangerous because the authentication flow itself exposes the username and password in every request. An attacker can exploit this by chaining authentication with injected headers to manipulate logging, bypass header-based security policies, or inject additional response bodies. For example, an attacker could inject a Set-Cookie header to hijack sessions:
# Attacker's crafted Authorization header value:
# d3Jvbmc6\r\nSet-Cookie: session=attacker; Path=/
# Results in duplicated Set-Cookie header after parsing
response['X-User'] = "guest\r\nSet-Cookie: session=attacker; Path=/"
Even when Sinatra applications do not directly echo the username, logging frameworks or error handlers might include the raw Authorization header value in logs or responses, creating an indirect injection path. This makes input validation and header sanitization essential when Basic Auth is in use.
Basic Auth-Specific Remediation in Sinatra — concrete code fixes
To mitigate Crlf Injection in Sinatra with Basic Auth, you must treat any data derived from the authentication flow as untrusted. This includes the username, password, and any parsed components that might later be reflected into headers or logs.
1. Validate and sanitize usernames and passwords before any use. Strip or encode CRLF characters explicitly. Avoid using raw user input in headers:
def sanitize_header_value(value)
value.to_s.gsub(/[\r\n]/, '')
end
get '/profile' do
auth = request.env['HTTP_AUTHORIZATION']
if auth && auth.start_with?('Basic ')
decoded = Base64.decode64(auth.split(' ').last)
username, password = decoded.split(':', 2)
# Safe: sanitize before using in headers
safe_username = sanitize_header_value(username)
response['X-User'] = safe_username
"Welcome, #{safe_username}"
else
response.status = 401
'Unauthorized'
end
2. Use structured authentication data instead of raw header reflection. Store the username in the session or a signed token rather than echoing it into headers:
enable :sessions
get '/login' do
auth = request.env['HTTP_AUTHORIZATION']
if auth && auth.start_with?('Basic ')
decoded = Base64.decode64(auth.split(' ').last)
username, _password = decoded.split(':', 2)
session[:username] = sanitize_header_value(username)
redirect '/profile'
else
response.status = 401
'Unauthorized'
end
end
get '/profile' do
if session[:username]
"Welcome, #{session[:username]}"
else
response.status = 401
'Unauthorized'
end
end
3. Reject credentials containing newline characters at the protocol level. Enforce a strict username policy that excludes control characters:
USERNAME_REGEXP = /^[^\r\n]+$/
def valid_credentials?(username, password)
username.match?(USERNAME_REGEXP) && !password.nil?
end
get '/login' do
auth = request.env['HTTP_AUTHORIZATION']
if auth && auth.start_with?('Basic ')
decoded = Base64.decode64(auth.split(' ').last)
username, password = decoded.split(':', 2)
if valid_credentials?(username, password)
session[:username] = username
redirect '/profile'
else
response.status = 400
'Bad Request'
end
else
response.status = 401
'Unauthorized'
end
end
These patterns ensure that CRLF characters are never trusted, reflected, or logged in a way that can alter the HTTP message structure. Even when using middleware or logging tools, sanitize any value derived from the Basic Auth header before it reaches external systems.