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?