80 lines
16 KiB
JSON
80 lines
16 KiB
JSON
{
|
||
"hunts": [
|
||
{
|
||
"slug": "falsche-rechnung", "title": "Fall #1: Die falsche Rechnung", "language": "Java", "difficulty": "junior", "examScope": "AP1",
|
||
"description": "Eine Rechnung liefert im letzten Monat einen falschen Wert.", "briefing": "Die Monatsauswertung soll alle zwölf Monate addieren. Das Ticket nennt nur einen fehlenden Dezemberwert.", "consoleOutput": "[report] Summe 2025: 11 Monate verarbeitet\n[warn] Erwartet: 12 Monatswerte",
|
||
"files": [{"filename":"Jahresreport.java","content":"public class Jahresreport {\n static int summe(int[] monate) {\n int total = 0;\n for (int i = 0; i <= monate.length; i++) {\n total += monate[i];\n }\n return total;\n }\n}"}],
|
||
"bugs": [{"points":100,"file":"Jahresreport.java","line":4,"category":"logik","evidence":"Die Schleife greift nach dem letzten Array-Element zu.","fixOptions":["for (int i = 0; i < monate.length; i++) {","for (int i = 1; i <= monate.length; i++) {","for (int i = 0; i <= monate.length - 1; i += 2) {","for (int i = monate.length; i >= 0; i--) {"],"fixIndex":0,"fixedLine":" for (int i = 0; i < monate.length; i++) {","explanation":"Bei Arrays ist der letzte gültige Index length - 1. Mit < length werden alle Elemente genau einmal verarbeitet."}]
|
||
},
|
||
{
|
||
"slug": "ampel-bleibt-rot", "title": "Fall #2: Die sture Ampel", "language": "Java", "difficulty": "junior", "examScope": "AP1",
|
||
"description": "Die Ampel zeigt immer Rot – selbst bei einer grünen Konfiguration.", "briefing": "Im Schulungsprojekt wird der Status aus einer Konfigurationsdatei gelesen.", "consoleOutput": "Status aus config: green\nUI-Ausgabe: ROT",
|
||
"files": [{"filename":"Ampel.java","content":"public class Ampel {\n static String anzeigen(String status) {\n if (status = \"green\") {\n return \"GRÜN\";\n }\n return \"ROT\";\n }\n}"}],
|
||
"bugs": [{"points":100,"file":"Ampel.java","line":3,"category":"syntax","evidence":"Der Vergleich verändert den Wert statt zwei Werte zu vergleichen.","fixOptions":["if (\"green\".equals(status)) {","if (status := \"green\") {","if (status.equals = \"green\") {","if (status =! \"green\") {"],"fixIndex":0,"fixedLine":" if (\"green\".equals(status)) {","explanation":"equals vergleicht Zeichenketten. Die Literalkonstante links verhindert zusätzlich eine NullPointerException."}]
|
||
},
|
||
{
|
||
"slug": "menue-fallt-durch", "title": "Fall #3: Das fallende Menü", "language": "Java", "difficulty": "junior", "examScope": "AP1",
|
||
"description": "Der Punkt ‚Status‘ öffnet unerwartet den Abmelde-Dialog.", "briefing": "Ein Azubi meldet einen Fehler im Konsolenmenü. Prüfe die Verzweigungen.", "consoleOutput": "Eingabe: 2\nStatus: OK\nAbmeldung gestartet",
|
||
"files": [{"filename":"Menue.java","content":"switch (auswahl) {\n case 1:\n System.out.println(\"Start\");\n break;\n case 2:\n System.out.println(\"Status: OK\");\n case 3:\n System.out.println(\"Abmeldung gestartet\");\n break;\n default:\n System.out.println(\"Ungültig\");\n}"}],
|
||
"bugs": [{"points":100,"file":"Menue.java","line":6,"category":"logik","evidence":"Nach dem Status-Fall wird der nächste case ebenfalls ausgeführt.","fixOptions":[" break;"," continue;"," return false;"," case 2: break;"],"fixIndex":0,"fixedLine":" break;","explanation":"Ohne break fällt ein switch in den nächsten Fall durch (fall-through)."}]
|
||
},
|
||
{
|
||
"slug": "absturz-um-mitternacht", "title": "Fall #4: Der Absturz um Mitternacht", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
|
||
"description": "Ein nächtlicher Job stürzt sporadisch ab.", "briefing": "Der Job läuft um 00:00 Uhr. Der Stacktrace zeigt auf die Formatierung des letzten Ereignisses.", "consoleOutput": "java.lang.NullPointerException: Cannot invoke \"String.toUpperCase()\"\n at Nachtjob.letztesEreignis(Nachtjob.java:5)\n at Nachtjob.main(Nachtjob.java:10)",
|
||
"files": [{"filename":"Nachtjob.java","content":"public class Nachtjob {\n static String letztesEreignis(String event) {\n if (event == null) {\n System.out.println(\"Kein Ereignis vorhanden\");\n }\n return event.toUpperCase();\n }\n\n public static void main(String[] args) {\n System.out.println(letztesEreignis(null));\n }\n}"}],
|
||
"bugs": [{"points":150,"file":"Nachtjob.java","line":5,"category":"laufzeit","evidence":"Der Stacktrace verweist direkt auf toUpperCase in Zeile 5.","fixOptions":[" return \"KEIN EREIGNIS\";"," event = event.toUpperCase();"," throw new NullPointerException();"," System.out.println(event.toUpperCase());"],"fixIndex":0,"fixedLine":" return \"KEIN EREIGNIS\";","explanation":"Nach der Null-Prüfung muss die Methode einen sicheren Ersatzwert zurückgeben, bevor event verwendet wird."}]
|
||
},
|
||
{
|
||
"slug": "offenes-login", "title": "Fall #5: Das offene Login", "language": "PHP", "difficulty": "senior", "examScope": "AP2_FIAE",
|
||
"description": "Ein Sicherheitscheck meldet eine manipulierbare Login-Abfrage.", "briefing": "Die Eingabe eines Benutzernamens landet direkt in der SQL-Abfrage. Finde die Schwachstelle und wähle den sicheren Fix.", "consoleOutput": "Scanner: CWE-89 SQL Injection\nTest-Eingabe: ' OR '1'='1",
|
||
"files": [{"filename":"login.php","content":"<?php\nfunction login($pdo, $name) {\n $sql = \"SELECT id FROM users WHERE name = '$name'\";\n return $pdo->query($sql)->fetch();\n}\n?>"}],
|
||
"bugs": [{"points":150,"file":"login.php","line":3,"category":"sicherheit","evidence":"Der externe Wert $name wird in einen SQL-String interpoliert.","fixOptions":[" $stmt = $pdo->prepare('SELECT id FROM users WHERE name = ?'); $stmt->execute([$name]);"," $name = addslashes($name); $sql = \"SELECT id FROM users WHERE name = '$name'\";"," $sql = \"SELECT id FROM users WHERE name LIKE '%$name%'\";"," $sql = \"SELECT id FROM users WHERE name = \" . $name;"],"fixIndex":0,"fixedLine":" $stmt = $pdo->prepare('SELECT id FROM users WHERE name = ?'); $stmt->execute([$name]);","explanation":"Prepared Statements trennen SQL-Code und Nutzdaten. Escaping allein ist kein zuverlässiger Schutz."}]
|
||
},
|
||
{
|
||
"slug": "geteilte-liste", "title": "Fall #6: Die geteilte Liste", "language": "Java", "difficulty": "principal", "examScope": "AP2_FIAE",
|
||
"description": "Parallele Worker verlieren gelegentlich Aufträge aus ihrer Queue.", "briefing": "Zwei Worker greifen gleichzeitig auf die gemeinsame Auftragsliste zu. Die Ursache liegt nicht dort, wo der Fehler sichtbar wird.", "consoleOutput": "Exception in thread worker-2 java.util.ConcurrentModificationException\n at java.base/java.util.ArrayList$Itr.next\n at Worker.run(Worker.java:6)",
|
||
"files": [
|
||
{"filename":"Worker.java","content":"public class Worker implements Runnable {\n private final Auftragsliste liste;\n public Worker(Auftragsliste liste) { this.liste = liste; }\n public void run() {\n for (String job : liste.jobs()) {\n System.out.println(job);\n }\n }\n}"},
|
||
{"filename":"Auftragsliste.java","content":"import java.util.*;\npublic class Auftragsliste {\n private final List<String> jobs = new ArrayList<>();\n public void add(String job) { jobs.add(job); }\n public List<String> jobs() { return jobs; }\n}"}
|
||
],
|
||
"bugs": [{"points":200,"file":"Auftragsliste.java","line":5,"category":"sicherheit","evidence":"Die interne, veränderbare Liste wird direkt an parallele Aufrufer zurückgegeben.","fixOptions":[" public List<String> jobs() { return List.copyOf(jobs); }"," public List<String> jobs() { return new ArrayList<>(); }"," public List<String> jobs() { return null; }"," public List<String> jobs() { jobs.clear(); return jobs; }"],"fixIndex":0,"fixedLine":" public List<String> jobs() { return List.copyOf(jobs); }","explanation":"Eine unveränderliche Momentaufnahme verhindert, dass externe Aufrufer die interne Liste gleichzeitig verändern. Für echte gemeinsame Mutation wäre zusätzlich eine threadsichere Datenstruktur nötig."}]
|
||
},
|
||
{
|
||
"slug": "archiv-unter-druck", "title": "Fall #7: Archiv unter Druck", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
|
||
"description": "Der tägliche Audit-Export bricht bei einem leeren Archiv ab und die CSV-Route stürzt nur in der Nachtverarbeitung ab.", "briefing": "Das Compliance-Team benötigt den Export vor 06:00 Uhr. Zwei Fehler liegen in getrennten Dateien; der Stacktrace nennt nur den Controller.", "consoleOutput": "java.lang.NullPointerException: Cannot invoke \"String.equals(Object)\"\n at ExportController.ausliefern(ExportController.java:4)\n[warn] Audit-Archiv enthält heute keine Einträge",
|
||
"files": [
|
||
{"filename":"AuditExport.java","content":"import java.util.List;\n\npublic class AuditExport {\n public String export(List<String> eintraege) {\n if (eintraege == null && eintraege.isEmpty()) {\n return \"Keine Einträge\";\n }\n return String.join(\"\\n\", eintraege);\n }\n}"},
|
||
{"filename":"ExportController.java","content":"public class ExportController {\n public String ausliefern(String format, AuditExport export, java.util.List<String> eintraege) {\n if (format.equals(\"csv\")) {\n return export.export(eintraege);\n }\n return \"Format nicht unterstützt\";\n }\n}"}
|
||
],
|
||
"bugs": [
|
||
{"points":150,"file":"AuditExport.java","line":5,"category":"laufzeit","evidence":"Bei null wird rechts von && trotzdem ein Methodenaufruf ausgeführt.","fixOptions":[" if (eintraege == null || eintraege.isEmpty()) {"," if (eintraege != null && eintraege.isEmpty()) {"," if (eintraege.isEmpty() || eintraege == null) {"," if (eintraege == null & eintraege.isEmpty()) {"],"fixIndex":0,"fixedLine":" if (eintraege == null || eintraege.isEmpty()) {","explanation":"Die ODER-Verknüpfung beendet die Auswertung sofort, wenn die Liste null ist. Danach kann isEmpty sicher geprüft werden."},
|
||
{"points":150,"file":"ExportController.java","line":3,"category":"laufzeit","evidence":"Der Stacktrace zeigt auf den Methodenaufruf equals; format kann aus einem optionalen Jobparameter stammen.","fixOptions":[" if (\"csv\".equals(format)) {"," if (format == \"csv\") {"," if (format.equalsIgnoreCase == \"csv\") {"," if (format != \"csv\") {"],"fixIndex":0,"fixedLine":" if (\"csv\".equals(format)) {","explanation":"Wird die feste Zeichenkette links verwendet, liefert equals bei einem null-Wert einfach false statt eine NullPointerException auszulösen."}
|
||
]
|
||
},
|
||
{
|
||
"slug": "buchung-im-doppelpack", "title": "Fall #8: Buchung im Doppelpack", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
|
||
"description": "Nach einem Retry erscheinen Bestellungen doppelt in der Buchhaltung; zusätzlich wird eine Nullzahlung nicht erkannt.", "briefing": "Ein Zahlungsanbieter sendet einen Retry, wenn die Antwort verzögert ist. Prüfe die Idempotenz und die Geldwert-Prüfung, bevor der Monatsabschluss startet.", "consoleOutput": "[payment] retry received for order A-1042\n[ledger] duplicate reference A-1042\n[warn] zero amount accepted",
|
||
"files": [
|
||
{"filename":"BestellungService.java","content":"public class BestellungService {\n private final BestellungRepository repository;\n public BestellungService(BestellungRepository repository) { this.repository = repository; }\n\n public void speichern(Bestellung bestellung) {\n if (repository.existsByReferenz(bestellung.referenz())) {\n repository.save(bestellung);\n }\n }\n}"},
|
||
{"filename":"ZahlungPruefung.java","content":"import java.math.BigDecimal;\n\npublic class ZahlungPruefung {\n static boolean istNullzahlung(BigDecimal betrag) {\n return betrag == BigDecimal.ZERO;\n }\n}"}
|
||
],
|
||
"bugs": [
|
||
{"points":150,"file":"BestellungService.java","line":6,"category":"logik","evidence":"Bei einer bereits bekannten Referenz wird die Bestellung erneut gespeichert.","fixOptions":[" if (!repository.existsByReferenz(bestellung.referenz())) {"," if (repository.existsByReferenz(bestellung.referenz()) == null) {"," if (repository.existsByReferenz(bestellung.referenz()) = false) {"," if (repository.existsByReferenz(bestellung.referenz()) || true) {"],"fixIndex":0,"fixedLine":" if (!repository.existsByReferenz(bestellung.referenz())) {","explanation":"Eine Referenz dient hier als Idempotenzschlüssel. Nur unbekannte Bestellungen dürfen den Speichervorgang auslösen."},
|
||
{"points":150,"file":"ZahlungPruefung.java","line":4,"category":"logik","evidence":"BigDecimal-Werte können denselben Betrag darstellen, ohne dass sie dasselbe Objekt sind.","fixOptions":[" return betrag != null && betrag.compareTo(BigDecimal.ZERO) == 0;"," return betrag.equals(BigDecimal.ZERO) || betrag == null;"," return betrag == new BigDecimal(\"0\");"," return betrag.toString() == \"0\";"],"fixIndex":0,"fixedLine":" return betrag != null && betrag.compareTo(BigDecimal.ZERO) == 0;","explanation":"compareTo vergleicht den numerischen Wert unabhängig von der Skala. Zusätzlich verhindert die Null-Prüfung einen Absturz."}
|
||
]
|
||
},
|
||
{
|
||
"slug": "protokoll-ohne-schutz", "title": "Fall #9: Protokoll ohne Schutz", "language": "PHP", "difficulty": "principal", "examScope": "AP2_FIAE",
|
||
"description": "Ein Security-Audit findet Zugangsdaten im Log und eine unsichere Passwortprüfung im internen API-Gateway.", "briefing": "Das Incident-Team hat einen Logauszug gesichert. Finde beide Schwachstellen, ohne die Fehlersuche selbst unbrauchbar zu machen.", "consoleOutput": "[api] Token: eyJhbGciOiJIUzI1NiJ9...\nSecurity finding: CWE-532 Sensitive Information in Log Files\nSecurity finding: CWE-256 Plaintext Storage of a Password",
|
||
"files": [
|
||
{"filename":"api.php","content":"<?php\nfunction requestApi(string $token, string $path): void {\n error_log('API request ' . $path . ' with token ' . $token);\n // Anfrage senden\n}\n?>"},
|
||
{"filename":"auth.php","content":"<?php\nfunction anmelden(array $user, string $passwort): bool {\n return $user['password'] == $passwort;\n}\n?>"}
|
||
],
|
||
"bugs": [
|
||
{"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."}
|
||
]
|
||
}
|
||
]
|
||
}
|