fix: Audit-Findings — en_US-Parity, LIKE-Escape, Rate-Limit [deploy]
All checks were successful
Deploy Eplan / deploy (push) Successful in 9s

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/<sha1(ip)>.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-04-19 11:58:14 +02:00
parent cf0b78893c
commit 8930d7804a
3 changed files with 53 additions and 2 deletions

View file

@ -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."%')

View file

@ -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.

View file

@ -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;
}