Fix #131: CSV-Formel-Injection im Anlagelisten-Export behoben

This commit is contained in:
fds2610-bot 2026-07-27 12:03:49 +02:00
parent 70d98ff48a
commit fc7ad72b96
6 changed files with 69 additions and 17 deletions

View file

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

View file

@ -13,4 +13,15 @@ class BaseController
include dirname(__DIR__) . '/Views/' . $template . '.php';
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;
}
}

View file

@ -485,12 +485,4 @@ class BelegController extends BaseController
Beleg::updateHash($id, hash_file('sha256', $absPath));
echo json_encode(['ok' => true]);
}
private function csvSanitize(string $value): string
{
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
return "'" . $value;
}
return $value;
}
}

View file

@ -330,13 +330,5 @@ class BookingController extends BaseController
View::redirect('/journal', $e->getMessage(), 'error');
}
}
private function csvSanitize(string $value): string
{
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
return "'" . $value;
}
return $value;
}
}

View file

@ -116,7 +116,7 @@ class EuerController extends BaseController
fputcsv($out, [
$al['anlage_nr'] ?: $al['lfd_nr'],
$al['bezeichnung'],
$this->csvSanitize((string)$al['bezeichnung']),
$fmtDate($al['anschaffungsdatum']),
$fmtNum($al['basis']),
$nd,

View 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)'
);
}
}