HIGH crlf injectionsinatrabasic auth

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.

Frequently Asked Questions

Can Crlf Injection occur even if the application does not directly echo the username in headers?
Yes. If usernames or passwords are logged verbatim or passed to external systems without sanitization, injected CRLF sequences can manipulate log entries or secondary outputs, leading to log injection or secondary request splitting in downstream services.
Is using HTTPS sufficient to prevent Crlf Injection in Basic Auth flows?
No. HTTPS protects confidentiality and integrity in transit, but it does not prevent an attacker from injecting CRLF sequences into headers or logs on the server side. Input validation and header sanitization remain necessary regardless of transport security.