Fix #131: CSV-Formel-Injection im Anlagelisten-Export behoben
This commit is contained in:
parent
70d98ff48a
commit
fc7ad72b96
6 changed files with 69 additions and 17 deletions
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-27 – Fix: CSV-Formel-Injection im Anlagelisten-Export (Issue #131)
|
||||||
|
|
||||||
|
- `EuerController::anlageCsv()` schrieb `assets.bezeichnung` (frei editierbarer Text) ungefiltert per `fputcsv()` in die exportierte `Anlageliste-{year}.csv`. Eine Bezeichnung wie `=cmd|'/c calc'!A1` konnte beim Öffnen der CSV in Excel/LibreOffice zu Formelausführung führen (CSV-/Formel-Injection, CWE-1236). `BelegController`/`BookingController` waren dank eigener, privater `csvSanitize()`-Methoden bereits abgesichert.
|
||||||
|
- **`src/Controllers/BaseController.php`:** Die bisher in `BelegController` und `BookingController` duplizierte `csvSanitize()`-Methode wurde als gemeinsame `protected` Methode hierher verschoben (Präfix `'` bei führendem `=`, `+`, `-` oder `@`).
|
||||||
|
- **`src/Controllers/EuerController.php`:** `anlageCsv()` sanitisiert `bezeichnung` jetzt über `$this->csvSanitize()`.
|
||||||
|
- **`src/Controllers/BelegController.php`, `src/Controllers/BookingController.php`:** Duplizierte private `csvSanitize()`-Methoden entfernt, nutzen jetzt die geerbte Methode aus `BaseController`.
|
||||||
|
- **`tests/64_AnlageCsvInjectionTest.php`:** Neuer Test prüft, dass eine Anlagenbezeichnung mit führendem `=` im CSV-Export mit vorangestelltem `'` maskiert wird.
|
||||||
|
|
||||||
## 2026-07-27 – Fix: SSRF-Schutz per DNS-Rebinding umgehbar (TOCTOU) (Issue #130)
|
## 2026-07-27 – Fix: SSRF-Schutz per DNS-Rebinding umgehbar (TOCTOU) (Issue #130)
|
||||||
|
|
||||||
- `SsrfGuard::assertPublicHttpsUrl()` löste den Hostnamen einmalig auf und prüfte die IPs, der eigentliche Request (cURL bzw. `php-fints`) löste den Hostnamen aber später erneut selbst auf. Ein Angreifer mit Kontrolle über die DNS-Antworten der konfigurierten Domain konnte zwischen Prüfung und Request von einer öffentlichen auf eine interne IP (127.0.0.1, internes Netz, Cloud-Metadata) umschalten und so die SSRF-Prüfung aus #127/#118 umgehen.
|
- `SsrfGuard::assertPublicHttpsUrl()` löste den Hostnamen einmalig auf und prüfte die IPs, der eigentliche Request (cURL bzw. `php-fints`) löste den Hostnamen aber später erneut selbst auf. Ein Angreifer mit Kontrolle über die DNS-Antworten der konfigurierten Domain konnte zwischen Prüfung und Request von einer öffentlichen auf eine interne IP (127.0.0.1, internes Netz, Cloud-Metadata) umschalten und so die SSRF-Prüfung aus #127/#118 umgehen.
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,15 @@ class BaseController
|
||||||
include dirname(__DIR__) . '/Views/' . $template . '.php';
|
include dirname(__DIR__) . '/Views/' . $template . '.php';
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verhindert CSV-/Formel-Injection: stellt bei führendem =, +, - oder @ ein ' voran.
|
||||||
|
*/
|
||||||
|
protected function csvSanitize(string $value): string
|
||||||
|
{
|
||||||
|
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
|
||||||
|
return "'" . $value;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -485,12 +485,4 @@ class BelegController extends BaseController
|
||||||
Beleg::updateHash($id, hash_file('sha256', $absPath));
|
Beleg::updateHash($id, hash_file('sha256', $absPath));
|
||||||
echo json_encode(['ok' => true]);
|
echo json_encode(['ok' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function csvSanitize(string $value): string
|
|
||||||
{
|
|
||||||
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
|
|
||||||
return "'" . $value;
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -330,13 +330,5 @@ class BookingController extends BaseController
|
||||||
View::redirect('/journal', $e->getMessage(), 'error');
|
View::redirect('/journal', $e->getMessage(), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function csvSanitize(string $value): string
|
|
||||||
{
|
|
||||||
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
|
|
||||||
return "'" . $value;
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ class EuerController extends BaseController
|
||||||
|
|
||||||
fputcsv($out, [
|
fputcsv($out, [
|
||||||
$al['anlage_nr'] ?: $al['lfd_nr'],
|
$al['anlage_nr'] ?: $al['lfd_nr'],
|
||||||
$al['bezeichnung'],
|
$this->csvSanitize((string)$al['bezeichnung']),
|
||||||
$fmtDate($al['anschaffungsdatum']),
|
$fmtDate($al['anschaffungsdatum']),
|
||||||
$fmtNum($al['basis']),
|
$fmtNum($al['basis']),
|
||||||
$nd,
|
$nd,
|
||||||
|
|
|
||||||
49
tests/64_AnlageCsvInjectionTest.php
Normal file
49
tests/64_AnlageCsvInjectionTest.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Steuerkram\Controllers\EuerController;
|
||||||
|
|
||||||
|
class AnlageCsvInjectionTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 64: CSV-Formel-Injection im Anlagelisten-Export verhindert';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$year = (int)date('Y');
|
||||||
|
|
||||||
|
\Steuerkram\Database::execute(
|
||||||
|
"INSERT INTO assets (lfd_nr, anlage_nr, bezeichnung, anschaffungsdatum, anschaffungskosten)
|
||||||
|
VALUES (9998, 'TEST-CSV-INJ', ?, ?, 500.00)",
|
||||||
|
["=cmd|'/c calc'!A1", $year . '-01-15']
|
||||||
|
);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$controller = new EuerController();
|
||||||
|
$controller->anlageCsv(['year' => (string)$year]);
|
||||||
|
$csv = ob_get_clean();
|
||||||
|
|
||||||
|
$lines = explode("\n", $csv);
|
||||||
|
$zeile = null;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (str_contains($line, 'TEST-CSV-INJ')) {
|
||||||
|
$zeile = $line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNotEmpty($zeile, 'Zeile mit Test-Anlage in der CSV-Ausgabe gefunden');
|
||||||
|
|
||||||
|
$felder = explode(';', (string)$zeile);
|
||||||
|
$bezeichnung = $felder[1] ?? '';
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
"'=cmd|'/c calc'!A1",
|
||||||
|
$bezeichnung,
|
||||||
|
'Führendes = in der Bezeichnung wird mit vorangestelltem \' entschärft'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
!str_starts_with($bezeichnung, '='),
|
||||||
|
'Bezeichnungsfeld beginnt nicht mehr mit = (kein von Excel interpretiertes Formelfeld)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue