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
| Impact | Details |
|---|---|
| Access Control | Scope: 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
- MITRE Corporation. "CWE-551: Incorrect Behavior Order: Authorization Before Parsing and Canonicalization." https://cwe.mitre.org/data/definitions/551.html
- OWASP. "Path Traversal Cheat Sheet."
- CWE-41. "Improper Resolution of Path Equivalence."