Incorrect Behavior Order: Authorization Before Parsing and Canonicalization

Description

Incorrect Behavior Order: Authorization Before Parsing and Canonicalization is a vulnerability where a web server or application performs authorization checks on URL paths before fully parsing and normalizing them. This ordering mistake allows attackers to bypass access controls by using alternative path representations that are semantically equivalent but textually different. For example, if /admin is protected but the server checks authorization before path normalization, requests to /./admin, //admin, /admin/../admin, or URL-encoded variants like /%61dmin may bypass the protection.

Risk

Path canonicalization bypass vulnerabilities create critical access control failures. Attackers can access protected resources, administrative functions, or sensitive files by crafting URLs that pass authorization checks in their raw form but resolve to protected paths after processing. Path equivalence techniques include dot segments (./, ../), double slashes, URL encoding, Unicode normalization, case variations, and trailing characters. Since these bypasses exploit the fundamental order of operations, they can affect multiple protected resources simultaneously. The impact ranges from unauthorized data access to complete administrative takeover.

Solution

Always perform URL parsing and canonicalization before authorization checks. Normalize paths by resolving dot segments, collapsing multiple slashes, decoding URL-encoded characters, and normalizing case where appropriate. Perform authorization checks on the canonicalized, final path that will actually be used. Be careful to avoid double-decoding vulnerabilities by decoding exactly once. Use established libraries or framework functions for path normalization rather than custom implementations. Implement defense in depth by validating paths at multiple layers. Test authorization with various path encoding and equivalence techniques.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Authorization controls can be circumvented through uncanonical path representations that evade access checks but resolve to protected resources.

Example Code

Vulnerable Code

// Vulnerable: Authorization before canonicalization
public class VulnerableAuthorizationFilter implements Filter {

    private Set<String> protectedPaths = Set.of("/admin", "/api/admin", "/management");

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

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestPath = httpRequest.getRequestURI();

        // Vulnerable: Checking raw path before normalization
        if (isProtectedPath(requestPath)) {
            if (!isAuthenticated(httpRequest)) {
                ((HttpServletResponse) response).sendError(403, "Access denied");
                return;
            }
        }

        // Path is canonicalized later during resource resolution
        chain.doFilter(request, response);
    }

    private boolean isProtectedPath(String path) {
        // Vulnerable: Checking against raw, non-normalized path
        for (String protectedPath : protectedPaths) {
            if (path.startsWith(protectedPath)) {
                return true;
            }
        }
        return false;
    }

    // Bypass examples:
    // /./admin - Passes check (doesn't start with "/admin")
    // //admin - Passes check (starts with "//admin", not "/admin")
    // /admin/../admin - May pass depending on implementation
    // /%61dmin - URL-encoded 'a' bypasses string comparison
    // /Admin - Case-sensitive comparison bypass
}
# Vulnerable: Flask authorization middleware with path issues
from flask import Flask, request, abort
from functools import wraps
import re

app = Flask(__name__)

PROTECTED_PATHS = ['/admin', '/api/internal', '/management']

