Unzureichende Validierung von Funktions-Hook-Argumenten

Beschreibung

Unzureichende Validierung von Funktions-Hook-Argumenten tritt auf, wenn eine Anwendung externem Code erlaubt, Hooks oder Callbacks zu registrieren, aber die an oder von diesen Hooks übergebenen Argumente nicht ordnungsgemäß validiert. Dies ist häufig in Plugin-Systemen, ereignisgesteuerten Architekturen und erweiterbaren Frameworks. Bösartige Hooks können sensible Daten empfangen, auf die sie keinen Zugriff haben sollten, oder manipulierte Werte zurückgeben, die den Anwendungszustand korrumpieren.

Risiko

Hooks können sensible Daten im Transit abfangen und modifizieren. Bösartige Plugins können Privilegien durch Hook-Manipulation eskalieren. Rückgabewerte von Hooks können Sicherheitsprüfungen korrumpieren. Hooks können Informationen an unbefugte Parteien leaken. Denial of Service durch Hooks, die hängen oder abstürzen. Supply-Chain-Angriffe durch kompromittierte Plugins.

Lösung

Validieren Sie alle an Hooks übergebenen Daten. Bereinigen Sie Rückgabewerte von Hooks. Implementieren Sie ein Hook-Berechtigungssystem. Verwenden Sie typisierte Schnittstellen für Hook-Verträge. Sandbox-Hook-Ausführung, wo möglich. Auditieren und verifizieren Sie Plugin-Quellen. Beschränken Sie Hook-Zugriff auf nur notwendige Daten.

Häufige Auswirkungen

AuswirkungDetails
VertraulichkeitBereich: Informationsoffenlegung

Hooks können auf sensible Daten zugreifen.
IntegritätBereich: Datenkorruption

Hook-Rückgabewerte können Zustand korrumpieren.
AuthentifizierungBereich: Bypass

Hooks können Authentifizierungsfluss modifizieren.

Beispielcode + Lösungscode

Verwundbarer Code

<?php
// VERWUNDBAR: Hooks empfangen sensible Daten
class VulnerableUserService {
    private array $hooks = [];

    public function registerHook(string $event, callable $callback): void {
        $this->hooks[$event][] = $callback;
    }

    public function authenticate(string $username, string $password): bool {
        // Hooks empfangen Klartext-Passwort!
        $this->triggerHook('before_auth', [
            'username' => $username,
            'password' => $password  // Sensibel!
        ]);

        $result = $this->doAuth($username, $password);

        // Hooks können Ergebnis modifizieren
        $this->triggerHook('after_auth', [
            'result' => &$result,  // Veränderbare Referenz!
            'user' => $this->getUser($username)
        ]);

        return $result;
    }

    private function triggerHook(string $event, array $args): void {
        foreach ($this->hooks[$event] ?? [] as $callback) {
            $callback($args);  // Keine Validierung!
        }
    }
}

// Bösartiges Plugin
$service->registerHook('before_auth', function($args) {
    // Stiehlt Passwort
    file_put_contents('/tmp/passwords.txt',
        $args['username'] . ':' . $args['password'] . "\n",
        FILE_APPEND);
});

$service->registerHook('after_auth', function(&$args) {
    // Gewährt immer Zugriff
    $args['result'] = true;
});
?>
# VERWUNDBAR: Django-Signal mit sensiblen Daten
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_in

@receiver(user_logged_in)
def vulnerable_login_handler(sender, request, user, **kwargs):
    # Jedes Plugin kann registrieren und erhält:
    # - Vollständiges User-Objekt
    # - Request mit Session
    # - Alle Login-Details
    pass

# VERWUNDBAR: Flask-Hook mit modifizierbarem Kontext
from flask import Flask, g

app = Flask(__name__)

@app.before_request
def vulnerable_before_request():
    # Plugins können g (Anwendungskontext) modifizieren
    # was alle nachfolgende Verarbeitung beeinflusst
    pass

# Bösartiges Plugin setzt:
# g.user = admin_user  # Privilegien-Eskalation
// VERWUNDBAR: Event-System ohne Validierung
class VulnerableEventSystem {
    constructor() {
        this.listeners = {};
    }

    on(event, callback) {
        this.listeners[event] = this.listeners[event] || [];
        this.listeners[event].push(callback);
    }

    emit(event, data) {
        const listeners = this.listeners[event] || [];

        // Jeder Listener kann Daten modifizieren
        for (const listener of listeners) {
            listener(data);  // Daten per Referenz übergeben
        }

        return data;
    }
}

// Verwendung
const events = new VulnerableEventSystem();

// Bösartiges Plugin
events.on('user.authenticate', (data) => {
    // Authentifizierungsergebnis modifizieren
    data.authenticated = true;
    data.role = 'admin';
});

// Anwendungscode
const result = events.emit('user.authenticate', {
    username: 'attacker',
    authenticated: false
});

// result.authenticated ist jetzt true!

Lösungscode

<?php
// SICHER: Hooks mit validierten Argumenten
class SafeUserService {
    private array $hooks = [];
    private array $hookPermissions = [];

    public function registerHook(
        string $event,
        callable $callback,
        string $pluginId
    ): void {
        // Verifizieren, dass Plugin für diesen Hook autorisiert ist
        if (!$this->canAccessHook($pluginId, $event)) {
            throw new UnauthorizedException("Plugin kann nicht auf $event zugreifen");
        }

        $this->hooks[$event][] = [
            'callback' => $callback,
            'plugin' => $pluginId
        ];
    }

