Zum Inhalt

Modul: GDV-Daten / Abrechnungen / Courtage

Stand

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

Zweck

Dieses Modul stellt drei eng verwandte Datendienste für Vermittler bereit:

  • GDV-Daten — maschinenlesbare Vertragsdaten (GDV-Format, tab-separierte Textdateien + ZIP) für Import in Makler-Software
  • Beitragsinformationen — PDF-Dokumente aus manuellen Nachbearbeitungen
  • Courtageabrechnungen — monatliche Courtagelisten als PDF

Alle drei teilen die gleiche Datenbank (abrechnungen_u_gdv), den gleichen Download-Endpoint und die gleiche Reminder-Pipeline.

Template-Struktur

sachpool-portal/gdv-abrechnungen-courtage-daten/
├── anzeige/
│   ├── anzeige-gdvdaten.tmpl                  (202)  Liste GDV/ZIP-Dateien
│   ├── anzeige-beitragsdaten-abrechnungen.tmpl (216)  Liste PDF-Nachbearbeitungen
│   └── anzeige-courtagelisten.tmpl            (134)  Link zur Courtageübersicht
├── cron/
│   ├── benachrichtung/   (271)   Reminder-Cron (sic! Tippfehler im Ordnernamen)
│   └── parser-datenstand/ (270)   Parser-Cron für eingehende Salia-Lieferungen
├── downloadprotection/
│   └── protected-serve-file.tmpl              (203)  Zentraler Download-Endpoint
└── mail/
    ├── _inc/                                 (263)
    ├── mail-gemeinsame-signatur.tmpl          (267)  Sachpool-Impressum
    ├── mail-reminder-beitragsdaten-abrechnung.tmpl (205)
    ├── mail-reminder-courtageabrechnung.tmpl  (266)
    └── mail-reminder-gdv-daten.tmpl           (265)

Einstiegspunkte (Frontend)

Doc-ID Pfad Zeigt Template
547 /_login/meine-daten/gdv-daten.php GDV-Dateien-Liste 202
39508 /_login/meine-daten/beitragsinformationen.php Nachbearbeitungs-PDFs 216
566 /_login/meine-daten/courtageliste.php Courtagelisten-Link 134
14294 /_login/_gdv-daten/ → Rewrite auf Template 203 Download-Endpoint 203

Datenmodell: abrechnungen_u_gdv

Zentrale Tabelle für alle drei Datentypen. Aus Code ableitbare Felder:

Feld Typ Rolle
id int PK Datensatz-ID
user varchar Vermittlernummer (entspricht $sach_user / $benutzer)
dateiname varchar Dateiname im Storage-Ordner
extension varchar gdv, zip, pdf — steuert die Anzeige-Seite
kategorie varchar u. a. Nachbearbeitung (für Beitragsinfos)
erstelldatum varchar Datum als String TT.MM.JJJJnicht DATE, wird per substr() geparst
groesse int Dateigröße in Bytes
lastdownload varchar Zeitstempel letzter Download als String; leer wenn nie heruntergeladen

User-Feld-Variante mit Trailing-Underscore

Die Anzeige-Queries haben einen Fallback, wenn unter user='$benutzer' nichts gefunden wird: user='$benutzer_' (mit angehängtem _). Historische Namens-Konvention — Details unklar, siehe OFFENE-FRAGEN.md.

Physische Dateien

Download-Verzeichnisse unter /_login/:

ID Pfad Inhalt
2767 /_login/_gdv-daten/ GDV-Dateien + Unterordner Nachbearbeitungen/ (für PDF-Nachbearbeitungen, historisch gewachsen)
16667 /_login/_beitragsinformationen/ PDFs (Beitragsdaten-Nachbearbeitungen)
1657 /_login/_courtagelisten-pdf/ Courtagelisten-PDFs

Alle Ordner haben published = 0 → direkter HTTP-Zugriff gesperrt.

Anzeige-Templates (gemeinsames Muster)

anzeige-gdvdaten.tmpl (202) und anzeige-beitragsdaten-abrechnungen.tmpl (216) haben nahezu identischen Code. Unterschiede:

Aspekt GDV (202) Beitragsinfos (216)
WHERE-Klausel extension='gdv' OR extension='zip' extension='pdf' AND kategorie='Nachbearbeitung'
Storage-Pfad /_login/_gdv-daten/ /_login/_beitragsinformationen/
Tabellen-Header „zur GDV-Datei" „Datei"

