Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Content\UpdateManager;
/**
* Deckt die Pfad-Schranken des Updaters ab.
*
* Der Updater schreibt Dateien, die von einem entfernten Dienst stammen, in das
* Projektverzeichnis. Zwei Zusagen hängen daran: Kein gelieferter Pfad darf aus
* dem Verzeichnis herausführen, und die Daten des Betreibers (Konfiguration,
* Uploads, Speicher) dürfen ein Update nie überschreiben.
*/
return static function (): void {
/** Ruft die private Prüfung auf geschützte Pfade auf. */
$geschuetzt = static function (string $pfad): bool {
$m = new ReflectionMethod(UpdateManager::class, 'isProtectedPath');
$m->setAccessible(true);
return (bool)$m->invoke((new ReflectionClass(UpdateManager::class))->newInstanceWithoutConstructor(), $pfad);
};
/** Ruft die Pfadprüfung auf, die stageFiles() und downloadAndApplyFiles() nutzen. */
$zulaessig = static function (string $pfad): bool {
$m = new ReflectionMethod(UpdateManager::class, 'isAcceptableRemotePath');
$m->setAccessible(true);
return (bool)$m->invoke((new ReflectionClass(UpdateManager::class))->newInstanceWithoutConstructor(), $pfad);
};
// ----------------------------------------------------------------
// Pfad-Traversal
// ----------------------------------------------------------------
test('Pfade, die aus dem Projektverzeichnis führen, werden verworfen', function () use ($zulaessig) {
foreach ([
'../../etc/passwd',
'src/../../../etc/passwd',
'/etc/passwd',
'src\\Core\\Application.php',
"src/Core/App\0.php",
'src/Core/App;rm.php',
'src/Core/App .php',
] as $pfad) {
assertFalse($zulaessig($pfad), 'Angenommen: ' . $pfad);
}
});
test('Gewöhnliche Projektpfade bleiben zulässig', function () use ($zulaessig) {
foreach ([
'src/Core/Application.php',
'templates/admin/update/index.php',
'public/assets/css/app.css',
'database/migrations/204_audit_field_anchor.sql',
'bin/run-update.php',
'README.md',
] as $pfad) {
assertTrue($zulaessig($pfad), 'Verworfen: ' . $pfad);
}
});
// ----------------------------------------------------------------
// Daten des Betreibers
// ----------------------------------------------------------------
test('Konfiguration, Speicher und Uploads werden nie überschrieben', function () use ($geschuetzt) {
foreach ([
'config/app.php',
'config/runtime.php',
'.env',
'storage/uploads/vault-icons/x.png',
'public/uploads/logo/logo.png',
'vendor/autoload.php',
'composer.lock',
'.git/config',
] as $pfad) {
assertTrue($geschuetzt($pfad), 'Nicht geschützt: ' . $pfad);
}
});
test('Der Programmcode wird sehr wohl aktualisiert', function () use ($geschuetzt) {
foreach ([
'src/Core/Application.php',
'templates/tickets/index.php',
'public/index.php',
'public/assets/js/orders.js',
'database/migrations/205_encrypted_secrets_at_rest.sql',
] as $pfad) {
assertFalse($geschuetzt($pfad), 'Fälschlich geschützt: ' . $pfad);
}
});
test('Ein Verzeichnis mit gleichem Namensanfang ist nicht mitgeschützt', function () use ($geschuetzt) {
// `storage/` ist geschützt – `storages/` und `config-example/` sind
// eigenständige Pfade und dürfen nicht davon zehren.
assertFalse($geschuetzt('storages/datei.php'));
assertFalse($geschuetzt('config-example/app.php'));
assertFalse($geschuetzt('public/uploads-doc/hinweis.md'));
});
test('Der Schutz gilt auch bei umgekehrten Schrägstrichen', function () use ($geschuetzt) {
// Die Prüfung normalisiert den Trenner, bevor sie vergleicht.
assertTrue($geschuetzt('config\\app.php'));
assertTrue($geschuetzt('storage\\uploads\\x.png'));
});
// ----------------------------------------------------------------
// Ausnahmen
// ----------------------------------------------------------------
test('Der Ausführungsschutz im Upload-Verzeichnis wird mitgeliefert', function () use ($geschuetzt) {
// Einzige Ausnahme vom Schutz des Upload-Verzeichnisses: Die .htaccess
// gehört zum Auslieferungsumfang und muss aktualisierbar bleiben.
assertFalse($geschuetzt('public/uploads/.htaccess'));
assertTrue($geschuetzt('public/uploads/beliebig.png'));
});
test('Entwicklungs-Hilfsdateien werden nicht ausgeliefert', function () use ($geschuetzt) {
foreach (['CLAUDE.md', 'AGENTS.md', '.cursorrules', '.claude/settings.json'] as $pfad) {
assertTrue($geschuetzt($pfad), 'Würde ausgeliefert: ' . $pfad);
}
});
};