422.44 Invalid forwarding and redirecting

Prevent attackers from manipulating redirects and forwards to bypass access controls, deliver malicious payloads, or trick users.

422.44 Invalid Forwarding and Redirecting

Prevent attackers from manipulating redirects and forwards to bypass access controls, deliver malicious payloads, or trick users.

Overview

Invalid forwarding and redirecting occur when an application redirects or forwards a user to another page without validating the destination. Attackers can exploit this by crafting links that appear legitimate but lead users to untrusted or dangerous locations, or even gain access to restricted areas of a system.

These flaws are often overlooked because redirects and forwards are standard features in login workflows, post-submission pages, and internal navigation. When misused, they can lead to phishing, privilege escalation, and session hijacking.

Learning Targets

In this topic, students learn to:

  • Explain how unvalidated redirects and forwards create vulnerabilities

  • Identify common scenarios where redirects or forwards are used in Flask applications

  • Implement secure checks before performing any redirect or forward

  • Validate all user-supplied URLs and destinations

What is an Invalid Redirect?

Redirects send users from one URL to another. If an attacker can control the destination URL, they can trick users into visiting a fake or malicious site that looks trustworthy.

Vulnerable Flask Example

from flask import Flask, request, redirect

app = Flask(__name__)

@app.route('/redirect')
def unsafe_redirect():
    # VULNERABLE: Redirecting to user-controlled URL
    next_url = request.args.get('next', '/')
    return redirect(next_url)

# Attack example:
# /redirect?next=http://evil.com/fake-login
# User thinks they're going to your site but ends up on attacker's site

Real Attack Scenario

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    
    if authenticate_user(username, password):
        session['user_id'] = get_user_id(username)
        
        # VULNERABLE: Redirect to user-specified page after login
        next_page = request.form.get('next', '/dashboard')
        return redirect(next_page)
    
    return "Invalid credentials"

# Attacker sends victim this link:
# /login?next=http://evil.com/steal-session
# After successful login, user gets redirected to attacker's site
# Attacker's site can steal session cookies or trick user into entering credentials again

What is an Invalid Forward?

Forwards transfer control internally to another part of the application. If input isn't validated, attackers may access restricted files or bypass security checks.

Vulnerable Forward Example

@app.route('/view')
def view_page():
    # VULNERABLE: Forward to user-specified page
    page = request.args.get('page', 'home')
    
    # Attacker could request: /view?page=admin
    # This might show admin pages to regular users
    return render_template(f'{page}.html')

# Attack examples:
# /view?page=../admin/users  (path traversal)
# /view?page=admin           (privilege escalation)

Secure Redirect Implementation

Method 1: Whitelist Allowed URLs

from urllib.parse import urlparse

# Define allowed redirect destinations
ALLOWED_REDIRECTS = [
    '/dashboard',
    '/profile', 
    '/settings',
    '/logout'
]

# For external redirects, specify allowed domains
ALLOWED_DOMAINS = [
    'yoursite.com',
    'api.yoursite.com'
]

def is_safe_url(url):
    """
    Check if URL is safe for redirecting
    Returns: True if safe, False if dangerous
    """
    if not url:
        return False
    
    # Parse the URL to check its components
    try:
        parsed = urlparse(url)
    except:
        return False
    
    # Check for dangerous patterns
    if url.startswith('javascript:') or url.startswith('data:'):
        return False
    
    # If it's a relative URL (no domain), check against whitelist
    if not parsed.netloc:
        return url in ALLOWED_REDIRECTS
    
    # If it's an absolute URL, check domain whitelist
    domain = parsed.netloc.lower()
    return domain in ALLOWED_DOMAINS

@app.route('/redirect_secure')
def safe_redirect():
    next_url = request.args.get('next', '/')
    
    # SECURE: Validate URL before redirecting
    if is_safe_url(next_url):
        return redirect(next_url)
    else:
        # Redirect to safe default instead
        return redirect('/dashboard')

Method 2: Relative URLs Only

@app.route('/login', methods=['POST'])
def secure_login():
    username = request.form['username']
    password = request.form['password']
    
    if authenticate_user(username, password):
        session['user_id'] = get_user_id(username)
        
        next_page = request.form.get('next', '/dashboard')
        
        # SECURE: Only allow relative URLs (same domain)
        if next_page.startswith('/') and not next_page.startswith('//'):
            return redirect(next_page)
        else:
            # Default redirect for suspicious URLs
            return redirect('/dashboard')
    
    return "Invalid credentials"

# This prevents:
# - External redirects (http://evil.com)
# - Protocol-relative URLs (//evil.com)
# - JavaScript URLs (javascript:alert())

Secure Forward Implementation

Validate Page Names

# Define allowed pages
ALLOWED_PAGES = [
    'home',
    'about', 
    'contact',
    'products'
]

@app.route('/view_secure')
def view_page_secure():
    page = request.args.get('page', 'home')
    
    # SECURE: Validate against whitelist
    if page in ALLOWED_PAGES:
        return render_template(f'{page}.html')
    else:
        # Return error or default page for invalid requests
        return "Page not found", 404

