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
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Informationsoffenlegung Hooks können auf sensible Daten zugreifen. |
| Integrität | Bereich: Datenkorruption Hook-Rückgabewerte können Zustand korrumpieren. |
| Authentifizierung | Bereich: 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
- MITRE. "CWE-622: Improper Validation of Function Hook Arguments." https://cwe.mitre.org/data/definitions/622.html
- Plugin-Sicherheits-Best-Practices-Dokumentation.