Container Escape in Sinatra with Bearer Tokens
Container Escape in Sinatra with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A container escape in a Sinatra application that uses Bearer token authentication occurs when an attacker who compromises a token or the API surface is able to break out of the container’s runtime constraints and interact with the host or other containers. This specific combination is risky because Sinatra is lightweight and often deployed in containers without additional process isolation, and Bearer tokens are frequently handled in ways that leak privileges or trust boundaries.
One common pattern is defining routes that accept a token via an Authorization header and then performing host-level operations based on token claims. For example, consider a Sinatra route that reads a token, decodes it, and uses claims to decide whether to execute a system command or access host files:
require 'sinatra'
require 'json'
require 'base64'
helpers do
def current_user
auth = request.env['HTTP_AUTHORIZATION']
return nil unless auth&&auth.start_with?('Bearer ')
token = auth.split(' ').last
begin
payload = JSON.parse(Base64.decode64(token.split('.')[1] + '=='))
rescue
nil
end
end
end
get '/admin/run' do
halt 401, 'Missing token' unless current_user
# Dangerous: using token claims to decide host access
if current_user['admin'] == true
cmd = params['cmd']
result = `#{cmd}`
{ result: result }.to_json
else
halt 403, 'Insufficient scope'
end
end
In a container, this route can enable an authenticated user with a valid Bearer token to execute arbitrary commands on the host if the container is misconfigured with elevated privileges or shared namespaces (e.g., using the host network or mounting sensitive paths like /proc or /var/run/docker.sock). An attacker who steals a Bearer token with admin claims can invoke this endpoint to run host commands, leading to container escape.
Another vector involves insecure token validation and overly permissive container capabilities. If the Sinatra app validates Bearer tokens using a weak or public algorithm (e.g., none algorithm or a hardcoded secret), an attacker can forge tokens and gain unauthorized access to admin routes. When the container runs with capabilities like CAP_SYS_ADMIN or has access to the Docker socket, a forged token can be used to issue privileged API calls from within the app, effectively escaping the container’s intended boundaries.
Additionally, logging or error handling that exposes tokens or internal paths can aid an attacker in refining an escape attempt. For instance, returning stack traces that include file paths from the host filesystem can reveal mounted volumes or entrypoints. Combined with Bearer tokens that lack proper scope isolation, this can expose endpoints that should be container-internal, enabling lateral movement or host interaction.
To summarize, the risk arises when:
- Bearer tokens are accepted with broad claims and used to gate host-level operations.
- The container runs with elevated Linux capabilities or mounts sensitive host paths.
- Token validation is weak or error handling leaks internal details.
Bearer Tokens-Specific Remediation in Sinatra — concrete code fixes
Remediation focuses on strict token validation, avoiding host interaction based on token claims, and hardening the container runtime. Below are concrete Sinatra examples that address the issues described above.
1. Validate tokens with a secure library and avoid host commands
Replace command execution with safe, internal logic and use a robust JWT library to verify tokens properly:
require 'sinatra'
require 'json'
require 'jwt' # Ensure 'gem install jwt' is present
# Use a strong secret or RSA public key; avoid hardcoded secrets in source
SECRET_KEY = ENV['JWT_SECRET'] || 'fallback_for_dev_only'
helpers do
def current_user
auth = request.env['HTTP_AUTHORIZATION']
return nil unless auth&&auth.start_with?('Bearer ')
token = auth.split(' ').last
begin
decoded = JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' })
# Store claims for downstream use; keep it minimal
@current_user = decoded.first
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end
end
get '/admin/data' do
halt 401, 'Missing or invalid token' unless current_user
# Do not execute commands or access host files based on token claims
{ users: ['alice', 'bob'], scope: current_user['scope'] }.to_json
end
2. Enforce least privilege and avoid dangerous capabilities
Ensure the container does not run as root and does not mount sensitive paths. In your Dockerfile and runtime, use a non-root user and drop capabilities:
# Dockerfile example FROM ruby:3.2-slim RUN useradd -m appuser WORKDIR /app COPY Gemfile Gemfile.lock ./ RUN bundle install COPY . . USER appuser CMD ["ruby", "app.rb"]
At runtime, avoid --privileged and do not mount /var/run/docker.sock. If the app must call external services, use a dedicated service account with minimal scopes instead of relying on token claims for host operations.
3. Use token binding and short lifetimes
Configure your authentication provider to issue short-lived Bearer tokens and bind them to the client IP or TLS channel where possible. In Sinatra, you can add additional checks to reject tokens used from unexpected contexts:
helpers do
def verify_token_context(user_claims)
# Example: compare token's 'client_ip' claim with request.ip
request_ip = request.ip.gsub(/::1|127\.0\.0\.1/, '127.0.0.1')
user_claims['client_ip'] == request_ip
end
end
get '/secure/action' do
halt 401, 'Invalid token context' unless current_user && verify_token_context(current_user)
{ status: 'ok' }.to_json
end
4. Harden error handling and logging
Do not return stack traces or internal paths in responses. Use generic error messages and ensure logs do not leak tokens:
configure do
set :logging, true
configure :production do
logger = ::Logger.new(STDOUT)
logger.formatter = proc do |severity, datetime, prog, msg|
# Redact potential token-like strings from logs
msg = msg.to_s.gsub(/[A-Za-z0-9_-]{20,}/, '[REDACTED]')
"#{datetime} #{severity}: #{msg}\n"
end
set :logger, logger
end
end