# This prevents:
# - Path traversal (../admin/secret)
# - Arbitrary file access 
# - Access to restricted pages

Role-Based Page Access

@app.route('/admin_view')
def admin_view():
    if 'user_id' not in session:
        return redirect('/login')
    
    # Check if user is admin
    user_id = session['user_id']
    if not is_admin_user(user_id):
        return "Access denied", 403
    
    page = request.args.get('page', 'dashboard')
    
    # SECURE: Admin-specific page whitelist
    admin_pages = ['dashboard', 'users', 'settings', 'reports']
    
    if page in admin_pages:
        return render_template(f'admin/{page}.html')
    else:
        return redirect('/admin_view?page=dashboard')

def is_admin_user(user_id):
    """Check if user has admin privileges"""
    cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,))
    result = cursor.fetchone()
    return result and result[0] == 'admin'

External Redirect with Warning

Sometimes you need to redirect to external sites. In these cases, warn the user first:

@app.route('/external_redirect')
def external_redirect():
    target_url = request.args.get('url')
    confirmed = request.args.get('confirmed') == '1'
    
    # Basic URL validation
    if not target_url or not target_url.startswith(('http://', 'https://')):
        return "Invalid URL", 400
    
    if not confirmed:
        # Show confirmation page
        return render_template('redirect_warning.html', target_url=target_url)
    
    # User confirmed, proceed with redirect
    return redirect(target_url)
<!-- redirect_warning.html -->
<div class="warning">
    <h2>External Redirect Warning</h2>
    <p>You are about to leave our site and go to:</p>
    <p><strong>{{ target_url }}</strong></p>
    <p>Do you want to continue?</p>
    
    <a href="{{ target_url }}">Yes, continue</a>
    <a href="/">No, stay here</a>
</div>

Code Interpretation Examples

# Example 1: Identify the vulnerability
@app.route('/go')
def redirect_user():
    destination = request.args.get('to')
    return redirect(destination)

# Problem: No validation of destination
# Attacker could use: /go?to=http://evil.com
# Example 2: Path traversal vulnerability  
@app.route('/file')
def serve_file():
    filename = request.args.get('name')
    return send_file(f'uploads/{filename}')

# Problem: Could access files outside uploads directory
# Attack: /file?name=../../../etc/passwd
# Example 3: Secure version
@app.route('/file_secure')
def serve_file_secure():
    filename = request.args.get('name')
    
    # Validate filename
    if not filename or '..' in filename or filename.startswith('/'):
        return "Invalid filename", 400
    
    # Only allow specific file types
    allowed_extensions = ['.jpg', '.png', '.pdf', '.txt']
    if not any(filename.endswith(ext) for ext in allowed_extensions):
        return "File type not allowed", 400
    
    return send_file(f'uploads/{filename}')

Real-World Examples in Student Projects

# Example: After user updates profile
@app.route('/update_profile', methods=['POST'])
def update_profile():
    if 'user_id' not in session:
        return redirect('/login')
    
    # Update user profile...
    
    # SECURE: Fixed redirect after successful update
    return redirect('/profile')
    
    # AVOID: User-controlled redirect
    # return redirect(request.form.get('next', '/profile'))

# Example: Search results with safe pagination
@app.route('/search')
def search_results():
    query = request.args.get('q', '')
    page = request.args.get('page', '1')
    
    # Validate page number
    try:
        page_num = int(page)
        if page_num < 1:
            page_num = 1
    except ValueError:
        page_num = 1
    
    # Perform search...
    return render_template('search.html', results=results, page=page_num)

# Example: Safe logout redirect
@app.route('/logout')
def logout():
    session.clear()
    
    # SECURE: Always redirect to home page
    return redirect('/')
    
    # AVOID: User-controlled logout redirect
    # return redirect(request.args.get('next', '/'))

Common Attack Patterns to Prevent

# Dangerous URLs that should be blocked:

# External redirects
"http://evil.com/fake-login"
"https://attacker.site/steal-data"

# Protocol-relative URLs
"//evil.com/malicious-page"

# JavaScript URLs
"javascript:alert('XSS')"
"javascript:document.location='http://evil.com'"

# Data URLs
"data:text/html,<script>alert('XSS')</script>"

# Path traversal
"../admin/secret"
"../../etc/passwd"
"/admin/users"

Prevention Checklist

Summary

  • Invalid redirects can send users to malicious sites that appear legitimate

  • Invalid forwards can give attackers access to restricted files or functionality

  • Whitelist validation is the most secure approach for both redirects and forwards

  • Relative URLs are safer than absolute URLs for internal navigation

  • User warnings should be shown before external redirects

  • Input validation must be applied to all user-controlled navigation parameters

By implementing proper validation and using safe defaults, you can prevent attackers from manipulating your Flask application's navigation to trick users or bypass security controls.

Last updated

Was this helpful?