fix(releases): Lizenzschluessel nicht mehr im Klartext, Selbsttest, Zielorte
Vier Befunde aus einer externen Durchsicht der 2.4-Integration.
1. Die .htpasswd war eine Klartext-Kundenliste
Das htpasswd-Format hasht nur die Passwortspalte. Benutzername UND Passwort
waren der Lizenzschluessel - der Schluessel stand also im Klartext direkt
neben seinem eigenen bcrypt-Hash, und der Hash war Dekoration. Geschuetzt
hat das Ganze nur die FilesMatch-Regel in derselben Datei.
Der Benutzername wird jetzt abgeleitet: lic_<sha256(schluessel), 16 Hex>.
Die Datei enthaelt damit nur noch eine Einwegableitung und einen Hash ueber
einen hochentropen Schluessel.
Server und SDK muessen dabei zeichengenau uebereinstimmen; ein Test prueft
die C#-Ableitung gegen die PHP-Formel.
2. Ein Formatwechsel blieb unbemerkt liegen
Beim Umbau auf 1. faellt auf: reconcile() sah keinen Anlass zur
Neuerzeugung, die Dateien behielten das alte Format, waehrend die Clients
bereits das neue schickten. Die erzeugten Dateien tragen deshalb jetzt eine
Formatkennung; weicht sie ab, wird neu erzeugt.
3. Doku beschrieb Nginx, der Schutz ist Apache-only
.htaccess wird von Nginx ignoriert - dort waeren die Verzeichnisse offen und
die .htpasswd oeffentlich abrufbar. Die Statusanzeige pruefte nur, ob die
Dateien existieren, und haette in dem Fall "GESCHUETZT" gemeldet.
Neu: ein echter Selbsttest ruft die eigene Paket-Adresse OHNE Zugangsdaten
ab und erwartet 401. Er laeuft beim manuellen Erzeugen und nach jeder
automatischen Neuerzeugung; das Ergebnis steht in der Oberflaeche, ein
Fehlschlag im Log. Er findet nebenbei auch abgeschaltetes AllowOverride und
Tippfehler in der erzeugten Datei. Doku korrigiert, Nginx-Vorlage ergaenzt.
4. Erstinstallation schrieb an einen Ort, an dem Linux-Anwendungen nicht lesen
setup.json-Ziele waren immer installationsrelativ. Eine Anwendung, die sich
unter Linux richtig verhaelt, liest aus $XDG_CONFIG_HOME - /opt/<app> ist
fuer den Dienstbenutzer meist nicht schreibbar. Der Installer legte die
Datei also dorthin, wo nie jemand nachsieht.
Ziele haben jetzt ein "location": install (Vorgabe), config, data, home,
plus ${VAR}- und %VAR%-Ersetzung in "file". Unbekannte Variablen bleiben
stehen statt leer zu werden - ein Platzhalter faellt auf, ein falscher Pfad
nicht. Der Installer gibt den aufgeloesten Pfad aus, weil bei config das
Konto entscheidet, unter dem er laeuft.
Ausserdem
- Doku zeigte "status": "ok" fuer update/delete; Http::ok() erzeugt
"status": "success".
- UPGRADE §16.1 deckte Neuprodukte nicht ab: Fuer ein Produkt ohne Release
existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis
entsteht erst mit dem ersten Upload, der naechste Tick schuetzt es. Der erste
ausgelieferte Build muss die Zugangsdaten also schon mitbringen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ceb977187e
commit
7a3a5dad69
+52
-1
@@ -1268,6 +1268,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
case 'regenerate_release_guard': {
|
||||
$guard = ReleaseGuard::regenerateAll($pdo);
|
||||
|
||||
// Direkt gegenpruefen, ob der Schutz tatsaechlich greift.
|
||||
// Vorhandene Dateien beweisen nichts - unter Nginx werden sie
|
||||
// ignoriert, und die Oberflaeche meldete trotzdem Erfolg.
|
||||
$test = ReleaseGuard::selfTest($pdo);
|
||||
|
||||
if ($test['failed'] !== []) {
|
||||
$reasons = array_map(
|
||||
static fn(array $f): string => ($f['slug'] ?? '?') . ': ' . ($f['reason'] ?? ''),
|
||||
$test['failed']
|
||||
);
|
||||
dc_flash(
|
||||
'Zugangsschutz erzeugt, aber der Selbsttest schlaegt fehl! ' . e(implode(' | ', $reasons)),
|
||||
'danger'
|
||||
);
|
||||
dc_redirect('#sub-update-guard');
|
||||
}
|
||||
|
||||
if ($guard['errors'] !== []) {
|
||||
dc_flash(
|
||||
'Zugangsschutz teilweise erneuert. Fehler: ' . e(implode(' | ', $guard['errors'])),
|
||||
@@ -1275,9 +1292,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
);
|
||||
} else {
|
||||
dc_flash(sprintf(
|
||||
'Zugangsschutz erneuert: %d Produkt(e), %d Zugang/Zugaenge.%s',
|
||||
'Zugangsschutz erneuert und geprueft: %d Produkt(e), %d Zugang/Zugaenge, '
|
||||
. '%d Adresse(n) antworten ohne Zugangsdaten korrekt mit 401.%s',
|
||||
$guard['projects'],
|
||||
$guard['entries'],
|
||||
$test['ok'],
|
||||
$guard['skipped'] !== []
|
||||
? ' Ohne Zugaenge und damit gesperrt: ' . e(implode(', ', $guard['skipped'])) . '.'
|
||||
: ''
|
||||
@@ -1704,6 +1723,10 @@ $usersSchemaReady = $users === [] || array_key_exists('role', $users[0]);
|
||||
// Zustand des Zugangsschutzes der Release-Verzeichnisse.
|
||||
$releaseGuard = ReleaseGuard::status();
|
||||
|
||||
// Ergebnis des letzten Selbsttests. Bewusst nur gelesen: ein HTTP-Abruf je
|
||||
// Produkt bei jedem Seitenaufbau waere Verschwendung.
|
||||
$releaseGuardTest = ReleaseGuard::lastSelfTest($pdo);
|
||||
|
||||
// Installer-Downloads. Die Binaries liegen unter /installer/ im Webroot und
|
||||
// werden nicht von deploy.py uebertragen, sondern von
|
||||
// scripts/upload_installer.py - der Quelltext des Agenten gehoert nicht auf
|
||||
@@ -3189,6 +3212,34 @@ Antwort:
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ($releaseGuardTest !== null): ?>
|
||||
<?php $testFailed = !empty($releaseGuardTest['failed']); ?>
|
||||
<div class="alert" style="margin-top:1rem; <?= $testFailed
|
||||
? 'background:rgba(255,70,70,0.12); border:1px solid rgba(255,70,70,0.35); color:#fecaca;'
|
||||
: 'background:rgba(60,200,120,0.12); border:1px solid rgba(60,200,120,0.3); color:#bbf7d0;' ?>">
|
||||
<strong>Selbsttest vom <?= e((string)($releaseGuardTest['ran_at'] ?? '?')) ?> UTC:</strong>
|
||||
<?= (int)($releaseGuardTest['ok'] ?? 0) ?> von
|
||||
<?= (int)($releaseGuardTest['checked'] ?? 0) ?> Adresse(n) antworten ohne
|
||||
Zugangsdaten korrekt mit 401.
|
||||
<?php if ($testFailed): ?>
|
||||
<ul style="margin:0.5rem 0 0 1rem;">
|
||||
<?php foreach ($releaseGuardTest['failed'] as $f): ?>
|
||||
<li><strong><?= e((string)($f['slug'] ?? '?')) ?>:</strong> <?= e((string)($f['reason'] ?? '')) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.8rem; margin-top:1rem;">
|
||||
<strong>Der Selbsttest ist die einzige belastbare Aussage.</strong> Dass die
|
||||
Dateien vorhanden sind, beweist nichts: Unter Nginx werden
|
||||
<code>.htaccess</code>-Dateien vollständig ignoriert — die Verzeichnisse wären
|
||||
dann offen und die <code>.htpasswd</code> öffentlich abrufbar, während hier
|
||||
„GESCHÜTZT“ stünde. Der Test ruft die eigene Paket-Adresse ohne Zugangsdaten
|
||||
ab und erwartet 401.
|
||||
</p>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.8rem; margin-top:1rem;">
|
||||
<strong>OFFEN</strong> heißt: jeder im Internet kann die Pakete laden.
|
||||
<strong>GESPERRT</strong> heißt: niemand kommt heran, weil es weder gültige
|
||||
|
||||
Reference in New Issue
Block a user