Dns Rebinding in Grape
How Dns Rebinding Manifests in Grape
Dns Rebinding attacks exploit the trust relationship between a client and a server when both use the same domain name. In Grape applications, this manifests through several specific patterns that create dangerous trust assumptions.
The most common manifestation occurs in Grape APIs that serve both web applications and API endpoints from the same domain. When a malicious actor controls DNS records, they can switch IP addresses between requests, causing the client to believe it's communicating with the trusted API while actually connecting to an attacker-controlled server.
Consider a Grape API mounted at /api on api.example.com. A typical vulnerable pattern looks like this:
class API < Grape::API
prefix 'api'
format :json
# Vulnerable: trusts origin based on domain
before do
if request.headers['Origin'] == 'https://api.example.com'
@trusted_origin = true
end
end
get '/sensitive-data' do
if @trusted_origin
{ data: User.find(current_user.id).sensitive_info }
else
error!('Unauthorized', 403)
end
end
end
The DNS rebinding attack works by registering a domain that points to the attacker's server, then rapidly changing DNS records to point to the victim's internal API server. The browser's DNS cache expiration creates a window where the same domain resolves to different IPs.
Grape's middleware stack can also introduce vulnerabilities. The built-in Grape::Middleware::Globals sets various request attributes that developers might trust without validation:
# Vulnerable: trusting Grape's request context
before do
if env['api.endpoint'].routes.first.path == '/admin'
# Assumes this is the admin endpoint
perform_admin_action
end
end
Another Grape-specific pattern involves dynamic endpoint mounting based on configuration that can be manipulated through DNS rebinding. If your Grape API mounts different versions or modules based on subdomain resolution:
class DynamicAPI < Grape::API
mount APIv1 if resolve_to_internal?('api.example.com')
mount APIv2 if resolve_to_internal?('api.example.com')
# resolve_to_internal might be fooled by DNS rebinding
def resolve_to_internal?(domain)
# This check can be bypassed if DNS returns attacker's IP
IPSocket.getaddress(domain).start_with?('192.168.')
end
end
Rate limiting in Grape can also be compromised. If rate limiting is based on client IP but the DNS rebinding causes the same client to appear to come from different IPs:
class RateLimitedAPI < Grape::API
before do
# Vulnerable: IP-based rate limiting can be bypassed
@client_ip = request.ip
track_request(@client_ip)
end
end
Grape-Specific Detection
Detecting DNS rebinding vulnerabilities in Grape applications requires examining both the code structure and runtime behavior. The middleBrick scanner includes specific checks for Grape applications that look for these patterns.
First, examine your Grape API files for trust assumptions based on domain resolution. The middleBrick CLI can scan your Grape application directory:
npm install -g middlebrick
middlebrick scan ./app/api --type grape
The scanner specifically looks for:
- Domain-based trust checks without IP verification
- Dynamic endpoint mounting based on DNS resolution
- IP-based rate limiting without additional client fingerprinting
- Middleware that sets trusted context based on request origin
For runtime detection, middleBrick's black-box scanning tests for DNS rebinding vulnerabilities by attempting controlled DNS resolution changes during the scan. It checks if your Grape API responds differently to requests that appear to come from the same domain but resolve to different IPs.
Manual detection should focus on these Grape-specific patterns:
# Check for these vulnerable patterns in your Grape files
Dir.glob('app/api/**/*.rb').each do |file|
content = File.read(file)
# Pattern 1: Domain trust without IP validation
if content =~ /request.headers\[['"]Origin['"]\]
.*==.*['"].*example\.com['"]
# Pattern 2: Dynamic mounting based on DNS
if content =~ /mount.*resolve_to_internal.*
|IPSocket\.getaddress.*
|Resolv\.getaddress.*
# Pattern 3: IP-based rate limiting
if content =~ /request\.ip.*
|env\['HTTP_X_FORWARDED_FOR'
The middleBrick dashboard provides a security score specifically for API trust boundary violations, which includes DNS rebinding risks. It shows you exactly which endpoints are vulnerable and provides the specific code locations that need remediation.
For CI/CD integration, add middleBrick to your Grape API pipeline to catch these issues before deployment:
# .github/workflows/security.yml
name: API Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install -g middlebrick
- run: middlebrick scan ./app/api --type grape --fail-below B
env:
MIDDLEBRICK_API_KEY: ${{ secrets.MIDDLEBRICK_API_KEY }}
Grape-Specific Remediation
Remediating DNS rebinding vulnerabilities in Grape requires eliminating trust assumptions based on domain resolution and implementing proper origin verification. Here are specific fixes for Grape applications.
First, replace domain-based trust checks with cryptographic origin verification. Grape supports custom middleware where you can implement robust origin validation:
class OriginValidator
def initialize(app)
@app = app
end
def call(env)
request = Grape::Request.new(env)
origin = request.headers['Origin']
if trusted_origin?(origin, request.ip)
@app.call(env)
else
[403, { 'Content-Type' => 'application/json' }, [{ error: 'Forbidden' }.to_json]]
end
end
private
def trusted_origin?(origin, client_ip)
# Verify origin using a secure comparison
return false unless origin
# Check against known origins
trusted_origins = ['https://api.example.com']
return false unless trusted_origins.include?(origin)
# Additional IP validation to prevent DNS rebinding
trusted_ips = ['203.0.113.1'] # Production API server
return false unless trusted_ips.include?(client_ip)
true
end
end
# Mount the middleware in your Grape API
class API < Grape::API
use OriginValidator
# ... rest of your API
end
For rate limiting in Grape, implement client fingerprinting that combines multiple factors:
class RateLimitedAPI < Grape::API
before do
# Combine IP with other factors to prevent bypass
@client_fingerprint = generate_fingerprint(
request.ip,
request.headers['User-Agent'],
request.headers['Accept-Language']
)
enforce_rate_limit(@client_fingerprint)
end
private
def generate_fingerprint(ip, user_agent, language)
Digest::SHA256.hexdigest([ip, user_agent, language].join)
end
def enforce_rate_limit(fingerprint)
# Use Redis or similar for distributed rate limiting
current_count = $redis.get(fingerprint)&.to_i || 0
if current_count >= MAX_REQUESTS
error!('Rate limit exceeded', 429)
else
$redis.incr(fingerprint)
$redis.expire(fingerprint, RATE_LIMIT_WINDOW)
end
end
end
For dynamic endpoint mounting, validate DNS resolution with additional checks:
class SecureDynamicAPI < Grape::API
def initialize
super
validate_and_mount_endpoints
end
private
def validate_and_mount_endpoints
# Get canonical IP addresses
canonical_ips = get_canonical_ips('api.example.com')
# Only mount if resolution matches expected IPs
if canonical_ips.include?(current_request_ip)
mount APIv1
mount APIv2
else
# Log potential DNS rebinding attempt
logger.warn('Suspicious DNS resolution detected')
end
end
def get_canonical_ips(domain)
# Use multiple DNS queries to verify consistency
ips = []
3.times do
ips << IPSocket.getaddress(domain)
sleep 0.1 # Small delay between queries
end
# Return only IPs that are consistent across queries
ips.tally.select { |_, count| count > 1 }.keys
end
end
Finally, implement comprehensive logging in your Grape API to detect DNS rebinding attempts:
class API < Grape::API
before do
# Log requests that might indicate DNS rebinding
if suspicious_request?
logger.warn(
"Potential DNS rebinding attempt: " \
"Origin: #{request.headers['Origin']}, " \
"IP: #{request.ip}, " \
"User-Agent: #{request.headers['User-Agent']}"
)
end
end
private
def suspicious_request?
# Check for rapid IP changes from same origin
recent_requests = $redis.lrange("requests:#{request.headers['Origin']}", 0, -1)
if recent_requests.size > 5
recent_ips = recent_requests.map { |req| JSON.parse(req)['ip'] }
return true if recent_ips.uniq.size > 2
end
false
end
end