Add BugHunter cases by difficulty
mindflux-ci / build (push) Successful in 22s

This commit is contained in:
AliceDarkest
2026-07-13 21:40:12 +02:00
parent 0f08c93a67
commit 6bde27a241
6 changed files with 47 additions and 5 deletions
+38
View File
@@ -74,6 +74,44 @@
{"points":200,"file":"api.php","line":3,"category":"sicherheit","evidence":"Der vollständige Bearer-Token wird in eine dauerhaft gespeicherte Logzeile übernommen.","fixOptions":[" error_log('API request ' . $path . ' with token [redigiert]');"," error_log('API request ' . $path . ' with token ' . substr($token, 0));"," error_log($token);"," error_log(base64_encode($token));"],"fixIndex":0,"fixedLine":" error_log('API request ' . $path . ' with token [redigiert]');","explanation":"Authentifizierungsdaten gehören nie in Logs. Für die Diagnose reichen Pfad, Request-ID und ein klarer Hinweis auf einen vorhandenen Token."},
{"points":200,"file":"auth.php","line":3,"category":"sicherheit","evidence":"Das Passwort wird direkt mit einem gespeicherten Wert verglichen; sichere Passwort-Hashes benötigen eine spezialisierte Prüfung.","fixOptions":[" return password_verify($passwort, $user['password_hash']);"," return md5($passwort) == $user['password'];"," return $user['password'] === $passwort;"," return strlen($passwort) > 0;"],"fixIndex":0,"fixedLine":" return password_verify($passwort, $user['password_hash']);","explanation":"password_verify prüft einen Passwort-Hash inklusive Salt und Algorithmus sicher. Klartextvergleiche und MD5 sind dafür ungeeignet."}
]
},
{
"slug": "rabatt-mit-nebenwirkung", "title": "Fall #10: Rabatt mit Nebenwirkung", "language": "Java", "difficulty": "junior", "examScope": "AP1",
"description": "Im Campus-Shop erhalten Studierende keinen Rabatt, während die Kasse bei manchen Warenkörben einen Artikel zu viel zählt.", "briefing": "Vor dem Start der Prüfungswoche wurde ein neues Kassenterminal ausgerollt. Der Support meldet zwei widersprüchliche Beobachtungen: Der Rabatt erscheint bei einigen Testkonten nicht, und die Tagesstatistik weicht bei einem Warenkorb mit drei Artikeln um genau eins ab. Die Fachabteilung möchte eine kleine, nachvollziehbare Korrektur statt eines Umbaus der Kasse.", "consoleOutput": "[checkout] Kundengruppe: STUDENT\n[checkout] Rabatt angewendet: false\n[stats] Warenkorb W-17: erwartet 3 Artikel, gezählt 4",
"files": [
{"filename":"Kundenprofil.java","content":"public class Kundenprofil {\n static boolean hatStudentenrabatt(String gruppe) {\n return gruppe == \"STUDENT\";\n }\n\n static int rabattProzent(String gruppe) {\n return hatStudentenrabatt(gruppe) ? 10 : 0;\n }\n}"},
{"filename":"Warenkorb.java","content":"import java.util.List;\n\npublic class Warenkorb {\n static int artikelAnzahl(List<String> artikel) {\n int anzahl = 0;\n for (int i = 0; i <= artikel.size(); i++) {\n anzahl++;\n }\n return anzahl;\n }\n}"}
],
"bugs": [
{"points":100,"file":"Kundenprofil.java","line":3,"category":"logik","evidence":"Die Gruppe kommt aus dem Profilservice und kann denselben Text in einem anderen String-Objekt enthalten.","fixOptions":[" return \"STUDENT\".equals(gruppe);"," return gruppe = \"STUDENT\";"," return gruppe.equalsIgnoreCase == \"STUDENT\";"," return gruppe != \"STUDENT\";"],"fixIndex":0,"fixedLine":" return \"STUDENT\".equals(gruppe);","explanation":"Bei Zeichenketten vergleicht == die Objektidentität, nicht den Inhalt. equals prüft den Text; die Konstante links bleibt auch bei einer fehlenden Gruppe sicher."},
{"points":100,"file":"Warenkorb.java","line":6,"category":"logik","evidence":"Die Statistik zählt pro Schleifendurchlauf. Bei drei Artikeln wird die Schleife viermal durchlaufen.","fixOptions":[" for (int i = 0; i < artikel.size(); i++) {"," for (int i = 1; i <= artikel.size(); i++) {"," for (int i = 0; i <= artikel.size() - 1; i += 2) {"," for (int i = artikel.size(); i >= 0; i--) {"],"fixIndex":0,"fixedLine":" for (int i = 0; i < artikel.size(); i++) {","explanation":"Die Anzahl der Durchläufe reicht von Index 0 bis vor die Größe der Liste. Mit < artikel.size() wird jeder Artikel genau einmal gezählt."}
]
},
{
"slug": "freigabe-vor-pruefung", "title": "Fall #11: Freigabe vor Prüfung", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
"description": "Ein bereits abgelaufener Release wird im Deployment-Portal als freigegeben angezeigt, und ein API-Schlüssel taucht im Diagnoseprotokoll auf.", "briefing": "Das Release-Team stoppte ein Hotfix-Deployment wenige Minuten vor dem Rollout. Die Oberfläche zeigte trotz abgelaufener Freigabe ein grünes Licht. Parallel hat das Security-Team in einem zentralen Log einen produktiven API-Schlüssel entdeckt. Untersuche die Entscheidungslogik und die Diagnoseausgabe; beide Fehler liegen in verschiedenen Komponenten und dürfen nicht mit einem bloßen Workaround kaschiert werden.", "consoleOutput": "[release] Status: GEPRUEFT\n[release] Freigabe gültig bis: 2026-07-12T18:00:00Z\n[release] Bewertung um 2026-07-13T08:14:00Z: freigegeben\n[audit] deployment payments-api mit key sk_live_...",
"files": [
{"filename":"DeploymentFreigabe.java","content":"import java.time.Instant;\n\npublic class DeploymentFreigabe {\n static boolean darfAusrollen(Release release, Instant jetzt) {\n return release.freigabeBis().isAfter(jetzt)\n && release.status() == \"GEPRUEFT\";\n }\n}\n"},
{"filename":"Release.java","content":"import java.time.Instant;\n\npublic record Release(String name, String status, Instant freigabeBis) {\n}\n"},
{"filename":"AuditLog.java","content":"public class AuditLog {\n static void deploymentStart(String releaseName, String apiKey) {\n System.out.println(\"deployment \" + releaseName + \" mit key \" + apiKey);\n }\n}"}
],
"bugs": [
{"points":150,"file":"DeploymentFreigabe.java","line":6,"category":"logik","evidence":"Der Zeitvergleich ist korrekt; der Status stammt jedoch aus einer Datenbankabfrage und wird als String geliefert.","fixOptions":[" && \"GEPRUEFT\".equals(release.status());"," && release.status() = \"GEPRUEFT\";"," && release.status().equals = \"GEPRUEFT\";"," && release.status() != \"GEPRUEFT\";"],"fixIndex":0,"fixedLine":" && \"GEPRUEFT\".equals(release.status());","explanation":"Der Freigabestatus muss inhaltlich verglichen werden. Mit == kann derselbe Text je nach Herkunft fälschlich als verschieden behandelt werden."},
{"points":150,"file":"AuditLog.java","line":3,"category":"sicherheit","evidence":"Der Schlüssel ist ein Credential. Zentrale Logs sind für Betriebsteams einsehbar und oft länger gespeichert als die eigentliche Sitzung.","fixOptions":[" System.out.println(\"deployment \" + releaseName + \" mit key [redigiert]\");"," System.out.println(\"deployment \" + releaseName + \" mit key \" + apiKey.substring(0));"," System.out.println(apiKey);"," System.out.println(\"deployment \" + releaseName + \" mit key \" + java.util.Base64.getEncoder().encodeToString(apiKey.getBytes()));"],"fixIndex":0,"fixedLine":" System.out.println(\"deployment \" + releaseName + \" mit key [redigiert]\");","explanation":"API-Schlüssel dürfen nicht protokolliert werden – auch nicht kodiert oder vollständig abgeschnitten. Für die Diagnose reichen Release-Name, Zeit und eine Request-ID."}
]
},
{
"slug": "bericht-mit-fremdzugriff", "title": "Fall #12: Bericht mit Fremdzugriff", "language": "PHP", "difficulty": "principal", "examScope": "AP2_FIAE",
"description": "Ein Mandantenbericht lässt sich über einen manipulierten Dateinamen außerhalb des Archivbereichs aufrufen; außerdem prüft die Berechtigung den Mandanten nicht.", "briefing": "Ein externer Penetrationstest hat zwei Befunde kombiniert: Eine angemeldete Person kann einen ungewöhnlichen Berichtsnamen an den Download-Endpunkt senden und die Zugriffsprüfung bestätigt nur eine allgemeine Rolle. Im Incident-Log steht eine Anfrage, die auf einen fremden Mandanten zeigt. Die Lösung muss Eingaben begrenzen und den Zugriff an den tatsächlich angeforderten Kunden binden, ohne den normalen Monatsreport zu beschädigen.", "consoleOutput": "GET /reports/download?bericht=../../kunden/K-204/umsatz\n[report] Datei angefordert: ../../kunden/K-204/umsatz\n[auth] user U-88, role REPORT_VIEWER, angeforderter Kunde K-204\nSecurity finding: CWE-22 Path Traversal\nSecurity finding: CWE-639 Authorization Bypass Through User-Controlled Key",
"files": [
{"filename":"ReportController.php","content":"<?php\nfunction downloadReport(): string {\n $name = $_GET['bericht'] ?? '';\n $file = REPORT_DIR . '/' . $name . '.pdf';\n if (!is_file($file)) {\n throw new RuntimeException('Bericht nicht gefunden');\n }\n return file_get_contents($file);\n}\n?>"},
{"filename":"ReportAccess.php","content":"<?php\nfunction darfLesen(array $user, string $kundeId): bool {\n return $user['role'] === 'REPORT_VIEWER';\n}\n?>"},
{"filename":"ReportAudit.php","content":"<?php\nfunction protokolliereDownload(string $userId, string $kundeId, string $bericht): void {\n error_log(\"report-download user=$userId kunde=$kundeId bericht=$bericht\");\n}\n?>"}
],
"bugs": [
{"points":200,"file":"ReportController.php","line":3,"category":"sicherheit","evidence":"Der Dateiname kommt direkt aus dem Query-Parameter. Die anschließende Dateiprüfung verhindert keinen Pfadwechsel aus REPORT_DIR heraus.","fixOptions":[" $name = $_GET['bericht'] ?? ''; if (!preg_match('/^[A-Z0-9_-]+$/', $name)) { throw new InvalidArgumentException('Ungültiger Bericht'); }"," $name = str_replace('../', '', $_GET['bericht'] ?? '');"," $name = base64_encode($_GET['bericht'] ?? '');"," $name = $_GET['bericht'] ?? ''; chmod(REPORT_DIR, 0777);"],"fixIndex":0,"fixedLine":" $name = $_GET['bericht'] ?? ''; if (!preg_match('/^[A-Z0-9_-]+$/', $name)) { throw new InvalidArgumentException('Ungültiger Bericht'); }","explanation":"Eine Positivliste erlaubt nur die erwarteten Berichtsschlüssel. Das bloße Entfernen einzelner Zeichenfolgen lässt Varianten von Pfadmanipulationen offen."},
{"points":200,"file":"ReportAccess.php","line":3,"category":"sicherheit","evidence":"Die Rolle erlaubt das Lesen von Berichten, aber sie beweist nicht, dass der angeforderte Kunde zum Benutzer gehört.","fixOptions":[" return $user['role'] === 'REPORT_VIEWER' && in_array($kundeId, $user['kundenIds'], true);"," return $user['role'] === 'REPORT_VIEWER' || $kundeId !== '';"," return $user['role'] == 'REPORT_VIEWER' && $kundeId == $kundeId;"," return strlen($kundeId) > 0;"],"fixIndex":0,"fixedLine":" return $user['role'] === 'REPORT_VIEWER' && in_array($kundeId, $user['kundenIds'], true);","explanation":"Autorisierung braucht sowohl die passende Rolle als auch eine Mandantenbindung. Der strikte Vergleich in in_array verhindert dabei ungewollte Typumwandlungen."}
]
}
]
}
@@ -11,4 +11,5 @@ body[class*="mf-a11y-skin-"] .mf-a11y-panel{border-color:color-mix(in srgb,var(-
body[class*="mf-a11y-skin-"] .mf-a11y-head{background:color-mix(in srgb,var(--mf-a11y-accent) 28%,#07121f)}
body[class*="mf-a11y-skin-"] .mf-a11y-close{background:var(--mf-a11y-accent);color:var(--mf-a11y-ink)}
body[class*="mf-a11y-skin-"] .mf-a11y-option[aria-pressed="true"]{border-color:var(--mf-a11y-accent);background:color-mix(in srgb,var(--mf-a11y-accent) 18%,var(--surface));box-shadow:inset 0 0 0 2px var(--mf-a11y-accent)}
.bh-cabinet-level{grid-column:1/-1;margin:.5rem 0 -.15rem;padding-bottom:.35rem;border-bottom:1px solid #60473d;color:var(--bh-accent);font:700 .66rem "Courier New",monospace;letter-spacing:.12em}
@media(max-width:700px){.mf-a11y-panel{inset:70px 6px auto 6px!important;width:auto;max-height:calc(100dvh - 82px)}}
+2 -2
View File
@@ -12,8 +12,8 @@
function renderCatalog() {
if (!catalog.length) { error(new Error('Zurzeit sind keine Fallakten verfügbar.')); return; }
selectedCase = Math.max(0, Math.min(selectedCase, catalog.length - 1));
const selectedHunt = catalog[selectedCase];
view.innerHTML = `<section class="bh-hero"><span class="bh-stamp">VERTRAULICH</span><span class="bh-kicker">DEBUG-FALLAKTEN</span><h1>Welche Spur verfolgst du?</h1><p>Öffne eine Akte, lies die Beweise und sichere deinen Befund Schritt für Schritt. Jede Akte bleibt so lange offen, wie du sie brauchst.</p></section><section class="bh-archive"><div class="bh-archive-head"><div><span class="bh-kicker">AKTENSCHRANK · ${catalog.length} AKTEN</span><h2>Fallarchiv</h2></div><p>Wähle eine Schublade, um die Akte zu prüfen.</p></div><div class="bh-cabinet" aria-label="Fallakten"><div class="bh-cabinet-label">BUGHUNTER · ARCHIV</div>${catalog.map((hunt, i) => `<button class="bh-drawer ${i === selectedCase ? 'active' : ''}" data-select="${i}" ${i === selectedCase ? 'aria-pressed="true"' : 'aria-pressed="false"'}><span class="bh-drawer-handle"></span><span class="bh-case-no">${esc(hunt.examScope)} · ${esc(hunt.difficulty).toUpperCase()}</span><strong>${esc(hunt.title)}</strong><small>${hunt.bugCount} Bug${hunt.bugCount === 1 ? '' : 's'} · ${hunt.fileCount} ${hunt.fileCount === 1 ? 'Datei' : 'Dateien'}</small></button>`).join('')}</div><article class="bh-case-detail"><span class="bh-stamp">AUSGEWÄHLTE AKTE</span><span class="bh-kicker">${esc(selectedHunt.examScope)} · ${esc(selectedHunt.difficulty).toUpperCase()} · ${esc(selectedHunt.language)}</span><h2>${esc(selectedHunt.title)}</h2><p>${esc(selectedHunt.description)}</p><div class="bh-tags"><span>${selectedHunt.fileCount} ${selectedHunt.fileCount === 1 ? 'Datei' : 'Dateien'}</span><span>${selectedHunt.bugCount} Bug${selectedHunt.bugCount === 1 ? '' : 's'}</span></div><button class="bh-btn primary" data-start="${esc(selectedHunt.slug)}">AKTE ÖFFNEN</button></article></section>`;
const selectedHunt = catalog[selectedCase], difficultyNames = {junior:'JUNIOR · GRUNDLAGEN', senior:'SENIOR · PRAXIS', principal:'PRINCIPAL · KOMPLEXFÄLLE'}, drawers = ['junior','senior','principal'].map(level => { const cases = catalog.map((hunt, i) => ({hunt, i})).filter(entry => entry.hunt.difficulty === level); if (!cases.length) return ''; return `<div class="bh-cabinet-level">${difficultyNames[level]}</div>${cases.map(({hunt, i}) => `<button class="bh-drawer ${i === selectedCase ? 'active' : ''}" data-select="${i}" ${i === selectedCase ? 'aria-pressed="true"' : 'aria-pressed="false"'}><span class="bh-drawer-handle"></span><span class="bh-case-no">${esc(hunt.examScope)} · ${esc(hunt.difficulty).toUpperCase()}</span><strong>${esc(hunt.title)}</strong><small>${hunt.bugCount} Bug${hunt.bugCount === 1 ? '' : 's'} · ${hunt.fileCount} ${hunt.fileCount === 1 ? 'Datei' : 'Dateien'}</small></button>`).join('')}`).join('');
view.innerHTML = `<section class="bh-hero"><span class="bh-stamp">VERTRAULICH</span><span class="bh-kicker">DEBUG-FALLAKTEN</span><h1>Welche Spur verfolgst du?</h1><p>Öffne eine Akte, lies die Beweise und sichere deinen Befund Schritt für Schritt. Jede Akte bleibt so lange offen, wie du sie brauchst.</p></section><section class="bh-archive"><div class="bh-archive-head"><div><span class="bh-kicker">AKTENSCHRANK · ${catalog.length} AKTEN</span><h2>Fallarchiv</h2></div><p>Nach Schwierigkeit sortiert: Grundlagen, Praxis, Komplexfälle.</p></div><div class="bh-cabinet" aria-label="Fallakten nach Schwierigkeit"><div class="bh-cabinet-label">BUGHUNTER · ARCHIV</div>${drawers}</div><article class="bh-case-detail"><span class="bh-stamp">AUSGEWÄHLTE AKTE</span><span class="bh-kicker">${esc(selectedHunt.examScope)} · ${esc(selectedHunt.difficulty).toUpperCase()} · ${esc(selectedHunt.language)}</span><h2>${esc(selectedHunt.title)}</h2><p>${esc(selectedHunt.description)}</p><div class="bh-tags"><span>${selectedHunt.fileCount} ${selectedHunt.fileCount === 1 ? 'Datei' : 'Dateien'}</span><span>${selectedHunt.bugCount} Bug${selectedHunt.bugCount === 1 ? '' : 's'}</span></div><button class="bh-btn primary" data-start="${esc(selectedHunt.slug)}">AKTE ÖFFNEN</button></article></section>`;
view.querySelectorAll('[data-select]').forEach(drawer => drawer.addEventListener('click', () => { selectedCase = Number(drawer.dataset.select); renderCatalog(); }));
view.querySelector('[data-start]')?.addEventListener('click', event => begin(event.currentTarget.dataset.start));
}
+1 -1
View File
@@ -32,7 +32,7 @@
document.head.appendChild(accessibilityCss);
var accessibilityPlacementCss = document.createElement('link');
accessibilityPlacementCss.rel = 'stylesheet';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=3';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=4';
accessibilityPlacementCss.setAttribute('data-accessibility', '');
document.head.appendChild(accessibilityPlacementCss);
}
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>BugHunter · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/bughunter.css?v=2"><script src="/js/theme.js"></script></head><body><main class="bh-app"><header class="bh-top"><a href="/dashboard" class="bh-back">← Dashboard</a><div class="bh-brand"><b>BUGHUNTER</b><span>DEBUG-FALLAKTEN</span></div><span class="mf-a11y-host"></span></header><section id="bhView" class="bh-view"><div class="bh-loading">FALLAKTEN WERDEN GEÖFFNET …</div></section></main><script src="/js/bughunter.js?v=2"></script></body></html>
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>BugHunter · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/bughunter.css?v=2"><script src="/js/theme.js"></script></head><body><main class="bh-app"><header class="bh-top"><a href="/dashboard" class="bh-back">← Dashboard</a><div class="bh-brand"><b>BUGHUNTER</b><span>DEBUG-FALLAKTEN</span></div><span class="mf-a11y-host"></span></header><section id="bhView" class="bh-view"><div class="bh-loading">FALLAKTEN WERDEN GEÖFFNET …</div></section></main><script src="/js/bughunter.js?v=3"></script></body></html>