    public function authenticate(string $username, string $password): bool {
        // Hooks empfangen nur notwendige, nicht-sensible Daten
        $hookData = new AuthHookData(
            username: $username,
            timestamp: time()
            // KEIN Passwort übergeben!
        );

        $this->triggerHook('before_auth', $hookData);

        $result = $this->doAuth($username, $password);

        // Ergebnis-Hooks empfangen unveränderliche Daten
        $resultData = new AuthResultData(
            username: $username,
            success: $result,
            timestamp: time()
        );

        // Hooks können Ergebnis nicht direkt modifizieren
        $this->triggerHook('after_auth', $resultData);

        return $result;  // Ursprüngliches Ergebnis unverändert
    }

    private function triggerHook(string $event, object $data): void {
        foreach ($this->hooks[$event] ?? [] as $hook) {
            try {
                // Daten klonen um Modifikation zu verhindern
                $hookData = clone $data;

                // Mit Timeout ausführen
                $this->executeWithTimeout(
                    fn() => $hook['callback']($hookData),
                    timeout: 1000
                );
            } catch (Throwable $e) {
                // Protokollieren aber Hook darf Anwendung nicht crashen lassen
                $this->logHookError($hook['plugin'], $event, $e);
            }
        }
    }
}

// Unveränderliche Hook-Datenklassen
readonly class AuthHookData {
    public function __construct(
        public string $username,
        public int $timestamp
    ) {}
}

readonly class AuthResultData {
    public function __construct(
        public string $username,
        public bool $success,
        public int $timestamp
    ) {}
}
?>
# SICHER: Django-Signale mit gefilterten Daten
from django.dispatch import Signal

# Signal mit dokumentiertem Datenvertrag definieren
user_authenticated = Signal()  # Liefert: user_id, timestamp nur

class SafeAuthBackend:
    def authenticate(self, request, username, password):
        user = self._do_auth(username, password)

        if user:
            # Nur sichere, nicht-sensible Daten senden
            user_authenticated.send(
                sender=self.__class__,
                user_id=user.id,  # Nicht vollständiges User-Objekt
                timestamp=timezone.now()
                # KEIN request, KEINE session, KEIN password
            )

        return user

# SICHER: Flask mit validierten Hooks
from flask import Flask, g
from functools import wraps

app = Flask(__name__)

class HookRegistry:
    def __init__(self):
        self.hooks = {}
        self.validators = {}

    def register(self, event, validator=None):
        def decorator(f):
            self.hooks.setdefault(event, []).append(f)
            return f
        return decorator

    def trigger(self, event, data):
        # Unveränderliche Kopie erstellen
        safe_data = self._sanitize(event, data)

        for hook in self.hooks.get(event, []):
            try:
                # Hooks können keine Werte zurückgeben, die Flow beeinflussen
                hook(safe_data)
            except Exception as e:
                app.logger.error(f"Hook-Fehler: {e}")

    def _sanitize(self, event, data):
        # Sensible Felder entfernen
        sanitized = {k: v for k, v in data.items()
                    if k not in ['password', 'token', 'secret']}
        return types.MappingProxyType(sanitized)  # Unveränderlich
// SICHER: Event-System mit Validierung
class SafeEventSystem {
    constructor() {
        this.listeners = new Map();
        this.schemas = new Map();
    }

    // Erwartetes Datenschema für Event definieren
    defineEvent(event, schema) {
        this.schemas.set(event, schema);
    }

    on(event, callback, pluginId) {
        // Plugin-Berechtigungen verifizieren
        if (!this.canListen(pluginId, event)) {
            throw new Error(`Plugin ${pluginId} kann nicht auf ${event} hören`);
        }

        const listeners = this.listeners.get(event) || [];
        listeners.push({ callback, pluginId });
        this.listeners.set(event, listeners);
    }

    emit(event, data) {
        // Daten gegen Schema validieren
        const schema = this.schemas.get(event);
        if (schema) {
            data = this.validateAndSanitize(data, schema);
        }

        // Unveränderliche Kopie erstellen
        const immutableData = Object.freeze({ ...data });

        const listeners = this.listeners.get(event) || [];

        for (const { callback, pluginId } of listeners) {
            try {
                // Kopie übergeben, nicht Referenz
                callback({ ...immutableData });
            } catch (error) {
                console.error(`Hook-Fehler in ${pluginId}:`, error);
            }
        }

        // Ursprüngliche Daten unverändert zurückgeben
        return data;
    }

    validateAndSanitize(data, schema) {
        const sanitized = {};

        for (const [key, type] of Object.entries(schema)) {
            if (key in data && typeof data[key] === type) {
                sanitized[key] = data[key];
            }
        }

        return sanitized;
    }
}

// Verwendung
const events = new SafeEventSystem();

events.defineEvent('user.authenticate', {
    username: 'string',
    timestamp: 'number'
    // Kein password-Feld erlaubt
});

events.emit('user.authenticate', {
    username: 'user',
    timestamp: Date.now(),
    password: 'secret'  // Wird entfernt
});

Ausgenutzt in der Praxis

WordPress-Plugin-Schwachstellen

Bösartige Plugins nutzten Hook-System für Datendiebstahl aus.

Authentifizierungs-Bypass

Hooks, die Auth-Ergebnisse modifizieren könnten, führten zu Bypasses.

Supply-Chain-Angriffe

Kompromittierte Pakete registrierten bösartige Hooks.


Tools zum Testen/Ausnutzen

  • Plugin-Sicherheitsscanner.
  • Statische Analyse für Hook-Schwachstellen.
  • Dynamisches Testen von Event-Systemen.

CVE-Beispiele

  • CVEs von Plugin-/Hook-Schwachstellen in CMS-Plattformen.
  • Authentifizierungs-Bypass durch Hook-Manipulation.

Referenzen

  1. MITRE. "CWE-622: Improper Validation of Function Hook Arguments." https://cwe.mitre.org/data/definitions/622.html
  2. Plugin-Sicherheits-Best-Practices-Dokumentation.