eplan/lib/eplan_token.lib.php
Eduard Wisch 8930d7804a
All checks were successful
Deploy Eplan / deploy (push) Successful in 9s
fix: Audit-Findings — en_US-Parity, LIKE-Escape, Rate-Limit [deploy]
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>
2026-04-19 11:58:14 +02:00

99 lines
2.8 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* GPL v3+
*
* Token-Handling für die Eplan-PWA-Anbindung.
* Kein JWT — ein simpler Shared-Secret-Token reicht, weil nur der
* ElektroPlan-Backend-Container den Endpoint anspricht (Server-zu-Server
* über HTTPS). Kein User-Kontext, keine Rotation per Request.
*/
/**
* Lazy-init: Token aus llx_const laden, falls leer frisch erzeugen.
* @return string 64-Zeichen hex Token
*/
function eplanTokenHolen()
{
global $db, $conf;
$token = getDolGlobalString('EPLAN_PWA_SECRET');
if (!empty($token)) {
return $token;
}
$token = bin2hex(random_bytes(32));
dolibarr_set_const($db, 'EPLAN_PWA_SECRET', $token, 'chaine', 0, 'PWA-Token', $conf->entity);
return $token;
}
/**
* Neuen Token erzeugen (überschreibt den alten).
* @return string neuer Token
*/
function eplanTokenRotieren()
{
global $db, $conf;
$token = bin2hex(random_bytes(32));
dolibarr_set_const($db, 'EPLAN_PWA_SECRET', $token, 'chaine', 0, 'PWA-Token', $conf->entity);
return $token;
}
/**
* Vergleicht einen übergebenen Token mit dem gespeicherten (timing-safe).
* @param string $eingang Token aus dem Request
* @return bool
*/
function eplanTokenPruefen($eingang)
{
if (empty($eingang)) return false;
$gespeichert = getDolGlobalString('EPLAN_PWA_SECRET');
if (empty($gespeichert)) return false;
return hash_equals($gespeichert, (string) $eingang);
}
/**
* Token aus Request holen: bevorzugt Header X-Eplan-Token, sonst Query-Param `token`.
* @return string|null
*/
function eplanTokenAusRequest()
{
$header = $_SERVER['HTTP_X_EPLAN_TOKEN'] ?? '';
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;
}