{ "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":"Menü.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":"Menü.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":"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 jobs = new ArrayList<>();\n public void add(String job) { jobs.add(job); }\n public List 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 jobs() { return List.copyOf(jobs); }"," public List jobs() { return new ArrayList<>(); }"," public List jobs() { return null; }"," public List jobs() { jobs.clear(); return jobs; }"],"fixIndex":0,"fixedLine":" public List 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 einträge) {\n if (einträge == null && einträge.isEmpty()) {\n return \"Keine Einträge\";\n }\n return String.join(\"\\n\", einträge);\n }\n}"}, {"filename":"ExportController.java","content":"public class ExportController {\n public String ausliefern(String format, AuditExport export, java.util.List einträge) {\n if (format.equals(\"csv\")) {\n return export.export(einträge);\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 (einträge == null || einträge.isEmpty()) {"," if (einträge != null && einträge.isEmpty()) {"," if (einträge.isEmpty() || einträge == null) {"," if (einträge == null & einträge.isEmpty()) {"],"fixIndex":0,"fixedLine":" if (einträge == null || einträge.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":""}, {"filename":"auth.php","content":""} ], "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."} ] }, { "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 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":""}, {"filename":"ReportAccess.php","content":""}, {"filename":"ReportAudit.php","content":""} ], "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."} ] } ] }