diff --git a/index.php b/index.php new file mode 100644 index 0000000..8a5524e --- /dev/null +++ b/index.php @@ -0,0 +1,414 @@ + $maxZeichen) break; + $ergebnis .= $wert[$i]; + } + return $ergebnis; +} + +/** Text säubern: ungültiges UTF-8 und Steuerzeichen raus, Länge begrenzen. */ +function sauberText(mixed $wert, int $maxLaenge = MAX_TEXT_LAENGE): string { + if (!is_string($wert)) return ''; + + // Ungültiges UTF-8 komplett verwerfen - sonst könnten kaputte Bytes + // die spätere Maskierung unterlaufen. + if (preg_match('//u', $wert) !== 1) return ''; + + // Steuerzeichen byteweise entfernen; bei UTF-8 gefahrlos, weil + // Folgebytes immer >= 0x80 sind. + $wert = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $wert) ?? ''; + $wert = trim($wert); + + if (textLaenge($wert) > $maxLaenge) { + $wert = textKuerzen($wert, $maxLaenge - 1) . '…'; + } + return $wert; +} + +/** Ausgabe maskieren - wird bei JEDER Ausgabe verwendet. */ +function e(string $wert): string { + return htmlspecialchars($wert, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8'); +} + +/** Externe Adresse: nur http/https, sonst verworfen. */ +function sichereUrl(mixed $wert): string { + if (!is_string($wert)) return ''; + $wert = trim($wert); + if ($wert === '' || textLaenge($wert) > 500) return ''; + if (!preg_match('#^https?://#i', $wert)) return ''; + if (filter_var($wert, FILTER_VALIDATE_URL) === false) return ''; + return $wert; +} + +/** Relativer Pfad innerhalb des Tool-Ordners - kein Ausbruch möglich. */ +function sichererEinstieg(mixed $wert, string $ordnerPfad): string { + if (!is_string($wert)) return ''; + $wert = trim($wert); + if ($wert === '' || strlen($wert) > 200) return ''; + if (!preg_match('#^[A-Za-z0-9._\-/]+$#', $wert)) return ''; + if (str_contains($wert, '..') || str_starts_with($wert, '/')) return ''; + + $ziel = realpath($ordnerPfad . DIRECTORY_SEPARATOR . $wert); + $basis = realpath($ordnerPfad); + if ($ziel === false || $basis === false) return ''; + if (!str_starts_with($ziel, $basis . DIRECTORY_SEPARATOR)) return ''; + if (!is_file($ziel)) return ''; + return $wert; +} + +/** Datum nur akzeptieren, wenn es wirklich eines ist. */ +function sicheresDatum(mixed $wert): string { + if (!is_string($wert)) return ''; + $wert = trim($wert); + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $wert)) return ''; + [$j, $m, $t] = array_map('intval', explode('-', $wert)); + return checkdate($m, $t, $j) ? $wert : ''; +} + +/** Alle Tools einsammeln. */ +function toolsLesen(): array { + $basis = __DIR__; + $eintraege = @scandir($basis); + if ($eintraege === false) return []; + + $tools = []; + foreach ($eintraege as $name) { + if ($name === '.' || $name === '..') continue; + if (str_starts_with($name, '.') || str_starts_with($name, '_')) continue; + if (in_array(strtolower($name), GESPERRTE_ORDNER, true)) continue; + + $ordner = $basis . DIRECTORY_SEPARATOR . $name; + if (!is_dir($ordner)) continue; + + // Symlinks, die aus dem Verzeichnis herausführen, werden ignoriert + $echt = realpath($ordner); + if ($echt === false || !str_starts_with($echt, $basis . DIRECTORY_SEPARATOR)) continue; + + $jsonPfad = $echt . DIRECTORY_SEPARATOR . 'tool.json'; + if (!is_file($jsonPfad)) continue; + if (filesize($jsonPfad) > MAX_JSON_BYTES) continue; + + $roh = @file_get_contents($jsonPfad, false, null, 0, MAX_JSON_BYTES); + if ($roh === false) continue; + + try { + $daten = json_decode($roh, true, 8, JSON_THROW_ON_ERROR); + } catch (JsonException) { + continue; // defekte Datei wird still übersprungen + } + if (!is_array($daten)) continue; + if (isset($daten['sichtbar']) && $daten['sichtbar'] === false) continue; + + $anzeigename = sauberText($daten['name'] ?? '', 80) ?: $name; + $einstieg = sichererEinstieg($daten['einstieg'] ?? '', $echt); + + $tags = []; + if (isset($daten['tags']) && is_array($daten['tags'])) { + foreach (array_slice($daten['tags'], 0, MAX_TAGS) as $tag) { + $tag = sauberText($tag, 24); + if ($tag !== '') $tags[] = $tag; + } + } + + $tools[] = [ + 'ordner' => $name, + 'name' => $anzeigename, + 'beschreibung' => sauberText($daten['beschreibung'] ?? ''), + 'einstieg' => $einstieg, + 'repository' => sichereUrl($daten['repository'] ?? ''), + 'repositoryName'=> sauberText($daten['repositoryName'] ?? '', 30) ?: 'Quellcode', + 'version' => sauberText($daten['version'] ?? '', 20), + 'lizenz' => sauberText($daten['lizenz'] ?? '', 40), + 'stand' => sicheresDatum($daten['stand'] ?? ''), + 'tags' => $tags, + ]; + } + + usort($tools, fn(array $a, array $b) => strcasecmp($a['name'], $b['name'])); + return $tools; +} + +$tools = toolsLesen(); +?> + + + + + +<?= e(SEITEN_TITEL) ?> + + + + + + +
+
+
SOFTWARE
+
+
+
+ +
+ +

Verfügbare Werkzeuge

+ + +
+ Hier ist noch nichts eingetragen.
+ Ein Ordner erscheint automatisch, sobald er eine Datei tool.json enthält. +
+ +
+ +
+

+ + +

+ +

Keine Beschreibung hinterlegt.

+ + + +
+ + + + + v + + + + + + Stand + +
+ + +
+ + Jetzt nutzen + + + + +
+
+ +
+ + +

+ Alle Werkzeuge laufen vollständig in deinem Browser. Es werden keine Inhalte, + die du darin bearbeitest, an diesen Server übertragen oder dort gespeichert. + Näheres in der Datenschutzerklärung. +

+ +
+ + + + + diff --git a/tool.json.beispiel b/tool.json.beispiel new file mode 100644 index 0000000..7480246 --- /dev/null +++ b/tool.json.beispiel @@ -0,0 +1,12 @@ +{ + "name": "JSON Studio & MariaDB Workbench", + "beschreibung": "Zwei JSON-Editoren nebeneinander: importieren aus CSV, Excel, YAML, XML und SQL, vergleichen, verschachtelte Daten auflösen und als MariaDB-Skript oder JSON-Schema wieder ausgeben.", + "einstieg": "json-workbench.html", + "repository": "https://gitea.obraasch.de/oliver/json-studio", + "repositoryName": "Quellcode (Gitea)", + "version": "3.0", + "lizenz": "MIT", + "stand": "2026-09-16", + "tags": ["JSON", "CSV", "Excel", "YAML", "MariaDB"], + "sichtbar": true +} diff --git a/webserver-sicherheit.md b/webserver-sicherheit.md new file mode 100644 index 0000000..2cd307e --- /dev/null +++ b/webserver-sicherheit.md @@ -0,0 +1,129 @@ +# Webserver absichern + +Konfiguration für `software.obraasch.de`. CloudPanel arbeitet in der Regel mit +nginx — nimm den entsprechenden Abschnitt und lade danach neu. + +Die Startseite `index.php` setzt ihre eigenen Header bereits selbst. Die +folgende Konfiguration deckt zusätzlich die statischen Dateien ab, also +auch `json-workbench.html` und die Rechtstexte. + +--- + +## nginx + +In den `server`-Block der Site einfügen: + +```nginx +# --- Sicherheitsheader für alle Antworten --- +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always; +add_header Cross-Origin-Opener-Policy "same-origin" always; + +# HSTS erst aktivieren, wenn HTTPS sicher und dauerhaft läuft. +# Ein zu früh gesetzter Wert sperrt Besucher für die angegebene Dauer aus, +# falls das Zertifikat später Probleme macht. +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + +# --- Verstecktes und Sensibles nicht ausliefern --- +location ~ /\. { + deny all; +} +location ~* /(tool\.json|LICENSE|README\.md|\.md)$ { + deny all; +} +# Die Datenbank-Brücke enthält Zugangsdaten und darf nie als Quelltext +# ausgeliefert werden. Das passiert nur, wenn PHP ausfällt - deshalb +# zur Sicherheit ausdrücklich absichern: +location = /db-bridge.php { + # Beispiel: nur aus dem eigenen Netz erreichbar. Anpassen oder entfernen, + # wenn die Brücke von außen benutzt werden soll. + # allow 192.168.0.0/16; + # deny all; + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; +} + +# --- Verzeichnislisten abschalten --- +autoindex off; + +# --- Uploads klein halten --- +client_max_body_size 4m; +``` + +### Content-Security-Policy für das Werkzeug + +Wenn die Bibliotheken lokal in `lib/` liegen, kannst du die Regeln eng ziehen. +`unsafe-inline` ist nötig, weil Stile und Skript direkt in der HTML-Datei +stehen; `unsafe-eval` benötigt der JSON-Editor für seine Abfragesprache. + +```nginx +location /json-workbench/ { + add_header Content-Security-Policy + "default-src 'self'; \ + script-src 'self' 'unsafe-inline' 'unsafe-eval'; \ + style-src 'self' 'unsafe-inline'; \ + img-src 'self' data:; \ + font-src 'self'; \ + connect-src *; \ + frame-ancestors 'none'; \ + base-uri 'none'" always; +} +``` + +`connect-src *` ist Absicht: URL-Import, Webhook und Datenbankanbindung +sprechen Adressen an, die die Nutzer selbst eingeben. Willst du das +einschränken, trage statt `*` die erlaubten Ziele ein — dann funktionieren +diese drei Funktionen nur noch mit diesen Zielen. + +Lädst du die Bibliotheken doch vom CDN, ergänze bei `script-src` und +`style-src` zusätzlich `https://cdn.jsdelivr.net`. + +--- + +## Apache (.htaccess) + +```apache + + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" + Header always set Cross-Origin-Opener-Policy "same-origin" + # Erst aktivieren, wenn HTTPS dauerhaft steht: + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + + +# Verzeichnislisten abschalten +Options -Indexes + +# Versteckte Dateien und Metadaten schützen + + Require all denied + + +# Nur nötig, falls die Domain nicht ohnehin auf HTTPS zwingt + + RewriteEngine On + RewriteCond %{HTTPS} !=on + RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +``` + +--- + +## Noch ein paar Punkte + +- **PHP-Version aktuell halten.** `index.php` und `db-bridge.php` brauchen + mindestens PHP 8.0. +- **Fehlermeldungen nicht anzeigen.** In der `php.ini` produktiv + `display_errors = Off` und `log_errors = On`. Eine PHP-Fehlermeldung verrät + sonst Pfade und unter Umständen Zugangsdaten. +- **Keine Backup-Dateien im Web-Verzeichnis.** Dateien wie + `db-bridge.php.bak` oder `index.php~` werden als Klartext ausgeliefert. Der + Editor-Ordner `.git` gehört ebenfalls nicht auf den Webserver — die + `\.`-Regel oben fängt das ab. +- **Automatische Zertifikatserneuerung prüfen**, bevor du HSTS einschaltest. +- **Logfile-Aufbewahrung** an das anpassen, was in der Datenschutzerklärung + steht. Beides muss zusammenpassen.