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

AuswirkungDetails
AutorisierungUmfang: Umgehung

Zugriffskontrollen durch URL-Manipulation umgangen.
VertraulichkeitUmfang: Datenoffenlegung

Zugriff auf geschützte Ressourcen.
IntegritätUmfang: 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

  1. MITRE. "CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions." https://cwe.mitre.org/data/definitions/647.html

  2. OWASP. "Path Traversal."