fix: HEAD-Requests für /templates/* korrekt auflösen

- Implementiere do_HEAD in Handler
- Sende nur Response-Header mit Content-Type und Content-Length
- Kein Body, wie von HTTP/1.1 für HEAD spezifiziert
- Nutze dieselbe Pfadlogik wie do_GET
This commit is contained in:
Michael 2026-04-26 23:03:09 +02:00
parent 625a2b72c7
commit 1845b30992

View file

@ -28,20 +28,54 @@ def find_free_port(start_port=9000):
class Handler(http.server.SimpleHTTPRequestHandler): class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs) super().__init__(*args, directory=DIRECTORY, **kwargs)
def _validate_template_path(self, path):
"""Validiert, dass der Pfad auf Schritte Beschränkt ist und innerhalb von ROOT_DIR/templates/ oder DIRECTORY liegt."""
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
if path == '/templates.json':
return os.path.join(DIRECTORY, 'templates.json')
# Nur /templates/* Pfade erlauben
if not path.startswith('/templates/'):
return None
# Extrahiere den relativen Pfad nach /templates/
rel_path = path[len('/templates/'):] # 'system/test.json' oder '../etc/passwd'
# 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(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(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
def do_PUT(self): def do_PUT(self):
# Nur PUT auf /templates/* Pfade erlauben # Nur PUT auf /templates/* Pfade erlauben
if not self.path.startswith('/templates'): file_path = self._validate_template_path(self.path)
self.send_error(403, "Method not allowed for this path") if file_path is None:
self.send_error(403, "Forbidden: Invalid path")
return return
# /templates.json liegt im web/-Verzeichnis (Katalog), alles andere unter ROOT
if self.path == '/templates.json':
file_path = os.path.join(DIRECTORY, 'templates.json')
else:
rel_path = self.path[1:] # '/templates/system/test.json' → 'templates/system/test.json'
file_path = os.path.join(ROOT_DIR, rel_path)
# Verzeichnis prüfen - muss existieren # Verzeichnis prüfen - muss existieren
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
if not os.path.exists(file_dir) or not os.path.isdir(file_dir): if not os.path.exists(file_dir) or not os.path.isdir(file_dir):
@ -81,15 +115,8 @@ class Handler(http.server.SimpleHTTPRequestHandler):
return super().do_GET() return super().do_GET()
# Anfragen für /templates.json oder /templates/* umleiten # Anfragen für /templates.json oder /templates/* umleiten
if self.path.startswith('/templates'): file_path = self._validate_template_path(self.path)
# /templates.json ist der Katalog und liegt im web/-Verzeichnis, if file_path is not None:
# /templates/... verweist auf Template-Dateien im Projekt-Root
if self.path == '/templates.json':
file_path = os.path.join(DIRECTORY, 'templates.json')
else:
rel_path = self.path[1:] # '/templates/system/x.json' → 'templates/system/x.json'
file_path = os.path.join(ROOT_DIR, rel_path)
if os.path.exists(file_path) and not os.path.isdir(file_path): if os.path.exists(file_path) and not os.path.isdir(file_path):
try: try:
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f: