From 8930d7804a945be8bbac37f7834db79c5b4b5d20 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Sun, 19 Apr 2026 11:58:14 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Audit-Findings=20=E2=80=94=20en=5FUS-Par?= =?UTF-8?q?ity,=20LIKE-Escape,=20Rate-Limit=20[deploy]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus /dolibarr audit Report: - en_US: 5 fehlende Tab-/Placeholder-Keys ergänzt (Parity 100%) - LIKE-Wildcard-Escape: %, _ und \\ im User-Input werden maskiert bevor in LIKE '%..%'-Pattern eingebaut (sonst matched "100%" zu viel) Betrifft auftraege_listen (Z.75-77) und kunden_suchen (Z.124) - Rate-Limit: File-basiert nach KB #354 — max. 10 fehlgeschlagene Token- Checks pro IP in 15 Minuten, dann 429. Bei Erfolg wird Zähler resettet. Zähler liegen unter DOL_DATA_ROOT/eplan/loginattempts/.json Co-Authored-By: Claude Opus 4.7 (1M context) --- ajax/pwa_api.php | 12 ++++++++++-- langs/en_US/eplan.lang | 7 +++++++ lib/eplan_token.lib.php | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index b71e7bb..eb7584a 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -36,12 +36,19 @@ header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); // --- Auth --- +if (!eplanRateLimit()) { + http_response_code(429); + echo json_encode(['error' => 'Too Many Requests', 'detail' => 'Zu viele fehlgeschlagene Versuche. Warte 15 Minuten.']); + exit; +} $token = eplanTokenAusRequest(); if (!eplanTokenPruefen($token)) { + eplanRateLimit(false); // Fehlversuch zählen http_response_code(401); echo json_encode(['error' => 'Unauthorized', 'detail' => 'Ungültiger oder fehlender Token']); exit; } +eplanRateLimit(true); // Erfolg → Zähler zurücksetzen if (!isModEnabled('eplan')) { http_response_code(503); @@ -71,7 +78,8 @@ try { LEFT JOIN ".MAIN_DB_PREFIX."societe t ON t.rowid = c.fk_soc WHERE c.entity IN (".getEntity('commande').")"; if (!empty($such)) { - $such_esc = $db->escape($such); + // LIKE-Wildcards im User-Input escapen, dann erst SQL-escape + $such_esc = $db->escape(str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $such)); $sql .= " AND (c.ref LIKE '%".$such_esc."%' OR c.ref_client LIKE '%".$such_esc."%' OR t.nom LIKE '%".$such_esc."%')"; @@ -118,7 +126,7 @@ try { case 'kunden_suchen': $such = GETPOST('such', 'alphanohtml'); if (empty($such) || strlen($such) < 2) { echo json_encode([]); break; } - $such_esc = $db->escape($such); + $such_esc = $db->escape(str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $such)); $sql = "SELECT rowid as id, nom, code_client FROM ".MAIN_DB_PREFIX."societe WHERE entity IN (".getEntity('societe').") AND (nom LIKE '%".$such_esc."%' OR code_client LIKE '%".$such_esc."%') diff --git a/langs/en_US/eplan.lang b/langs/en_US/eplan.lang index 80c7f95..17e4cc7 100644 --- a/langs/en_US/eplan.lang +++ b/langs/en_US/eplan.lang @@ -39,3 +39,10 @@ EplanFeature2 = Bluetooth laser (Bosch GLM) for precise measurements EplanFeature3 = Offline capable — works without internet EplanFeature4 = PDF and DXF export (CAD compatible) EplanFeature5 = Measure multiple rooms per project + +# Tabs +Aufmaß = Measurement +EplanStartAufmass = Start measurement +EplanKeinePwa = ElektroPlan PWA URL not configured. +EplanKeineAufmasse = No measurements linked yet. Start a new measurement via the button above. +EplanKeineAuftraege = No orders for this customer. diff --git a/lib/eplan_token.lib.php b/lib/eplan_token.lib.php index 7e5e7f7..516d92f 100644 --- a/lib/eplan_token.lib.php +++ b/lib/eplan_token.lib.php @@ -61,3 +61,39 @@ function eplanTokenAusRequest() if (!empty($header)) return $header; return $_GET['token'] ?? $_POST['token'] ?? null; } + +/** + * File-basierter Brute-Force-Schutz (KB #354): + * Pro IP maximal 10 fehlgeschlagene Versuche in 15 Minuten, danach blockiert. + * Bei Erfolg wird der Zähler zurückgesetzt. + * @return bool true wenn Request erlaubt, false wenn geblockt + */ +function eplanRateLimit($erfolg = null) +{ + global $conf; + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + $dir = DOL_DATA_ROOT.'/eplan/loginattempts'; + if (!is_dir($dir)) @mkdir($dir, 0755, true); + $datei = $dir.'/'.sha1($ip).'.json'; + $now = time(); + $fenster = 900; // 15 Minuten + $limit = 10; + + $daten = ['count' => 0, 'first' => $now]; + if (file_exists($datei)) { + $daten = json_decode(file_get_contents($datei), true) ?: $daten; + if ($now - ($daten['first'] ?? 0) > $fenster) { + $daten = ['count' => 0, 'first' => $now]; + } + } + + if ($erfolg === true) { + @unlink($datei); + return true; + } + if ($erfolg === false) { + $daten['count']++; + @file_put_contents($datei, json_encode($daten)); + } + return ($daten['count'] ?? 0) < $limit; +}