Verwendung nicht-kanonischer URL-Pfade für Autorisierungsentscheidungen
Beschreibung
Die Verwendung nicht-kanonischer URL-Pfade für Autorisierungsentscheidungen tritt auf, wenn Sicherheitskontrollen URL-Pfade abgleichen, ohne sie vorher zu normalisieren. Verschiedene URL-Darstellungen (Path-Traversal-Sequenzen, URL-Kodierung, Groß-/Kleinschreibungsvarianten, abschließende Schrägstriche) können auf dieselbe Ressource verweisen, aber Zugriffskontrollen umgehen, die einfachen String-Abgleich auf Pfade durchführen.
Risiko
Autorisierungsumgehung durch alternative URL-Kodierungen. Path-Traversal-Sequenzen umgehen Sicherheitsfilter. Groß-/Kleinschreibungssensitiver Abgleich auf groß-/kleinschreibungsunempfindlichen Dateisystemen ermöglicht Umgehung. Doppelte Kodierung umgeht Ein-Dekodierungs-Filter. Verzeichnisdurchquerung durch URL-Manipulation. Zugriff auf geschützte Ressourcen durch URL-Normalisierungsunterschiede.
Lösung
Kanonisieren Sie URLs vor Autorisierungsentscheidungen. Normalisieren Sie Path-Traversal-Sequenzen (/../, /./). Dekodieren Sie URL-Kodierung vor dem Abgleich. Behandeln Sie Groß-/Kleinschreibung konsistent. Entfernen Sie doppelte Schrägstriche und abschließende Schrägstriche. Verwenden Sie Framework-bereitgestellte Pfadnormalisierung. Wenden Sie Autorisierung nach URL-Auflösung an.
Häufige Konsequenzen
| Auswirkung | Details |
|---|---|
| Autorisierung | Umfang: Umgehung Zugriffskontrollen durch URL-Manipulation umgangen. |
| Vertraulichkeit | Umfang: Datenoffenlegung Zugriff auf geschützte Ressourcen. |
| Integrität | Umfang: Nicht autorisierte Aktionen Geschützte Operationen ausgeführt. |
Beispielcode + Lösungscode
Anfälliger Code
// ANFÄLLIG: String-Abgleich ohne Normalisierung
@WebFilter("/*")
public class VulnerableAuthFilter implements Filter {
private static final Set<String> PROTECTED_PATHS = Set.of(
"/admin",
"/admin/users",
"/api/internal"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String path = request.getRequestURI();
// ANFÄLLIG: Direkter String-Abgleich
if (PROTECTED_PATHS.contains(path)) {
if (!isAuthenticated(request)) {
((HttpServletResponse) res).sendError(403);
return;
}
}
// Umgehungen:
// /admin/ (abschließender Schrägstrich)
// /admin/../admin (Path-Traversal)
// /ADMIN (Groß-/Kleinschreibungsvariante)
// /admin%2Fusers (kodierter Schrägstrich)
// //admin (doppelter Schrägstrich)
// /./admin (Punkt-Segment)
chain.doFilter(req, res);
}
}
// ANFÄLLIG: Starts-with-Abgleich
public class VulnerableApiFilter {
public boolean isProtectedPath(String path) {
// ANFÄLLIG: Einfacher Präfix-Abgleich
return path.startsWith("/api/admin");
// Umgehung: /api/admin/../public
// Der Pfad beginnt mit /api/admin, aber löst zu /api/public auf
}
}
// ANFÄLLIG: Regex ohne Normalisierung
public class VulnerableRegexFilter {
private Pattern adminPattern = Pattern.compile("^/admin/.*");
public boolean isAdminPath(String path) {
// ANFÄLLIG: Regex auf nicht-normalisiertem Pfad
return adminPattern.matcher(path).matches();
// Umgehung: /admin%2F..%2Fpublic
// URL-kodiert aber nicht vor Abgleich dekodiert
}
}
# ANFÄLLIG: Python Pfad-Abgleich
from flask import Flask, request, abort
app = Flask(__name__)
PROTECTED_PATHS = {'/admin', '/admin/', '/api/internal'}
@app.before_request
def check_auth_vulnerable():
path = request.path
# ANFÄLLIG: Direkter String-Vergleich
if path in PROTECTED_PATHS:
if not is_authenticated():
abort(403)
# Umgehungen:
# /Admin (Groß-/Kleinschreibung)
# /admin// (doppelter Schrägstrich)
# /admin/.. (übergeordnetes Verzeichnis)
# /%61dmin (kodiertes 'a')
# ANFÄLLIG: Präfix-Abgleich
def is_protected_vulnerable(path):
# ANFÄLLIG: Einfacher String-Abgleich
protected_prefixes = ['/admin', '/internal']
return any(path.startswith(prefix) for prefix in protected_prefixes)
# Umgehung: /admin/../public
# ANFÄLLIG: Datei-Pfad-Bereitstellung
@app.route('/files/<path:filepath>')
def serve_file_vulnerable(filepath):
# ANFÄLLIG: Keine Pfadnormalisierung
base = '/var/www/files/'
# Angreifer: /files/../../etc/passwd
# Angreifer: /files/private%2fsecret.txt
if filepath.startswith('private/'):
if not is_admin():
abort(403)
return send_from_directory(base, filepath)
// ANFÄLLIG: Node.js Express Pfad-Abgleich
const express = require('express');
const app = express();
const PROTECTED_PATHS = ['/admin', '/api/internal', '/dashboard'];
// ANFÄLLIG: Direkter String-Abgleich
app.use((req, res, next) => {
const path = req.path;
if (PROTECTED_PATHS.includes(path)) {
if (!req.session.user) {
return res.status(403).send('Verboten');
}
}
next();
});
// Umgehungen:
// /admin/ (abschließender Schrägstrich)
// /ADMIN (Groß-/Kleinschreibung auf Systemen ohne Unterscheidung)
// /admin/../admin
// /%61dmin (URL-kodiert)
// ANFÄLLIG: Regex-Abgleich
app.use((req, res, next) => {
// ANFÄLLIG: Regex auf rohem Pfad
if (/^\/admin\//.test(req.path)) {
if (!req.session.isAdmin) {
return res.status(403).send('Nur Admin');
}
}
next();
});
// ANFÄLLIG: Statische Dateibereitstellung mit Pfad
app.get('/files/:filename', (req, res) => {
const filename = req.params.filename;
// ANFÄLLIG: Keine Normalisierung
if (filename.includes('secret')) {
if (!req.session.isAdmin) {
return res.status(403).send('Verboten');
}
}
// Angreifer: /files/..%2fsecret%2fdata.txt
res.sendFile(`/data/${filename}`);
});
<?php
// ANFÄLLIG: Direkter Pfad-Abgleich
$protectedPaths = ['/admin', '/admin/', '/internal'];
$requestPath = $_SERVER['REQUEST_URI'];
// ANFÄLLIG: Einfache in_array-Prüfung
if (in_array($requestPath, $protectedPaths)) {
if (!isAuthenticated()) {
http_response_code(403);
die('Verboten');
}
}
// Umgehungen:
// /admin?foo=bar (Query-String)
// /Admin (Groß-/Kleinschreibung)
// /admin/../admin
// /%61dmin (kodiert)
// ANFÄLLIG: String-Vergleich für Admin-Prüfung
function isAdminPathVulnerable($path) {
// ANFÄLLIG: strpos ohne Normalisierung
return strpos($path, '/admin') === 0;
}
// Umgehung: /admin/../public
// ANFÄLLIG: Dateizugriff mit Pfad
$file = $_GET['file'];
$basePath = '/var/www/uploads/';
// ANFÄLLIG: Pfad nicht normalisiert
$fullPath = $basePath . $file;
if (strpos($file, 'private/') === 0) {
if (!isAdmin()) {
die('Zugriff verweigert');
}
}
// Angreifer: ?file=../../../etc/passwd
// Oder: ?file=private/../public/data.txt (umgeht Prüfung aber greift auf public zu)
readfile($fullPath);
?>
Korrigierter Code
// SICHER: Kanonischer Pfad-Abgleich
@WebFilter("/*")
public class SafeAuthFilter implements Filter {
private static final Set<String> PROTECTED_PATHS = Set.of(
"/admin",
"/admin/users",
"/api/internal"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String path = canonicalizePath(request.getRequestURI());
// Prüfe mit normalisiertem Pfad
if (isProtectedPath(path)) {
if (!isAuthenticated(request)) {
((HttpServletResponse) res).sendError(403);
return;
}
}
chain.doFilter(req, res);
}
private String canonicalizePath(String path) {
if (path == null) return "/";
// Dekodiere URL-Kodierung
try {
path = URLDecoder.decode(path, "UTF-8");
} catch (UnsupportedEncodingException e) {
return "/";
}
// Normalisiere mit URI
try {
URI uri = new URI(path).normalize();
path = uri.getPath();
} catch (URISyntaxException e) {
return "/";
}
// Entferne abschließenden Schrägstrich (außer für Root)
if (path.length() > 1 && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
// Konvertiere zu Kleinschreibung für Groß-/Kleinschreibungsunabhängigen Abgleich
path = path.toLowerCase();
// Entferne doppelte Schrägstriche
path = path.replaceAll("/+", "/");
return path;
}
private boolean isProtectedPath(String normalizedPath) {
// Prüfe exakte Übereinstimmung
if (PROTECTED_PATHS.contains(normalizedPath)) {
return true;
}
// Prüfe Präfix-Übereinstimmung (für Sub-Pfade)
for (String protectedPath : PROTECTED_PATHS) {
if (normalizedPath.startsWith(protectedPath + "/")) {
return true;
}
}
return false;
}
}
// SICHER: Verwendung von Servlet-Pfadauflösung
public class SafePathMatcher {
public boolean isProtected(HttpServletRequest request) {
// Verwende getServletPath() das bereits normalisiert ist
String servletPath = request.getServletPath().toLowerCase();
// Oder verwende Springs AntPathMatcher mit normalisierten Pfaden
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match("/admin/**", servletPath);
}
}
# SICHER: Python mit Pfadnormalisierung
from flask import Flask, request, abort
from urllib.parse import unquote
import os
import re
app = Flask(__name__)
PROTECTED_PATHS = {'/admin', '/api/internal'}
def canonicalize_path(path):
"""Normalisiere URL-Pfad für sicheren Abgleich."""
if not path:
return '/'
# URL-Dekodierung
path = unquote(path)
# Normalisiere Pfad (löse . und .. auf)
# Verwende posixpath für URL-Pfade
import posixpath
path = posixpath.normpath(path)
# Stelle sicher dass mit / beginnt
if not path.startswith('/'):
path = '/' + path
# Entferne abschließenden Schrägstrich (außer Root)
if len(path) > 1 and path.endswith('/'):
path = path[:-1]
# Kleinschreibung für Groß-/Kleinschreibungsunabhängigen Abgleich
path = path.lower()
# Entferne doppelte Schrägstriche
path = re.sub(r'/+', '/', path)
return path
@app.before_request
def check_auth_safe():
# Normalisiere Pfad vor Prüfung
normalized_path = canonicalize_path(request.path)
for protected in PROTECTED_PATHS:
if normalized_path == protected or normalized_path.startswith(protected + '/'):
if not is_authenticated():
abort(403)
# SICHER: Dateibereitstellung mit ordnungsgemäßer Validierung
@app.route('/files/<path:filepath>')
def serve_file_safe(filepath):
base = os.path.realpath('/var/www/files/')
# Dekodiere und normalisiere
filepath = unquote(filepath)
# Baue vollständigen Pfad und löse auf
full_path = os.path.realpath(os.path.join(base, filepath))
# Verifiziere Pfad ist innerhalb des Basisverzeichnisses
if not full_path.startswith(base + os.sep):
abort(403, 'Path-Traversal erkannt')
# Prüfe Berechtigungen nach Normalisierung
relative_path = os.path.relpath(full_path, base)
if relative_path.startswith('private/'):
if not is_admin():
abort(403)
return send_from_directory(base, relative_path)
// SICHER: Node.js mit Pfadnormalisierung
const express = require('express');
const path = require('path');
const app = express();
const PROTECTED_PATHS = new Set(['/admin', '/api/internal', '/dashboard']);
function canonicalizePath(urlPath) {
if (!urlPath) return '/';
// Dekodiere URL-Kodierung
urlPath = decodeURIComponent(urlPath);
// Normalisiere Pfad
urlPath = path.posix.normalize(urlPath);
// Stelle sicher dass mit / beginnt
if (!urlPath.startsWith('/')) {
urlPath = '/' + urlPath;
}
// Entferne abschließenden Schrägstrich (außer Root)
if (urlPath.length > 1 && urlPath.endsWith('/')) {
urlPath = urlPath.slice(0, -1);
}
// Kleinschreibung
urlPath = urlPath.toLowerCase();
// Entferne doppelte Schrägstriche
urlPath = urlPath.replace(/\/+/g, '/');
return urlPath;
}
// SICHER: Middleware mit Kanonisierung
app.use((req, res, next) => {
const normalizedPath = canonicalizePath(req.path);
// Prüfe ob geschützt
if (PROTECTED_PATHS.has(normalizedPath) ||
Array.from(PROTECTED_PATHS).some(p => normalizedPath.startsWith(p + '/'))) {
if (!req.session.user) {
return res.status(403).send('Verboten');
}
}
next();
});
// SICHER: Dateibereitstellung mit Pfadvalidierung
app.get('/files/:filename(*)', (req, res) => {
const baseDir = path.resolve('/data/files');
const filename = decodeURIComponent(req.params.filename);
// Löse zu absolutem Pfad auf
const fullPath = path.resolve(baseDir, filename);
// Verifiziere innerhalb des Basisverzeichnisses
if (!fullPath.startsWith(baseDir + path.sep)) {
return res.status(403).send('Ungültiger Pfad');
}
// Prüfe Berechtigungen nach Normalisierung
const relativePath = path.relative(baseDir, fullPath);
if (relativePath.startsWith('secret')) {
if (!req.session.isAdmin) {
return res.status(403).send('Verboten');
}
}
res.sendFile(fullPath);
});
Ausgenutzt in der Praxis
Admin-Panel-Umgehung
URL-Kodierungsumgehungen für Admin-Zugriff.
WAF-Umgehung
Pfadnormalisierungsunterschiede umgehen WAF-Regeln.
Path-Traversal
Verzeichnisdurchquerung durch URL-Manipulation.
Tools zum Testen/Ausnutzen
-
Burp Suite - URL-Manipulation.
-
URL-Kodierungs-/Fuzzing-Tools.
-
Path-Traversal-Payloads.
CVE-Beispiele
-
CVE-2020-17530: Apache Struts Pfad-Umgehung.
-
Zahlreiche CVEs für URL-Normalisierungsumgehung.
Referenzen
-
MITRE. "CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions." https://cwe.mitre.org/data/definitions/647.html
-
OWASP. "Path Traversal."