def check_authorization(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # Vulnerable: Using raw request path
        request_path = request.path

        # Vulnerable: Check before canonicalization
        for protected in PROTECTED_PATHS:
            if request_path.startswith(protected):
                if not is_admin_user():
                    abort(403)

        return f(*args, **kwargs)
    return decorated

# Vulnerable: Direct path check without normalization
@app.before_request
def authorize_request():
    raw_path = request.path

    # Vulnerable: Path not normalized
    # Bypasses: /./admin, //admin, /admin%2f, /ADMIN
    if raw_path in PROTECTED_PATHS or any(raw_path.startswith(p + '/') for p in PROTECTED_PATHS):
        if not session.get('is_admin'):
            return abort(403)

# Vulnerable: URL routing happens after this check
# Flask normalizes paths during routing, but auth was already checked
// Vulnerable: Express.js middleware with authorization bypass
const express = require('express');
const app = express();

const PROTECTED_PATHS = ['/admin', '/api/admin', '/management'];

// Vulnerable: Authorization middleware
app.use((req, res, next) => {
    const requestPath = req.path;

    // Vulnerable: Checking raw path
    const isProtected = PROTECTED_PATHS.some(p =>
        requestPath.startsWith(p) || requestPath === p
    );

    if (isProtected) {
        if (!req.session?.isAdmin) {
            return res.status(403).send('Access denied');
        }
    }

    next();
});

// Vulnerable: Path normalization happens during routing
// Bypasses work because Express normalizes paths AFTER middleware

// Examples that bypass:
// GET /./admin -> Normalized to /admin after auth check
// GET //admin -> Double slash normalized
// GET /admin%2F -> URL decoding happens during routing
// GET /Admin -> Case sensitivity varies by OS

app.get('/admin', (req, res) => {
    res.send('Admin panel');
});
<?php
// Vulnerable: PHP authorization with path issues
class VulnerableAuthorization {
    private $protectedPaths = ['/admin', '/api/admin', '/dashboard'];

    public function checkAccess($requestUri) {
        // Vulnerable: Using raw URI without normalization
        $path = parse_url($requestUri, PHP_URL_PATH);

        // Vulnerable: Simple string matching on non-canonical path
        foreach ($this->protectedPaths as $protected) {
            if (strpos($path, $protected) === 0) {
                if (!$this->isAdminUser()) {
                    http_response_code(403);
                    die('Access denied');
                }
            }
        }

        // Bypasses:
        // /./admin - Dot segment not resolved
        // //admin - Double slash not collapsed
        // /admin/../admin - Parent traversal works
        // /%61dmin - URL encoding bypasses string match
        // /admin%00extra - Null byte injection (older PHP)
    }

    private function isAdminUser() {
        return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
    }
}

// Vulnerable: Authorization check
$auth = new VulnerableAuthorization();
$auth->checkAccess($_SERVER['REQUEST_URI']);

// PHP processes the request, normalizing the path for file resolution
// The authorization was already passed with the raw path
?>

Fixed Code

// Fixed: Canonicalize before authorization
public class SecureAuthorizationFilter implements Filter {

    private Set<String> protectedPaths = Set.of("/admin", "/api/admin", "/management");

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

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // Fixed: Canonicalize path BEFORE authorization
        String canonicalPath = canonicalizePath(httpRequest.getRequestURI());

        if (isProtectedPath(canonicalPath)) {
            if (!isAuthenticated(httpRequest)) {
                ((HttpServletResponse) response).sendError(403, "Access denied");
                return;
            }
        }

        chain.doFilter(request, response);
    }

    private String canonicalizePath(String path) {
        if (path == null) return "/";

        // Fixed: Decode URL-encoded characters (once!)
        try {
            path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // Should never happen with UTF-8
        }

        // Fixed: Normalize path using Java's URI normalization
        try {
            URI uri = new URI(path).normalize();
            path = uri.getPath();
        } catch (URISyntaxException e) {
            // Invalid URI, reject
            return null;
        }

        // Fixed: Collapse multiple slashes
        path = path.replaceAll("/+", "/");

        // Fixed: Remove trailing slash for consistency
        if (path.length() > 1 && path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        // Fixed: Normalize case (if filesystem is case-insensitive)
        path = path.toLowerCase();

        return path;
    }

    private boolean isProtectedPath(String path) {
        if (path == null) return true;  // Reject invalid paths

        for (String protectedPath : protectedPaths) {
            if (path.equals(protectedPath) || path.startsWith(protectedPath + "/")) {
                return true;
            }
        }
        return false;
    }

    private boolean isAuthenticated(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        return session != null && session.getAttribute("admin") != null;
    }
}
# Fixed: Flask authorization with proper path canonicalization
from flask import Flask, request, abort, session
from functools import wraps
from urllib.parse import unquote
import posixpath
import re

app = Flask(__name__)

PROTECTED_PATHS = ['/admin', '/api/internal', '/management']

def canonicalize_path(path):
    """Canonicalize URL path before authorization."""
    if not path:
        return '/'

    # Fixed: URL decode (once only)
    path = unquote(path)

    # Fixed: Normalize path (resolve . and ..)
    path = posixpath.normpath(path)

    # Fixed: Collapse multiple slashes
    path = re.sub(r'/+', '/', path)

    # Fixed: Ensure path starts with /
    if not path.startswith('/'):
        path = '/' + path

    # Fixed: Remove trailing slash (except for root)
    if path != '/' and path.endswith('/'):
        path = path.rstrip('/')

    # Fixed: Normalize case
    path = path.lower()

    return path

def check_authorization():
    """Middleware to check authorization on canonicalized paths."""
    # Fixed: Canonicalize BEFORE checking
    canonical_path = canonicalize_path(request.path)

    for protected in PROTECTED_PATHS:
        if canonical_path == protected or canonical_path.startswith(protected + '/'):
            if not session.get('is_admin'):
                abort(403)

@app.before_request
def authorize_request():
    check_authorization()

# Fixed: Additional path validation
@app.before_request
def validate_path():
    canonical = canonicalize_path(request.path)

    # Fixed: Reject path traversal attempts
    if '..' in request.path or canonical != canonicalize_path(canonical):
        abort(400, 'Invalid path')

@app.route('/admin')
def admin_panel():
    return 'Admin Panel'

@app.route('/api/internal/data')
def internal_data():
    return 'Internal Data'
// Fixed: Express.js with secure path canonicalization
const express = require('express');
const path = require('path');

const app = express();

const PROTECTED_PATHS = ['/admin', '/api/admin', '/management'];

function canonicalizePath(urlPath) {
    if (!urlPath) return '/';

    // Fixed: Decode URL-encoded characters (once)
    try {
        urlPath = decodeURIComponent(urlPath);
    } catch (e) {
        return null;  // Invalid encoding
    }

    // Fixed: Normalize using path module
    urlPath = path.posix.normalize(urlPath);

    // Fixed: Collapse multiple slashes
    urlPath = urlPath.replace(/\/+/g, '/');

    // Fixed: Remove trailing slash
    if (urlPath !== '/' && urlPath.endsWith('/')) {
        urlPath = urlPath.slice(0, -1);
    }

    // Fixed: Normalize case
    urlPath = urlPath.toLowerCase();

    return urlPath;
}

// Fixed: Authorization middleware with canonicalization
app.use((req, res, next) => {
    // Fixed: Canonicalize BEFORE authorization
    const canonicalPath = canonicalizePath(req.path);

    if (canonicalPath === null) {
        return res.status(400).send('Invalid path');
    }

    // Fixed: Check canonicalized path
    const isProtected = PROTECTED_PATHS.some(p =>
        canonicalPath === p || canonicalPath.startsWith(p + '/')
    );

    if (isProtected) {
        if (!req.session?.isAdmin) {
            return res.status(403).send('Access denied');
        }
    }

    next();
});

// Fixed: Validate path doesn't attempt traversal
app.use((req, res, next) => {
    if (req.path.includes('..')) {
        return res.status(400).send('Invalid path');
    }
    next();
});

app.get('/admin', (req, res) => {
    res.send('Admin panel');
});

module.exports = app;
<?php
// Fixed: PHP authorization with proper canonicalization
class SecureAuthorization {
    private $protectedPaths = ['/admin', '/api/admin', '/dashboard'];

    public function checkAccess($requestUri) {
        // Fixed: Canonicalize BEFORE authorization
        $canonicalPath = $this->canonicalizePath($requestUri);

        if ($canonicalPath === null) {
            http_response_code(400);
            die('Invalid request');
        }

        foreach ($this->protectedPaths as $protected) {
            if ($canonicalPath === $protected ||
                strpos($canonicalPath, $protected . '/') === 0) {
                if (!$this->isAdminUser()) {
                    http_response_code(403);
                    die('Access denied');
                }
            }
        }
    }

    private function canonicalizePath($uri) {
        // Fixed: Parse URL to get path
        $path = parse_url($uri, PHP_URL_PATH);
        if ($path === false) return null;

        // Fixed: URL decode (once)
        $path = rawurldecode($path);

        // Fixed: Check for null bytes (security)
        if (strpos($path, "\0") !== false) {
            return null;
        }

        // Fixed: Normalize path segments
        $segments = explode('/', $path);
        $normalized = [];

        foreach ($segments as $segment) {
            if ($segment === '.' || $segment === '') {
                continue;
            }
            if ($segment === '..') {
                array_pop($normalized);
            } else {
                $normalized[] = $segment;
            }
        }

        $path = '/' . implode('/', $normalized);

        // Fixed: Collapse multiple slashes
        $path = preg_replace('#/+#', '/', $path);

        // Fixed: Normalize case
        $path = strtolower($path);

        return $path;
    }

    private function isAdminUser() {
        return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
    }
}

// Fixed: Authorization check with canonicalization
$auth = new SecureAuthorization();
$auth->checkAccess($_SERVER['REQUEST_URI']);
?>

CVE Examples

  • CVE-2021-41773: Apache HTTP Server path traversal vulnerability allowed access to files outside the document root due to insufficient path normalization.
  • CVE-2020-5410: Spring Cloud Config Server had path traversal vulnerability allowing access to arbitrary files.
  • CVE-2019-11043: PHP-FPM vulnerability where path handling issues led to remote code execution.

References

  1. MITRE Corporation. "CWE-551: Incorrect Behavior Order: Authorization Before Parsing and Canonicalization." https://cwe.mitre.org/data/definitions/551.html
  2. OWASP. "Path Traversal Cheat Sheet."
  3. CWE-41. "Improper Resolution of Path Equivalence."