Execution After Redirect (EAR)

Description

Execution After Redirect (EAR) is a control flow vulnerability where a web application sends a redirect response to another location but instead of terminating execution, continues to run additional code. When a redirect header is sent to the browser, the server-side code may still execute subsequent statements unless explicitly terminated. This allows attackers to access functionality that should have been blocked by the redirect, such as viewing protected content, executing privileged operations, or accessing administrative features even when the redirect was meant to deny access.

Risk

EAR vulnerabilities create serious security risks because they allow bypass of authentication and authorization controls. When code continues executing after a redirect intended to deny access, attackers can view sensitive information, perform unauthorized actions, or gain administrative access. The vulnerability is particularly dangerous because it often goes undetected in testing—browsers follow redirects before showing any content, so testers don't see the leaked information. However, attackers using tools that don't follow redirects can capture the full response containing sensitive data. This can lead to exposure of configuration details, session information, or complete authentication bypass.

Solution

Always terminate script execution immediately after sending a redirect. In PHP, call exit() or die() after header("Location: ..."). In Java servlets, call return after sendRedirect(). In ASP.NET, use Response.Redirect() with the second parameter set to true, or call Response.End(). Structure code so that protected content only executes in explicit success paths, not as fallthrough after redirect logic. Use framework-provided redirect mechanisms that automatically terminate execution. Implement security testing that captures responses before redirects are followed.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Sensitive information may be exposed in the response body after the redirect header.
Access ControlScope: Access Control

Bypass Protection Mechanism - Authentication and authorization checks that rely on redirects can be bypassed.
IntegrityScope: Integrity

Execute Unauthorized Code or Commands - Actions that should be blocked may still execute.

Example Code

Vulnerable Code

<?php
// Vulnerable: Code executes after redirect
$requestingIP = $_SERVER['REMOTE_ADDR'];
$allowedIPs = array('192.168.1.100', '192.168.1.101');

if (!in_array($requestingIP, $allowedIPs)) {
    echo "You are not authorized to view this page";
    header("Location: /error.php");
    // Vulnerable: No exit! Code continues executing
}

// Sensitive operations continue despite redirect
$serverStatus = getServerStatus();
$databaseInfo = getDatabaseConnectionDetails();

// Vulnerable: All this output is sent before browser follows redirect
echo "<h1>Server Administration</h1>";
echo "<p>Server Status: " . $serverStatus . "</p>";
echo "<p>DB Connection: " . $databaseInfo . "</p>";

// Attacker can capture this even though browser would redirect
?>

<!-- Another vulnerable pattern -->
<?php
function checkAuthentication() {
    if (!isset($_SESSION['user'])) {
        header("Location: /login.php");
        // Vulnerable: Function returns, but caller continues
        return false;
    }
    return true;
}

// Vulnerable: Return value not checked properly
checkAuthentication();  // Redirect sent but...

// ...execution continues!
processAdminAction($_POST['action']);
displayAdminPanel();
?>
// Vulnerable: Java servlet continues after redirect
import javax.servlet.http.*;
import java.io.*;

public class VulnerableAdminServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request,
                        HttpServletResponse response)
            throws ServletException, IOException {

        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("admin") == null) {
            response.sendRedirect("/login.jsp");
            // Vulnerable: No return! Execution continues
        }

        // All this code runs even after sendRedirect
        PrintWriter out = response.getWriter();
        out.println("<h1>Admin Panel</h1>");
        out.println("Secret admin key: " + getAdminKey());
        out.println("User list: " + getAllUsers());

        // Sensitive operation also executes
        processAdminTasks();
    }
}

// Vulnerable: Exception-based flow doesn't stop execution
public class VulnerableAuthFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                        FilterChain chain)
            throws IOException, ServletException {

        HttpServletResponse response = (HttpServletResponse) res;

        if (!isAuthenticated((HttpServletRequest) req)) {
            response.sendRedirect("/unauthorized.jsp");
            // Vulnerable: Filter chain continues
        }

        // Request still processed by downstream servlets
        chain.doFilter(req, res);
    }
}
# Vulnerable: Flask redirect without return
from flask import Flask, redirect, request, session

app = Flask(__name__)

@app.route('/admin')
def vulnerable_admin():
    if 'user' not in session:
        redirect('/login')  # Vulnerable: Not returned!
        # Execution continues

    # Sensitive data exposed
    config = get_app_configuration()
    users = get_all_users()

    return f"""
    <h1>Admin Panel</h1>
    <p>Config: {config}</p>
    <p>Users: {users}</p>
    """

@app.route('/delete/<int:item_id>')
def vulnerable_delete(item_id):
    if not is_authorized(session.get('user')):
        redirect('/unauthorized')
        # Vulnerable: Item still gets deleted!

    delete_item(item_id)  # Executes regardless of authorization
    return "Item deleted"
<!-- Vulnerable: ASP.NET without Response.End -->
<%@ Page Language="C#" %>
<%
if (!User.Identity.IsAuthenticated)
{
    Response.Redirect("Login.aspx");
    // Vulnerable: Page continues rendering
}

// Sensitive content exposed
Response.Write("<h1>Secret Administration</h1>");
Response.Write("Admin Password: " + GetAdminPassword());
Response.Write("System Keys: " + GetSystemKeys());
%>
// Vulnerable: Node.js/Express without return
const express = require('express');
const app = express();

