Dateien nach "/" hochladen
This commit is contained in:
1 parent
dccee3694a
commit
f7e4833075
3 files changed
+555
No files matched your search
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* ============================================================
|
||||
* index.php - Startseite für software.obraasch.de
|
||||
* ============================================================
|
||||
*
|
||||
* Listet automatisch alle Unterordner auf, die eine Datei
|
||||
* "tool.json" enthalten. Ein Ordner ohne diese Datei taucht
|
||||
* nicht auf - so bestimmst du selbst, was öffentlich wird.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* BENUTZUNG
|
||||
* ------------------------------------------------------------
|
||||
* software.obraasch.de/
|
||||
* ├── index.php (diese Datei)
|
||||
* ├── impressum.html
|
||||
* ├── datenschutz.html
|
||||
* └── json-workbench/
|
||||
* ├── tool.json <- macht den Ordner sichtbar
|
||||
* ├── json-workbench.html
|
||||
* └── lib/
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* SICHERHEITSKONZEPT
|
||||
* ------------------------------------------------------------
|
||||
* - Die Seite verarbeitet KEINERLEI Nutzereingaben. Es werden
|
||||
* weder $_GET noch $_POST noch Cookies ausgewertet. Damit
|
||||
* entfällt die gesamte übliche Angriffsfläche für Injection.
|
||||
* - Es wird nichts aus einem Pfad zusammengesetzt, der von
|
||||
* außen beeinflussbar wäre. Gelesen wird ausschließlich das
|
||||
* eigene Verzeichnis.
|
||||
* - Inhalte aus tool.json stammen zwar von dir, werden aber
|
||||
* trotzdem streng geprüft und beim Ausgeben vollständig
|
||||
* maskiert. Selbst wenn dort HTML oder Skriptcode stünde,
|
||||
* erschiene er als Text.
|
||||
* - Die Seite kommt ganz ohne JavaScript aus. Die Content-
|
||||
* Security-Policy verbietet Skripte deshalb komplett.
|
||||
* - Alle externen Links werden auf http/https geprüft und mit
|
||||
* noopener/noreferrer versehen.
|
||||
*
|
||||
* Voraussetzung: PHP 8.0 oder neuer.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/* ===================== KONFIGURATION ===================== */
|
||||
|
||||
const SEITEN_TITEL = 'Software von Braasch Design';
|
||||
const SEITEN_UNTERTITEL = 'Kleine Werkzeuge, kostenlos nutzbar direkt im Browser';
|
||||
const BETREIBER = 'Oliver Braasch';
|
||||
const KONTAKT_SEITE = 'impressum.html';
|
||||
const DATENSCHUTZ_SEITE = 'datenschutz.html';
|
||||
|
||||
// Ordner, die nie aufgelistet werden - zusätzlich zu allen,
|
||||
// die keine tool.json besitzen.
|
||||
const GESPERRTE_ORDNER = ['lib', 'assets', 'vendor', 'node_modules', 'cgi-bin', 'tmp'];
|
||||
|
||||
// Obergrenzen als Schutz vor versehentlich riesigen Dateien
|
||||
const MAX_JSON_BYTES = 65536;
|
||||
const MAX_TEXT_LAENGE = 400;
|
||||
const MAX_TAGS = 8;
|
||||
|
||||
/* =================== AB HIER NICHTS MEHR ================= */
|
||||
|
||||
$nonce = base64_encode(random_bytes(16));
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header(
|
||||
"Content-Security-Policy: " .
|
||||
"default-src 'none'; " .
|
||||
"script-src 'none'; " .
|
||||
"style-src 'nonce-$nonce'; " .
|
||||
"img-src 'self' data:; " .
|
||||
"font-src 'self'; " .
|
||||
"connect-src 'none'; " .
|
||||
"form-action 'none'; " .
|
||||
"base-uri 'none'; " .
|
||||
"frame-ancestors 'none'"
|
||||
);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
|
||||
/**
|
||||
* Länge und Kürzen funktionieren auch ohne die mbstring-Erweiterung,
|
||||
* da diese nicht auf jedem Server installiert ist.
|
||||
*/
|
||||
function textLaenge(string $wert): int {
|
||||
if (function_exists('mb_strlen')) return mb_strlen($wert, 'UTF-8');
|
||||
// Zeichen zählen statt Bytes: Fortsetzungsbytes (10xxxxxx) überspringen
|
||||
return strlen(preg_replace('/[\x80-\xBF]/', '', $wert) ?? $wert);
|
||||
}
|
||||
|
||||
function textKuerzen(string $wert, int $maxZeichen): string {
|
||||
if (function_exists('mb_substr')) return mb_substr($wert, 0, $maxZeichen, 'UTF-8');
|
||||
$ergebnis = '';
|
||||
$zeichen = 0;
|
||||
$laenge = strlen($wert);
|
||||
for ($i = 0; $i < $laenge && $zeichen < $maxZeichen; $i++) {
|
||||
$byte = ord($wert[$i]);
|
||||
if (($byte & 0xC0) !== 0x80) $zeichen++; // neues Zeichen beginnt
|
||||
if ($zeichen > $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();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= e(SEITEN_TITEL) ?></title>
|
||||
<meta name="description" content="<?= e(SEITEN_UNTERTITEL) ?>">
|
||||
<meta name="robots" content="index, follow">
|
||||
<style nonce="<?= e($nonce) ?>">
|
||||
:root {
|
||||
--bg: #0a0a0c;
|
||||
--card: #16171a;
|
||||
--card-hover: #1c1d21;
|
||||
--text: #e7e8ea;
|
||||
--muted: #8b8d93;
|
||||
--border: #2a2b30;
|
||||
--accent: #ff7a1a;
|
||||
--accent-soft: rgba(255, 122, 26, 0.12);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #eef0f3;
|
||||
--card: #ffffff;
|
||||
--card-hover: #f6f7f9;
|
||||
--text: #1b1c1f;
|
||||
--muted: #6b6d74;
|
||||
--border: #dde0e5;
|
||||
--accent-soft: rgba(255, 122, 26, 0.10);
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 0;
|
||||
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
||||
background: var(--bg); color: var(--text);
|
||||
line-height: 1.6; -webkit-text-size-adjust: 100%;
|
||||
}
|
||||
.wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; }
|
||||
|
||||
header.top {
|
||||
background: #000; border-bottom: 2px solid var(--accent);
|
||||
padding: 26px 0 24px; margin-bottom: 34px;
|
||||
}
|
||||
@media (prefers-color-scheme: light) { header.top { background: #0b0c0e; } }
|
||||
.brand { font-size: 22px; font-weight: 800; letter-spacing: 0.02em; color: #f2f2f3; }
|
||||
.brand span { color: var(--accent); }
|
||||
.tagline { color: #9a9ca3; font-size: 14px; margin-top: 4px; }
|
||||
|
||||
h2 {
|
||||
font-size: 12px; font-weight: 700; letter-spacing: 0.1em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
margin: 0 0 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
||||
|
||||
.card {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 18px; display: flex; flex-direction: column;
|
||||
}
|
||||
.card:hover { background: var(--card-hover); border-color: var(--accent); }
|
||||
.card h3 { margin: 0 0 6px; font-size: 16px; font-weight: 700; }
|
||||
.card .desc { color: var(--muted); font-size: 13.5px; margin: 0 0 14px; flex: 1; }
|
||||
|
||||
.meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
|
||||
.tag {
|
||||
font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 20px;
|
||||
background: var(--accent-soft); color: var(--accent); border: 1px solid transparent;
|
||||
}
|
||||
.tag.plain { background: transparent; color: var(--muted); border-color: var(--border); }
|
||||
|
||||
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
a.btn {
|
||||
display: inline-block; text-decoration: none; font-size: 13px; font-weight: 700;
|
||||
padding: 8px 15px; border-radius: 7px; border: 1px solid var(--border); color: var(--text);
|
||||
}
|
||||
a.btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
a.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
a.btn.primary:hover { background: #e8690e; border-color: #e8690e; color: #fff; }
|
||||
|
||||
.empty {
|
||||
border: 1px dashed var(--border); border-radius: 10px;
|
||||
padding: 34px 24px; text-align: center; color: var(--muted); font-size: 14px;
|
||||
}
|
||||
.empty code {
|
||||
background: var(--card); padding: 2px 6px; border-radius: 4px;
|
||||
font-family: Consolas, monospace; font-size: 12.5px; color: var(--text);
|
||||
}
|
||||
|
||||
.info { color: var(--muted); font-size: 13.5px; margin: 34px 0 0; }
|
||||
|
||||
footer.bottom {
|
||||
margin-top: 48px; border-top: 1px solid var(--border);
|
||||
padding: 22px 0 34px; color: var(--muted); font-size: 13px;
|
||||
}
|
||||
footer.bottom a { color: var(--muted); text-decoration: none; }
|
||||
footer.bottom a:hover { color: var(--accent); text-decoration: underline; }
|
||||
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<div class="brand">SOFT<span>WARE</span></div>
|
||||
<div class="tagline"><?= e(SEITEN_UNTERTITEL) ?></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
|
||||
<h2>Verfügbare Werkzeuge<?= $tools ? ' (' . count($tools) . ')' : '' ?></h2>
|
||||
|
||||
<?php if (!$tools): ?>
|
||||
<div class="empty">
|
||||
Hier ist noch nichts eingetragen.<br>
|
||||
Ein Ordner erscheint automatisch, sobald er eine Datei <code>tool.json</code> enthält.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="grid">
|
||||
<?php foreach ($tools as $tool): ?>
|
||||
<article class="card">
|
||||
<h3><?= e($tool['name']) ?></h3>
|
||||
|
||||
<?php if ($tool['beschreibung'] !== ''): ?>
|
||||
<p class="desc"><?= e($tool['beschreibung']) ?></p>
|
||||
<?php else: ?>
|
||||
<p class="desc">Keine Beschreibung hinterlegt.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($tool['tags'] || $tool['version'] !== '' || $tool['lizenz'] !== '' || $tool['stand'] !== ''): ?>
|
||||
<div class="meta">
|
||||
<?php foreach ($tool['tags'] as $tag): ?>
|
||||
<span class="tag"><?= e($tag) ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($tool['version'] !== ''): ?>
|
||||
<span class="tag plain">v<?= e($tool['version']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($tool['lizenz'] !== ''): ?>
|
||||
<span class="tag plain"><?= e($tool['lizenz']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($tool['stand'] !== ''): ?>
|
||||
<span class="tag plain">Stand <?= e(date('d.m.Y', (int)strtotime($tool['stand']))) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="actions">
|
||||
<?php if ($tool['einstieg'] !== ''): ?>
|
||||
<a class="btn primary" href="<?= e(rawurlencode($tool['ordner'])) ?>/<?= e($tool['einstieg']) ?>">Jetzt nutzen</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($tool['repository'] !== ''): ?>
|
||||
<a class="btn" href="<?= e($tool['repository']) ?>" rel="noopener noreferrer external"><?= e($tool['repositoryName']) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="info">
|
||||
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 <a href="<?= e(DATENSCHUTZ_SEITE) ?>">Datenschutzerklärung</a>.
|
||||
</p>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="bottom">
|
||||
<div class="wrap">
|
||||
<div class="footer-links">
|
||||
<a href="<?= e(KONTAKT_SEITE) ?>">Impressum</a>
|
||||
<a href="<?= e(DATENSCHUTZ_SEITE) ?>">Datenschutz</a>
|
||||
</div>
|
||||
<div>© <?= e(date('Y')) ?> <?= e(BETREIBER) ?></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
<IfModule mod_headers.c>
|
||||
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"
|
||||
</IfModule>
|
||||
|
||||
# Verzeichnislisten abschalten
|
||||
Options -Indexes
|
||||
|
||||
# Versteckte Dateien und Metadaten schützen
|
||||
<FilesMatch "^\.|(tool\.json|LICENSE|README\.md)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Nur nötig, falls die Domain nicht ohnehin auf HTTPS zwingt
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTPS} !=on
|
||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Reference in new issue
Block a user