Zum Inhalt

Modul: Weiterbildung

Stand

Code-Analyse vom 2026-04-22 gegen sachpool.de, WebEdition 9.2.3, PHP 8.3.

Zweck

Versicherungsvermittler müssen nach §34d Abs. 9 GewO jährlich 15 Stunden berufliche Weiterbildung absolvieren (IHK-Pflicht). Das Modul:

  • Verwaltet Weiterbildungs-Nachweise pro Vermittler
  • Nimmt Datei-Uploads entgegen (PDF/DOC/JPG, als BLOB in MySQL!)
  • Generiert IHK-konforme PDF-Nachweise (FPDF) nach § 34d Abs. 9 GewO + § 7 Abs. 1 VersVermV
  • Dashboard-Widget mit aktuellem Stundenstand

Template-Struktur

sachpool-portal/weiterbildung/
├── frontend/
│   ├── weiterbildungskonto.tmpl              (249)  Haupt-UI (EasyUI-Datagrid)
│   ├── weiterbildungs-funktionen.tmpl        (252)  AJAX-Endpoint (9 Actions)
│   └── fpdf.tmpl                             (253)  FPDF-Library-Wrapper
├── backend/
│   ├── weiterbildungen-verwalten.tmpl        (240)  Redakteurs-UI
│   └── weiterbildungs-admin-download.tmpl    (273)
└── tools/
    ├── available-functions.tmpl              (214)
    ├── weiterbildung-migration.tmpl          (212)
    └── weiterbildung-verwaiste-dokumente.tmpl (213)

Einstiegspunkt

Doc-ID Pfad Zweck
/_login/weiterbildung/weiterbildungskonto.php Haupt-UI (Template 249)
40158 /_login/weiterbildung/funktionen.php AJAX-Endpoint (Template 252)
40184 FPDF-Library (Dokument-Include)

Zusätzlich: Dashboard-Widget „Weiterbildungskonto: 0 h 0 min" verlinkt direkt auf die Hauptseite.

Technische Besonderheit: EasyUI-Frontend

Im Gegensatz zum Rest des Portals (Bootstrap-4) verwendet dieses Modul jQuery EasyUI (datebox, datagrid, dialog, linkbutton, filebox).

Assets: - easyui.css (ID 40161) - icon.css (40163), color.css (40160) - jquery.easyui.min.js (40164) - easyui-lang-de.js (40162) — deutsche Lokalisierung

→ inkonsistent mit Rest der Portal-UI, siehe HANDLUNGSEMPFEHLUNGEN.md.

Datenmodell

Tabelle Weiterbildung

Feld Typ Rolle
id int PK Eintrag-ID
datum DATE Datum der Weiterbildung
anbieter varchar Name des Anbieters
thema varchar Thema
minuten int Dauer in Minuten
benutzer varchar Login-Name
benutzer_id int FK auf tblWebUser.id
ip varchar IP-Adresse bei Erfassung

Tabelle Weiterbildung_datei

Feld Typ Rolle
id int PK
weiterbildung_id int FK Referenz auf Weiterbildung.id
fname varchar Originaldateiname
dokument BLOB Dateiinhalt binär in MySQL (!)

Dateien als BLOB in MySQL

Uploads (PDF, DOC, JPG bis beliebige Größe) werden als BLOB in MySQL gespeichert. Keine Größenbegrenzung, kein MIME-Check. → Skalierungs- und Performance-Thema.

AJAX-Actions (Template 252)

Das Endpoint unterstützt 9 Actions via ?action=:

Action Methode Zweck
data GET JSON-Antwort für EasyUI-Datagrid (Liste + Jahressumme)
open GET HTML-Fragment: Bearbeiten-/Neu-Formular
save POST INSERT oder UPDATE (mit optionalem File-Upload)
delete GET DELETE auf Weiterbildung + Weiterbildung_datei
down GET Datei-Download (streamt BLOB mit Content-Type)
pdf GET IHK-Nachweis-PDF (FPDF) für Zeitraum ?ab=YYYYMMDD&bis=YYYYMMDD
askdruck GET HTML-Fragment: Jahresauswahl-Formular (Spinner 2017 – aktuelles Jahr)
konto GET Formular für Mail-Benachrichtigung (Feature unvollständig — speichert nichts)
test GET Debug-Action (Dead-Code, nicht im Produktiv-Flow genutzt)

PDF-Export (IHK-Nachweis)

action=pdf&benutzer_id=<id>&ab=YYYYMMDD&bis=YYYYMMDD

FPDF-Subklasse generiert Nachweis-PDF mit:

  • Name, Firma, Adresse (aus tblWebUser Kontakt_*-Feldern)
  • IHK-Registernummer
  • Tabelle aller Weiterbildungen im Zeitraum: Thema + Datum/Dauer + Anbieter
  • Bestätigungsformel nach § 34d Abs. 9 Satz 2 GewO i. V. m. § 7 Abs. 1 VersVermV
  • gez.-Zeile

Limitierungen: - thema auf 35 Zeichen gekürzt, anbieter auf 25 Zeichen - Mehrzeilen über explode("@", ...) — fragil, sollte wordWrap nutzen - utf8_decode()-basiert (FPDF kann kein UTF-8) — Umlaute über ISO-8859-1

Backend-Tools

weiterbildungen-verwalten.tmpl (240)

Redakteurs-UI für Sachpool-Admins: Gesamtübersicht aller Vermittler-Weiterbildungen, massenweises Anlegen per Dateiablage, plus einzelne Bearbeitung/Löschung.