app.get('/admin', (req, res) => {
    if (!req.session.isAdmin) {
        res.redirect('/login');
        // Vulnerable: No return - handler continues
    }

    // Sensitive data returned after redirect header
    const secrets = getSecrets();
    res.send(`<h1>Admin</h1><p>${secrets}</p>`);
    // Both redirect and content are sent!
});

Fixed Code

<?php
// Fixed: Exit immediately after redirect
$requestingIP = $_SERVER['REMOTE_ADDR'];
$allowedIPs = array('192.168.1.100', '192.168.1.101');

if (!in_array($requestingIP, $allowedIPs)) {
    header("Location: /error.php");
    exit();  // Fixed: Terminate execution immediately
}

// Only executes if IP is allowed
$serverStatus = getServerStatus();
$databaseInfo = getDatabaseConnectionDetails();

echo "<h1>Server Administration</h1>";
echo "<p>Server Status: " . $serverStatus . "</p>";
?>

<?php
// Fixed: Function terminates execution
function requireAuthentication() {
    if (!isset($_SESSION['user'])) {
        header("Location: /login.php");
        exit();  // Fixed: Terminate in function
    }
}

// Fixed: Call properly terminates if not authenticated
requireAuthentication();

// Only reaches here if authenticated
processAdminAction($_POST['action']);
displayAdminPanel();
?>

<?php
// Fixed: Alternative - structure code properly
if (isset($_SESSION['user'])) {
    // Protected content only in authenticated block
    processAdminAction($_POST['action']);
    displayAdminPanel();
} else {
    header("Location: /login.php");
    exit();
}
?>
// Fixed: Return after redirect
import javax.servlet.http.*;
import java.io.*;

public class SecureAdminServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request,
                        HttpServletResponse response)
            throws ServletException, IOException {

        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("admin") == null) {
            response.sendRedirect("/login.jsp");
            return;  // Fixed: Return immediately after redirect
        }

        // Only executes if authenticated
        PrintWriter out = response.getWriter();
        out.println("<h1>Admin Panel</h1>");
        out.println("Secret admin key: " + getAdminKey());

        processAdminTasks();
    }
}

// Fixed: Filter properly stops chain
public class SecureAuthFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                        FilterChain chain)
            throws IOException, ServletException {

        HttpServletResponse response = (HttpServletResponse) res;

        if (!isAuthenticated((HttpServletRequest) req)) {
            response.sendRedirect("/unauthorized.jsp");
            return;  // Fixed: Don't continue filter chain
        }

        // Only continue if authenticated
        chain.doFilter(req, res);
    }
}
# Fixed: Return redirect response
from flask import Flask, redirect, request, session

app = Flask(__name__)

@app.route('/admin')
def secure_admin():
    if 'user' not in session:
        return redirect('/login')  # Fixed: Return the redirect

    # Only executes if authenticated
    config = get_app_configuration()
    users = get_all_users()

    return f"""
    <h1>Admin Panel</h1>
    <p>Config: {config}</p>
    <p>Users: {users}</p>
    """

@app.route('/delete/<int:item_id>')
def secure_delete(item_id):
    if not is_authorized(session.get('user')):
        return redirect('/unauthorized')  # Fixed: Return redirect

    # Only executes if authorized
    delete_item(item_id)
    return "Item deleted"

# Alternative: Use decorator pattern
from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if 'user' not in session:
            return redirect('/login')
        return f(*args, **kwargs)
    return decorated

@app.route('/admin')
@require_auth
def admin_with_decorator():
    # This only runs if authenticated
    return render_admin_panel()
<%@ Page Language="C#" %>
<%
// Fixed: Use Response.Redirect with endResponse=true
if (!User.Identity.IsAuthenticated)
{
    Response.Redirect("Login.aspx", true);
    // Second parameter ends response
    // Alternatively: Response.Redirect("Login.aspx"); Response.End();
}

// Only executes if authenticated
Response.Write("<h1>Secret Administration</h1>");
%>
// Fixed: Node.js/Express with return
const express = require('express');
const app = express();

app.get('/admin', (req, res) => {
    if (!req.session.isAdmin) {
        return res.redirect('/login');  // Fixed: Return after redirect
    }

    // Only executes if admin
    const secrets = getSecrets();
    res.send(`<h1>Admin</h1><p>${secrets}</p>`);
});

// Fixed: Use middleware for auth
function requireAdmin(req, res, next) {
    if (!req.session.isAdmin) {
        return res.redirect('/login');  // Fixed: Return
    }
    next();
}

app.get('/admin', requireAdmin, (req, res) => {
    // Only reaches here if admin check passed
    res.send(getAdminPanel());
});

CVE Examples

  • CVE-2013-1402: Application exposed configuration details after redirect.
  • CVE-2009-1936: EAR combined with file inclusion and path traversal.
  • CVE-2007-2713: Unauthorized administrator access due to execution after redirect.
  • CVE-2007-5578: Authentication bypass through EAR.
  • CVE-2007-6652: Non-admin static code injection via EAR.

References

  1. MITRE Corporation. "CWE-698: Execution After Redirect (EAR)." https://cwe.mitre.org/data/definitions/698.html
  2. OWASP. "Execution After Redirect."
  3. CWE-670: Always-Incorrect Control Flow Implementation.