Ruby Rails Open Redirect Vulnerability (Brakeman Check)

Medium Risk Open Redirect
RubyRailsOpen RedirectBrakemanURL ValidationPhishing

What it is

Application uses redirect_to with user-controlled URLs in Rails, enabling open redirect attacks that can be exploited for phishing and malicious redirections.

class SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password])
    
    if user
      session[:user_id] = user.id
      
      # Vulnerable: User controls redirect destination
      redirect_url = params[:redirect_url] || root_path
      redirect_to redirect_url  # Open redirect vulnerability
    else
      flash[:error] = 'Invalid credentials'
      render :new
    end
  end
  
  def logout
    session.clear
    
    # Vulnerable: External redirect after logout
    return_url = params[:return_url]
    redirect_to return_url || login_path
  end
end

class ApplicationController < ActionController::Base
  def redirect_after_action
    # Vulnerable: Dynamic redirect without validation
    next_page = request.referer || params[:next]
    redirect_to next_page
  end
end
class SessionsController < ApplicationController
  ALLOWED_REDIRECT_HOSTS = [
    'app.example.com',
    'secure.example.com',
    'api.example.com'
  ].freeze
  
  def validate_redirect_url(url)
    return root_path if url.blank?
    
    begin
      uri = URI.parse(url)
      
      # Allow relative URLs
      if uri.relative?
        # Ensure it's a path, not a protocol-relative URL
        return url if url.start_with?('/')
        return "/#{url}" unless url.start_with?('//')
        raise ArgumentError, 'Protocol-relative URLs not allowed'
      end
      
      # For absolute URLs, check host allowlist
      if uri.absolute?
        unless uri.scheme.in?(%w[http https])
          raise ArgumentError, 'Invalid URL scheme'
        end
        
        unless ALLOWED_REDIRECT_HOSTS.include?(uri.host)
          raise ArgumentError, 'Host not in allowlist'
        end
        
        return url
      end
      
      raise ArgumentError, 'Invalid URL format'
      
    rescue URI::InvalidURIError
      raise ArgumentError, 'Malformed URL'
    end
  end
  
  def safe_redirect_to(url, fallback = root_path)
    begin
      safe_url = validate_redirect_url(url)
      redirect_to safe_url
    rescue ArgumentError => e
      Rails.logger.warn "Redirect validation failed: #{e.message} for URL: #{url}"
      redirect_to fallback
    end
  end
  
  def create
    user = User.authenticate(params[:email], params[:password])
    
    if user
      session[:user_id] = user.id
      
      # Secure: Validate redirect URL
      redirect_url = params[:redirect_url]
      safe_redirect_to(redirect_url, dashboard_path)
    else
      flash[:error] = 'Invalid credentials'
      render :new
    end
  end
  
  def logout
    session.clear
    
    # Secure: Validate return URL
    return_url = params[:return_url]
    safe_redirect_to(return_url, login_path)
  end
end

class ApplicationController < ActionController::Base
  def redirect_after_action
    # Secure: Use internal routes only
    begin
      # Check if next parameter is a valid internal route
      next_page = params[:next]
      
      if next_page.present?
        # Try to recognize as internal route
        Rails.application.routes.recognize_path(next_page)
        redirect_to next_page
      else
        redirect_to root_path
      end
      
    rescue ActionController::RoutingError
      # Invalid route, redirect to safe default
      redirect_to root_path
    rescue => e
      Rails.logger.error "Redirect error: #{e.message}"
      redirect_to root_path
    end
  end
  
  # Alternative: Use permitted redirect paths
  SAFE_REDIRECT_PATHS = [
    '/dashboard',
    '/profile',
    '/settings',
    '/help'
  ].freeze
  
  def safe_internal_redirect
    requested_path = params[:path]
    
    if SAFE_REDIRECT_PATHS.include?(requested_path)
      redirect_to requested_path
    else
      redirect_to root_path
    end
  end
end

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Rails controller redirects with user input: redirect_to params[:url]. params[:url] user-controlled. Open redirect vulnerability. Attackers craft phishing URLs. redirect_to trusts input without validation. Users redirected to malicious sites appearing legitimate through trusted domain.

Root causes

Using redirect_to with Unvalidated params

Rails controller redirects with user input: redirect_to params[:url]. params[:url] user-controlled. Open redirect vulnerability. Attackers craft phishing URLs. redirect_to trusts input without validation. Users redirected to malicious sites appearing legitimate through trusted domain.

Using redirect_to with URL from Request

Redirect based on request data: redirect_to request.referrer or redirect_to params[:return_to]. Referrer and params untrusted. Attackers control redirect destination. OAuth flows vulnerable to redirect manipulation. Post-login redirect exploitation common attack vector.

Not Validating Redirect URLs Against Allowlist

Missing URL validation: url = params[:destination]; redirect_to url. No check if URL within application or trusted domains. External redirects without validation. Allowlist approach not implemented. Any URL accepted for redirection.

Using only_path Without Additional Validation

Partial validation with only_path: redirect_to params[:path], only_path: true. only_path: true prevents full URLs but still allows manipulation. Path can be any route. External redirect still possible with //evil.com syntax. only_path insufficient protection alone.

Building Redirect URLs with String Interpolation

Constructing URLs: redirect_to "/dashboard?next=#{params[:page]}". String interpolation with params. Crafted values like /dashboard?next=//evil.com. URL parsing ambiguities. String building creates injection points for open redirects.

Fixes

1

Validate Redirect URLs Against Allowlist of Paths

Allowlist validation: ALLOWED_PATHS = ['/dashboard', '/profile', '/settings']; redirect_path = params[:path]; raise unless ALLOWED_PATHS.include?(redirect_path); redirect_to redirect_path. Explicit allowed paths. No arbitrary redirects. Users choose from predetermined safe destinations.

2

Use Named Routes Instead of Dynamic URLs

Use route helpers: redirect_to dashboard_path or redirect_to user_profile_path(current_user). Named routes guaranteed internal. No external redirect risk. Controller and route names instead of URLs. Rails routing ensures validity.

3

Validate URL Scheme and Host

Check URL components: uri = URI.parse(params[:url]); raise unless uri.scheme == 'https' && uri.host == request.host; redirect_to uri.to_s. Parse URL. Validate scheme and host. Ensure same origin. Prevent redirects to external sites.

4

Use only_path: true with Path Validation

Combine only_path with validation: path = params[:path]; raise unless path.starts_with?('/') && !path.include?('//'); redirect_to path, only_path: true. only_path prevents full URLs. Additional checks for // preventing protocol-relative URLs. Layered approach.

5

Implement Signed Redirect Tokens

Sign return URLs: redirect_url = verifier.generate(params[:return_to]); later: safe_url = verifier.verify(params[:token]); redirect_to safe_url. Rails message verifier. Cryptographically signed tokens. Tampering detected. Only application-generated URLs accepted.

6

Run Brakeman to Detect Unsafe Redirects

Use Brakeman scanner: brakeman -A detects redirect vulnerabilities. Review Redirect warnings. Fix flagged redirect_to calls. Automated detection in CI/CD. Regular scans prevent unsafe redirects. Brakeman specifically checks redirect_to with user input.

Detect This Vulnerability in Your Code

Sourcery automatically identifies ruby rails open redirect vulnerability (brakeman check) and many other security issues in your codebase.