Workflow — Massen-Import:

  1. Redakteur legt PDFs im Import-Ordner (<we:listdir id="39846">) ab.
  2. Dateiname-Konvention: <Vermittler-Kennung>_<Beschreibung>.pdf (z. B. 90100-11_IDD-Schulung-Q1.pdf). Der Teil vor dem _ wird als Vermittler-Kennung interpretiert und gegen tblWebUser.Username aufgelöst.
  3. Redakteur trägt Metadaten (Datum, Anbieter, Bezeichnung, Dauer) im Template-Formular ein und submitet.
  4. Pro Datei entsteht je ein Weiterbildung- und ein Weiterbildung_datei-Eintrag. Originaldatei wird als BLOB in dokument abgelegt und aus dem Import-Ordner per we_delete() gelöscht.
  5. Einzelne Einträge können danach über Detail-Dialoge editiert oder gelöscht werden — Bestätigung per Eintippen von „OK".

SQL-Queries (alle Template 240):

Zweck Query
Hauptlistung SELECT * FROM Weiterbildung ORDER BY datum DESC
Kennung → User-ID SELECT ID FROM tblWebUser WHERE Username='<kennung>'
Import-Insert INSERT INTO Weiterbildung (datum, anbieter, thema, minuten, benutzer_id, benutzer, geaendert) VALUES (...)
Datei-Insert INSERT INTO Weiterbildung_datei (weiterbildung_id, fname, dokument) VALUE (...)
Detail-Join SELECT ... FROM Weiterbildung t1 JOIN Weiterbildung_datei t2 ON t1.id=... AND t1.id = t2.weiterbildung_id
Update UPDATE Weiterbildung SET datum=..., anbieter=..., thema=..., minuten=..., benutzer=... WHERE id=...
Delete DELETE FROM Weiterbildung WHERE id=... (kein Cascade auf Weiterbildung_datei!)

Auth-Schutz: Nur <we:ifWebEdition> — prüft ausschließlich, ob irgendein WebEdition-Login besteht. Keine Rollen-/Verantwortungsbereichs-Prüfung.

Auffälligkeiten:

  • UPDATE schreibt benutzer (Anzeigename) ohne benutzer_id — Zuordnung kann auseinanderlaufen.
  • DELETE hinterlässt Waisen-Einträge in Weiterbildung_datei (kein CASCADE).
  • setInterval($(".weBtnSave").hide(), 500) blendet WebEdition-Save-Buttons alle 500 ms aus (UI-Hack).
  • SachPoolDB-Singleton wird 7× per getInstance() geholt und 7× close()-aufgerufen (harmlos, verwirrend).

→ Kritische Security-Funde (hartkodiertes Basic-Auth-Passwort, SQL-Injection im Import, Stored XSS in Hauptansicht): siehe HANDLUNGSEMPFEHLUNGEN.md.

weiterbildungs-admin-download.tmpl (273)

Admin-Download einzelner Nachweis-PDFs über GET-Parameter record_id.

$user_check = true;  // IMMER true — keine Besitzprüfung
if ($user_check) { /* Download-Logik */ }
DELETE FROM Weiterbildung WHERE id = $_GET['record_id'];
DELETE FROM Weiterbildung_datei WHERE id = $_GET['record_id'];  // Bug: sollte weiterbildung_id sein

Kritisch

$user_check = true ohne Vergleich → IDOR. Jeder Redakteur / jeder mit Session kann jede Datei herunterladen und löschen. Der DELETE trifft zusätzlich das falsche Feld (id statt weiterbildung_id) → inkonsistente Datenbereinigung. Siehe HANDLUNGSEMPFEHLUNGEN.md.

Tools-Ordner

  • weiterbildung-migration.tmpl (212) — einmalige Altdaten-Migration aus FastpublishCMS
  • weiterbildung-verwaiste-dokumente.tmpl (213) — findet Weiterbildung_datei-Einträge ohne zugehörige Weiterbildung (DB-Konsistenz-Check)
  • available-functions.tmpl (214) — Helper-Inventar

Siehe auch

Bekannte Altlasten

→ interne Datei HANDLUNGSEMPFEHLUNGEN.md im Repo-Root

  • 🔴🔴🔴 Flächendeckende IDOR im AJAX-Endpoint (fast alle Actions akzeptieren record_id/id ohne Besitzprüfung)
  • 🔴🔴🔴 Template 273 $user_check = true — Admin-Download ohne Besitzprüfung
  • 🔴🔴🔴 Template 240 hartkodiertes Basic-Auth-Passwort (vy7a1NATn93Ukbm0ypFd) im Klartext
  • 🔴 Template 240 SQL-Injection im Import-INSERT (POST-Werte ungeescaped)
  • 🔴 Template 240 Stored XSS in Hauptansicht (DB-Werte ungeescaped in HTML)
  • 🔴 SQL-Injection über $_REQUEST['sort']/$_REQUEST['order'] direkt in ORDER BY (Template 252)
  • 🔴 Bug action=delete: DELETE aus Weiterbildung_datei WHERE id=... statt WHERE weiterbildung_id=... — Datei-Leichen in DB
  • 🟡 Datei-Uploads als BLOB in MySQL (keine Größenbegrenzung, kein MIME-Check)
  • 🟡 EasyUI statt Bootstrap — UI-Inkonsistenz zum Rest des Portals
  • 🟡 15 DB-Instanzen ($wedb1..$wedb14, $wedb10b) — Singleton-Antipattern
  • 🟡 Template 240 UPDATE ohne benutzer_id-Sync — Anzeige und FK können auseinanderlaufen
  • 🟡 Template 240 DELETE ohne Cascade auf Weiterbildung_datei
  • 🟢 Dead-Code action=test, unvollständige action=konto
  • 🟢 Magic Number 2017 als Startjahr im PDF-Jahr-Spinner
  • 🟢 Template 214 (available-functions.tmpl) enthält var_dump($_SESSION["user"]["ID"]) im Produktivcode