Ablauf pro Zeile:

  1. SELECT alle Datensätze des Users (Fallback mit user_ wenn leer)
  2. File-Existenz-Check auf Dateisystem (file_exists())
  3. Wenn Datei fehlt: auto-DELETE aus DB (self-healing)
  4. Wenn Datei da + nie heruntergeladen: Zeile rosa hinterlegt (#efbac6)
  5. Table-Sorter-Output mit Filter, Pager, Download-Icon
SELECT * FROM abrechnungen_u_gdv
WHERE user='$benutzer'
  AND (extension='gdv' OR extension='zip')   -- für GDV
  -- ODER
  -- AND extension='pdf' AND kategorie='Nachbearbeitung'  -- für Beitragsinfos
ORDER BY id DESC;

Letzte Aktualisierung: Maximum-Datum über alle Datensätze in abrechnungen_u_gdv (nicht nur eigene) — für gemeinsame Aktualisierungs-Anzeige im Footer der Tabelle.

anzeige-courtagelisten.tmpl (134) — abweichendes Muster

Zeigt nur einen Link zur Courtageliste. Courtagestufe des Users (aus Session-Feld Courtage, umgesetzt zu $sach_courtage) filtert in Tabelle courtagelisten:

SELECT dateiname, version FROM courtagelisten
WHERE courtage = '$sach_courtage'

Bei Treffer: Link auf {weDownloadLink}?courtage=1 (→ Download-Endpoint).

Download-Endpoint: protected-serve-file.tmpl (203)

Dokument-ID 14294, Template 203. Zwei Modi je nach Parameter:

Modus 1: ?courtage=1

Holt die aktuellste Courtagelisten-Version passend zu $sach_courtage, streamt sie.

Modus 2: ?id=<fileid> (GDV oder Beitragsinfos)

$sql = "SELECT dateiname, kategorie FROM abrechnungen_u_gdv WHERE id='$fileid'";
// ...
if ($kategorie == "Nachbearbeitung") {
    $file = "_beitragsinformationen/" . $dateiname;
} else {
    $file = "_gdv-daten/" . $dateiname;
}

// KRITISCHE Zugriffsprüfung:
if (strpos($file, (string)$sach_user) !== false) {
    Download($internal_path . $web_path . $file);
}

Schutz-Pipeline (3 Walls)

  1. WebEdition-Login (Workspace 536 erforderlich)
  2. .htaccess DENY ALL in den _gdv-daten/, _beitragsinformationen/, _courtagelisten-pdf/-Verzeichnissen — direkter Browser-Zugriff nicht möglich
  3. Vermittlerkennung im Dateinamen: strpos($file, $sach_user) prüft, dass die Datei die Kennung des Users enthält

Security: IDOR-Vektor in Wall 3

strpos !== false entspricht „enthält als Substring", nicht „gehört zu User". Bei Vermittler 90100-11 würde eine Datei dateifuer_90100-111.pdf fälschlich durchgelassen. → interne Datei HANDLUNGSEMPFEHLUNGEN.md

Download-Streaming

Eigene Download()-Funktion mit:

  • HTTP 206 Partial Content bei Range-Headern (Resume-fähig, gute Praxis!)
  • Cache-Control: public, no-cache
  • 1024 KB/s Default-Rate-Limit ($speed)
  • set_time_limit(0) + session_write_close() für lange Downloads
  • connection_status() === CONNECTION_NORMAL in Schleife, saubere Cleanup-Semantik

Nebenwirkung: UPDATE lastdownload

Bei erfolgreichem GDV/Beitragsinfo-Download:

UPDATE abrechnungen_u_gdv SET lastdownload='TT.MM.JJJJ HH:MM' WHERE id='$fileid'
Darum verschwindet die rosa Markierung auf der Anzeigeseite nach dem ersten Download.

Parser-Cron (270)

Verarbeitet eingehende Salia-Lieferungen. Details des Parser-Templates werden in einer späteren Runde nachgezogen — siehe interne Datei OFFENE-FRAGEN.md.

Reminder-Mails

Pro Datentyp ein eigenes Template + gemeinsame Signatur (mail-gemeinsame-signatur.tmpl, ID 267) mit Sachpool-Impressum + rechtliche Standard-Fußzeile.

Die gemeinsame Signatur enthält u. a.: - HRB 12171 – Chemnitz - Geschäftsführer: R. André Klotz - Steuer-Nr.: 218/118/04018 - IHK-Reg.Nr: D-0CXQ-33EW8-28

Bei Änderung dieser Firmendaten müssen alle Mail-Templates aktualisiert werden.

Bekannte Altlasten

→ interne Datei HANDLUNGSEMPFEHLUNGEN.md im Repo-Root

  • 🔴🔴🔴 IDOR im protected-serve-file (Substring-Check)
  • 🔴 SQL-Konkatenation in allen Anzeige- und Serve-Templates
  • 🟢 Tippfehler-Ordner benachrichtung statt benachrichtigung
  • 🟡 Download nicht transaktionssicher (historisch)
  • 🟡 Datum als String-Feld in DB, substr()-Parsing für Sortierung
  • 🟡 Sachpool-Firmenangaben hartkodiert in mail-gemeinsame-signatur.tmpl

Siehe auch