Backend: - path_validator.py: PathValidator-Klasse für Pfad-Validierung - file_ops.py: read_file, write_file, directory_exists, file_exists - content_types.py: get_content_type mit EXTENSION_MAP - handler.py: Handler-Klasse mit do_GET/do_PUT, nutzt above modules - serve.py: Entry-Point (main, find_free_port), setzt Handler.validator/directory Frontend: - css/variables.css: CSS-Variablen (--bg-*, --text-*, --accent, etc.) - css/styles.css: Alle CSS-Regeln (modal, card, template-grid, etc.) - js/utils.js: esc, showToast, copyContentToClipboard - js/modal.js: showModal, closeModal, closeEditModal, wasViewModalOpen - js/editor.js: editModalContent, createJsonEditUI, extractJsonFromForm - js/api.js: viewTemplate, copyContent, loadTemplates, saveEditedContent - js/templates.js: renderTemplates, applyFilters, parseTypeFromHash - js/main.js: Event-Listener, Hash-Filter, Initialisierung - index.html: Inline-CSS/JS entfernt, <link>/<script src>-Tags hinzugefügt Smoke test: SO_REUSEADDR für schnelle Port-Wiederverwendung
79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Pfad-Validierung für Template-Dateien.
|
|
|
|
Verhindert Path-Traversal-Angriffe und validiert, dass angeforderte
|
|
Pfade innerhalb des erlaubten Verzeichnisses liegen.
|
|
"""
|
|
|
|
import os
|
|
from typing import Optional
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
|
|
class PathValidator:
|
|
"""Validiert und auflöst URL-Pfade zu Dateisystem-Pfaden."""
|
|
|
|
def __init__(self, root_dir: str, web_dir: str):
|
|
"""
|
|
Args:
|
|
root_dir: Projekt-Root-Verzeichnis (enthält templates/)
|
|
web_dir: web/-Verzeichnis (enthält templates.json)
|
|
"""
|
|
self.root_dir = root_dir
|
|
self.web_dir = web_dir
|
|
|
|
def resolve_template_path(self, path: str) -> Optional[str]:
|
|
"""
|
|
Resolve einen Request-Pfad zu einem Dateisystem-Pfad.
|
|
|
|
Gibt None zurück, falls der Pfad ungültig oder nicht autorisiert ist.
|
|
|
|
Beispiele:
|
|
/templates.json -> <web_dir>/templates.json
|
|
/templates/system/foo.json -> <root_dir>/templates/system/foo.json
|
|
"""
|
|
parsed = urlparse(path).path
|
|
path = parsed
|
|
|
|
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
|
|
if path == "/templates.json":
|
|
return os.path.join(self.web_dir, "templates.json")
|
|
|
|
# Nur /templates/* Pfade erlauben
|
|
if not path.startswith("/templates/"):
|
|
return None
|
|
|
|
# Extrahiere den relativen Pfad nach /templates/
|
|
rel_path = path[len("/templates/") :]
|
|
rel_path = unquote(rel_path)
|
|
|
|
# Ablehnen bei leeren Pfaden oder absoluten Pfaden
|
|
if not rel_path or os.path.isabs(rel_path):
|
|
return None
|
|
|
|
# Ablehnen bei '..' Sequenzen (vor der Normalisierung!)
|
|
if ".." in rel_path.split(os.sep):
|
|
return None
|
|
|
|
# Pfad normalisieren
|
|
normalized_rel = os.path.normpath(rel_path)
|
|
|
|
# Nach Normalisierung nochmal prüfen
|
|
if normalized_rel.startswith("..") or os.path.isabs(normalized_rel):
|
|
return None
|
|
|
|
# Vollständigen Pfad konstruieren
|
|
full_path = os.path.join(self.root_dir, "templates", normalized_rel)
|
|
|
|
# Explizite Prüfung, dass der Pfad innerhalb von ROOT_DIR/templates/ liegt
|
|
templates_base = os.path.abspath(os.path.join(self.root_dir, "templates"))
|
|
full_path_abs = os.path.abspath(full_path)
|
|
|
|
if (
|
|
not full_path_abs.startswith(templates_base + os.sep)
|
|
and full_path_abs != templates_base
|
|
):
|
|
return None
|
|
|
|
return full_path_abs
|