fix: Audit-Findings — en_US-Parity, LIKE-Escape, Rate-Limit [deploy]
All checks were successful
Deploy Eplan / deploy (push) Successful in 9s
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:
parent
cf0b78893c
commit
8930d7804a
3 changed files with 53 additions and 2 deletions
|
|
@ -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."%')
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue