fix: 26 kritische + 29 wichtige Sicherheits- und Robustheitsprobleme behoben

- serve.py: URL-Encoding fix, Body-Limit (10MB), Port-Reuse, TOCTOU-Race, Logging
- index.html: XSS in renderTemplates geheilt, Escape-Key für Edit-Modal, Accessibility
- templates.json: Pfade ohne ../, leere Descriptions ergänzt
- validate.py: categories/ entfernt, CLI-Flags umbenannt, zu mutually_exclusive_group
- smoke_test.sh: set -euo pipefail, Port-Validation, Timeout 5s, code-Fallback
- cleanup_server.sh: lsof statt pgrep, Graceful-Term + SIGKILL-Fallback
- agent_verify.sh: set -euo pipefail, ROOT-Pfade, dynamischer Port, grep-Crash fix
- AGENTS.md: history/ entfernt, Pfad-Schema präzisiert
- README.md: categories/ entfernt, Web-Ansicht + API-Endpunkte hinzugefügt
This commit is contained in:
Michael 2026-05-03 14:00:55 +02:00
parent 1845b30992
commit c294336058
9 changed files with 289 additions and 174 deletions

View file

@ -69,8 +69,8 @@ Ein Commit = eine logische Änderung. Commit-Message im Stil der bestehenden His
### Einstieg
```bash
python3 web/serve.py # startet Server auf http://localhost:8081
./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4)
python3 web/serve.py # startet Server auf http://localhost:8081 (Server-Port)
./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4, Standard-Port 8082)
./scripts/cleanup_server.sh # beendet hängende Instanzen
python3 scripts/validate.py # validiert Template-Struktur
```
@ -85,12 +85,14 @@ python3 scripts/validate.py # validiert Template-Struktur
| `templates/system/*.json` | System-Templates |
| `templates/user/*.md` | User-Templates |
| `scripts/validate.py` | Template-Validierung |
| `scripts/cleanup_server.sh` | Beendet serve.py auf Port 8081 (Standard) oder angegebenem Port |
| `docs/` | Weitere technische Dokumentation |
| `history/CHANGELOG.md` | Änderungs-Chronik |
### Datenschema `templates.json` und Template-Einträge
`web/templates.json` ist eine **Liste** von Einträgen. Schema jedes Eintrags:
`web/templates.json` ist eine **Liste** von Einträgen. `path`-Felder sind **relativ zu ROOT** (Projektstamm), OHNE `../` Präfix.
Schema jedes Eintrags:
```json
{ "path": "…", "type": "…", "name": "…", "description": "…", "version": "…", "tags": [], "format": "md|json" }

View file

@ -13,11 +13,6 @@ prompt_template/
│ ├── user/ # Benutzer-Prompts (Emails, Texte, etc.)
│ └── custom/ # Benutzerdefinierte Templates
├── categories/ # Optional: Kategorisierte Templates
│ ├── marketing/
│ ├── technical/
│ └── creative/
├── scripts/ # Hilfsskripte
│ └── validate.py # Template-Validierung
@ -27,8 +22,33 @@ prompt_template/
---
## Web-Ansicht
Der Server startet mit:
```bash
python3 web/serve.py # http://localhost:8081
```
### API-Endpunkte
| URL | Methode | Beschreibung |
|-----|---------|--------------|
| `/` | GET | Frontend (index.html) |
| `/templates.json` | GET | Katalog (aus web/templates.json) |
| `/templates/system/*.json` | GET | System-Templates |
| `/templates/user/*.md` | GET | User-Templates |
| `/templates/...` | PUT | Template speichern (Content-Type: text/plain) |
### Validierung
```bash
python3 scripts/validate.py --all
```
## Dateiformate
#### Einzelnes Template (JSON)
Ein Template-File (z.B. `templates/system/code_reviewer.json`) hat dieses Schema:
### JSON (empfohlen für strukturierte Templates)
```json
{
@ -45,6 +65,9 @@ prompt_template/
}
```
#### Katalog (web/templates.json)
Der Katalog ist eine Liste von Einträgen:
### Markdown (für einfache Templates mit Dokumentation)
```markdown
# Template Name

View file

@ -2,16 +2,17 @@
# Verifikations-Hook fuer dual_agent-Pipeline.
# Wird nach dem Executor-Lauf aufgerufen; Exit 0 = gruen.
# stdout/stderr landen bei Retry als Kontext beim Planner.
set -u
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FAIL=0
# --- 1. Statisch: JS-Syntax in index.html ------------------------------
if [ -f web/index.html ]; then
if [ -f "$ROOT/web/index.html" ]; then
python3 - <<'PY' || FAIL=1
import re, subprocess, tempfile, sys
html = open('web/index.html').read()
html = open(sys.argv[1] if len(sys.argv) > 1 else 'web/index.html').read()
for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False) as f:
with tempfile.NamedTemporaryFile('w', suffix='.js', delete=True) as f:
f.write(s); p = f.name
r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
if r.returncode != 0:
@ -19,41 +20,44 @@ for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
sys.exit(1)
print('JS syntax OK')
PY
else
echo "FAIL: web/index.html fehlt"
FAIL=1
fi
# --- 2. Dynamisch: Smoke-Test (alle Endpunkte) -------------------------
if [ -x scripts/smoke_test.sh ]; then
./scripts/smoke_test.sh 8088 || FAIL=1
if [ -x "$ROOT/scripts/smoke_test.sh" ]; then
SMOKE_PORT=$(( $$ % 1000 + 9000 ))
"$ROOT/scripts/smoke_test.sh" "$SMOKE_PORT" || FAIL=1
fi
# --- 3. Semantisch: Pseudo-Fix-Muster verbieten ------------------------
# Diese Patterns haben wir als No-Op-Fixes im JSON-Editor erlebt.
# Ein neuer Bugfix darf sie nicht wieder einfuehren.
if [ -f web/index.html ]; then
# No-Op-Pattern: leerer Container + while(firstChild) -> nichts zu bewegen
if grep -qE 'document\.createElement\([^)]+\)[^;]*;[[:space:]]*[^;]*\.appendChild\([^)]*firstChild\)' web/index.html; then
: # zu grob — deaktiviert
fi
if [ -f "$ROOT/web/index.html" ]; then
# Doppeldeklaration einer Funktion
for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do
count=$(grep -cE "function[[:space:]]+${fn}\\b" web/index.html || true)
count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || echo 0)
if [ "$count" -gt 1 ]; then
echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)"
FAIL=1
fi
done
# Platzhalter-Leichen in geaenderten HTML-Attributen
if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' web/index.html; then
if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' "$ROOT/web/index.html"; then
echo "FAIL: Platzhalter-Leichen in der Datei (Variablen nicht interpoliert)"
FAIL=1
fi
fi
# --- 4. Git-Hygiene: Commit-Message ohne Platzhalter-Leichen ----------
if ! git rev-parse HEAD >/dev/null 2>&1; then
echo "WARN: Kein Commit im Repo"
fi
last_msg=$(git log -1 --pretty=%B 2>/dev/null || true)
if echo "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then
if printf '%s\n' "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then
echo "FAIL: letzter Commit hat doppelte Leerzeichen (Platzhalter nicht ersetzt):"
echo "$last_msg" | head -3
printf '%s\n' "$last_msg" | head -3
FAIL=1
fi

View file

@ -1,10 +1,26 @@
#!/bin/bash
# Beendet laufende Instanzen von web/serve.py, um Port-Konflikte auf 8081 zu lösen.
#!/usr/bin/env bash
set -euo pipefail
if pgrep -f "python3 .*web/serve.py" >/dev/null; then
pkill -f "python3 .*web/serve.py"
echo "Alte serve.py-Prozesse beendet."
else
echo "Keine laufende serve.py-Instanz gefunden."
PORT="${1:-8081}"
# Finde Prozess auf Port
PID=$(lsof -ti ":$PORT" 2>/dev/null || true)
if [ -n "$PID" ]; then
# Graceful shutdown attempt
kill -TERM "$PID" 2>/dev/null || true
# Warte bis zu 5 Sekunden
for i in $(seq 1 10); do
if ! kill -0 "$PID" 2>/dev/null; then
break
fi
sleep 0.5
done
# Force kill if still running
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null || true
fi
echo "Prozess $PID auf Port $PORT beendet."
else
echo "Keine laufende Instanz auf Port $PORT gefunden."
fi

View file

@ -7,12 +7,15 @@
# ./scripts/smoke_test.sh 9000 # anderer Port
# Exit-Code: 0 = alle Endpunkte 200, sonst 1.
set -u
set -euo pipefail
PORT="${1:-8082}"
[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "Usage: $0 [port]" >&2; exit 1; }
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG="/tmp/smoke.$PORT.log"
EXIT_CODE=1
MARKER="SMOKE_TEST_SERVER_$PORT"
TIMEOUT=30
START_TIME=$(date +%s)
# Vorherige Instanzen dieses Smoke-Tests beenden
pkill -f "$MARKER" 2>/dev/null || true
@ -55,7 +58,10 @@ trap cleanup EXIT INT TERM
# 127.0.0.1 statt localhost umgeht den Resolver — sonst kann unter
# WSL2/IPv6 ein TCP-SYN auf einen unbenutzten Port minutenlang ohne
# RST hängen. --connect-timeout 1 ist Defense-in-Depth.
for i in $(seq 1 20); do
for i in $(seq 1 50); do
if (( $(date +%s) - START_TIME >= TIMEOUT )); then
break
fi
if curl -sf --connect-timeout 1 -o /dev/null "http://127.0.0.1:$PORT/"; then break; fi
sleep 0.1
done
@ -78,6 +84,7 @@ for p in "/" "/index.html" "/templates.json" \
"/templates/user/email_draft.md" \
"/templates/user/brainstorming.md"; do
code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p")
code="${code:-000}"
status="ok"
[ "$code" = "200" ] || { status="FAIL"; FAIL=1; }
printf '%-50s %s %s\n' "$p" "$code" "$status"

View file

@ -29,7 +29,10 @@ JSON_SCHEMA = {
r"^.+$": {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["string", "number", "enum", "boolean"]},
"type": {
"type": "string",
"enum": ["string", "number", "enum", "boolean"],
},
"required": {"type": "boolean"},
"default": {},
"description": {"type": "string"},
@ -46,7 +49,6 @@ JSON_SCHEMA = {
# Muster für Markdown-Templates
MD_REQUIRED_SECTIONS = ["Template", "Variablen"]
MD_VARIABLE_PATTERN = re.compile(r"\|\s*(\w+)\s*\|\s*(\w+)\s*\|")
def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
@ -54,7 +56,7 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
errors = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
return False, [f"❌ JSON Syntax Error: {e}"]
@ -84,7 +86,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
# Pattern validieren
if "pattern" in schema and isinstance(data[field], str):
if not re.match(schema["pattern"], data[field]):
errors.append(f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}")
errors.append(
f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}"
)
# Template prüfen
if "template" in data:
@ -98,7 +102,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
defined_vars = set(data["variables"].keys())
undefined_vars = template_vars - defined_vars
if undefined_vars:
errors.append(f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}")
errors.append(
f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}"
)
# Variablen validieren
if "variables" in data:
@ -113,7 +119,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
if "type" not in var_schema:
errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld")
if var_schema.get("type") == "enum" and "values" not in var_schema:
errors.append(f"❌ Enum Variable '{var_name}' benötigt 'values' Array")
errors.append(
f"❌ Enum Variable '{var_name}' benötigt 'values' Array"
)
return len(errors) == 0, errors
@ -123,14 +131,14 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
errors = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
# Mindestlänge
if len(content.strip()) < 50:
errors.append(" Template zu kurz (mind. 50 Zeichen)")
errors.append("⚠️ Warnung: Template zu kurz (mind. 50 Zeichen)")
# Titel prüfen
if not content.startswith("# "):
@ -154,16 +162,25 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
# Template-Block prüfen
if "Template" in content:
template_start = content.find("```")
if template_start == -1:
template_block_start = content.find("```")
if template_block_start != -1:
# Finde den Code-Block nach "## Template" oder "# Template"
template_section = content.find("## Template", content.find("Template"))
if template_section == -1:
template_section = content.find("# Template", content.find("Template"))
if template_section > template_block_start:
template_block_start = template_section
if template_block_start == -1:
errors.append("❌ Kein Code-Block für Template gefunden")
else:
# Prüfe ob Variablen im Template sind
template_content = content[template_start:]
template_content = content[template_block_start:]
if "{" not in template_content or "}" not in template_content:
errors.append("⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden")
errors.append(
"⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden"
)
return len(errors) == 0, errors
return len([e for e in errors if e.startswith("")]) == 0, errors
def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
@ -176,10 +193,17 @@ def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
return False, [f"❌ Unsupported file type: {filepath.suffix}"]
ALLOWED_DIRS = {"templates/system", "templates/user", "templates/custom", "categories"}
def find_templates(directory: Path) -> List[Path]:
"""Findet alle Template-Dateien in einem Verzeichnis Baum."""
templates = []
for root, _, files in os.walk(directory):
for root, dirs, files in os.walk(directory):
rel = os.path.relpath(root, directory)
if rel != "." and rel.split(os.sep)[0] not in ALLOWED_DIRS:
dirs.clear() # Skip nicht-erlaubte Subdirs
continue
for file in files:
if file.endswith((".json", ".md")):
templates.append(Path(root) / file)
@ -195,12 +219,17 @@ Beispiele:
python validate.py templates/system/code_reviewer.json
python validate.py --all
python validate.py --json templates/
"""
""",
)
parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis")
parser.add_argument("--all", action="store_true", help="Alle Templates validieren")
parser.add_argument("--json", action="store_true", help="Nur JSON-Templates validieren")
parser.add_argument("--md", action="store_true", help="Nur Markdown-Templates validieren")
type_group = parser.add_mutually_exclusive_group()
type_group.add_argument(
"--type-json", action="store_true", help="Nur JSON-Templates validieren"
)
type_group.add_argument(
"--type-md", action="store_true", help="Nur Markdown-Templates validieren"
)
args = parser.parse_args()
@ -209,21 +238,19 @@ Beispiele:
if args.all:
# Alle Templates finden
templates = find_templates(base_dir / "templates")
if not args.json:
templates += find_templates(base_dir / "categories")
if args.json:
if args.type_json:
templates = [t for t in templates if t.suffix == ".json"]
if args.md:
if args.type_md:
templates = [t for t in templates if t.suffix == ".md"]
elif args.path:
path = Path(args.path)
if path.is_dir():
templates = find_templates(path)
if args.json:
if args.type_json:
templates = [t for t in templates if t.suffix == ".json"]
if args.md:
if args.type_md:
templates = [t for t in templates if t.suffix == ".md"]
else:
templates = [path]

View file

@ -171,7 +171,7 @@
#edit-content-content input[type="checkbox"] + label,
#edit-modal input[type="checkbox"] + label {
color: #222222;
color: #e0e0e0;
font-weight: normal;
}
@ -331,7 +331,7 @@
.template-item .tag {
background: var(--bg-input);
color: var(--text-muted);
color: #b0b0b0;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
@ -451,7 +451,7 @@
</head>
<body>
<!-- Modal -->
<div class="modal-overlay" id="modal">
<div class="modal-overlay" id="modal" role="dialog" aria-label="Template anzeigen">
<div class="modal">
<div class="modal-header">
<h2 id="modal-title">Template</h2>
@ -474,7 +474,7 @@
</div>
<!-- Edit Modal -->
<div class="modal-overlay" id="edit-modal">
<div class="modal-overlay" id="edit-modal" role="dialog" aria-label="Template bearbeiten">
<div class="modal" style="max-width: 800px;">
<div class="modal-header">
<h2 id="edit-title">Template bearbeiten</h2>
@ -494,14 +494,7 @@
<div class="header">
<h1>Prompt Templates</h1>
<span class="badge">Git Managed</span>
<div class="actions">
<button class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
Neues Template
</button>
</div>
<div class="actions"></div>
</div>
<div class="container">
@ -546,6 +539,14 @@ $ python web/serve.py</div>
<script>
let allTemplates = [];
let currentEditTemplate = null;
let editContainerRef = null;
// XSS-schutz:_esc-Helper
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
// Edit Modal Funktionen
function closeEditModal() {
@ -592,6 +593,7 @@ $ python web/serve.py</div>
}
function createJsonEditUI(container, jsonData, editPathHint = '') {
editContainerRef = container;
container.innerHTML = '';
const formDiv = document.createElement('div');
@ -793,7 +795,9 @@ $ python web/serve.py</div>
try {
if (currentEditTemplate.endsWith('.json')) {
const formDiv = document.getElementById('edit-content-content').querySelector('div');
const editContainer = document.getElementById('edit-content-content');
const firstChild = editContainer.children[0];
const formDiv = (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName === 'DIV') ? firstChild : null;
if (!formDiv) {
showToast('✗ Keine Eingabefelder gefunden');
return;
@ -998,20 +1002,20 @@ $ python web/serve.py</div>
container.innerHTML = templates.map(t => `
<div class="template-item">
<h3>${t.name}</h3>
<h3>${esc(t.name)}</h3>
<div class="meta">
<span>🏷️ ${t.type}</span>
<span>📄 ${t.format}</span>
<span>📌 v${t.version}</span>
<span>🏷️ ${esc(t.type)}</span>
<span>📄 ${esc(t.format)}</span>
<span>📌 v${esc(t.version)}</span>
</div>
<p>${t.description || 'Keine Beschreibung'}</p>
<p>${esc(t.description) || 'Keine Beschreibung'}</p>
<div class="tags">
${t.tags.map(tag => `<span class="tag">${tag.startsWith('#') ? tag : '#' + tag}</span>`).join('')}
${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}
</div>
<div class="actions">
<button class="btn btn-icon" onclick="viewTemplate('${t.path}')">Anzeigen</button>
<button class="btn btn-icon" onclick="editModalContent('${t.path}')">📝 Bearbeiten</button>
<button class="btn btn-icon" onclick="copyContent('${t.path}')">Inhalt kopieren</button>
<button class="btn btn-icon" onclick="viewTemplate('${esc(t.path)}')">Anzeigen</button>
<button class="btn btn-icon" onclick="editModalContent('${esc(t.path)}')">📝 Bearbeiten</button>
<button class="btn btn-icon" onclick="copyContent('${esc(t.path)}')">Inhalt kopieren</button>
</div>
</div>
`).join('');
@ -1064,7 +1068,7 @@ $ python web/serve.py</div>
// Escape key closes modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
if (e.key === 'Escape') { closeEditModal(); closeModal(); }
});
// Hash -> type filter
@ -1090,6 +1094,10 @@ $ python web/serve.py</div>
allTemplates = t;
currentType = parseTypeFromHash();
applyFilters();
}).catch(e => {
console.error('Failed to load templates:', e);
allTemplates = [];
applyFilters();
});
</script>
</body>

View file

@ -1,77 +1,93 @@
#!/usr/bin/env python3
"""
Minimaler Entwicklungs-Server für die Prompt Templates Webansicht.
Startet auf Port 8080 und dient die statischen Dateien aus.
Startet auf Port 8081 und dient die statischen Dateien aus.
"""
import http.server
import socketserver
import os
import socket
import os
import json
import logging
from urllib.parse import urlparse, unquote
logging.basicConfig(level=logging.INFO)
DIRECTORY = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.abspath(os.path.join(DIRECTORY, '..'))
ROOT_DIR = os.path.abspath(os.path.join(DIRECTORY, ".."))
def find_free_port(start_port=9000):
"""Finde einen freien Port ab start_port"""
"""Finde einen freien Port ab start_port
Für zukünftige Smoke-Test-Integration.
"""
port = start_port
while port < 10000:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', port))
s.bind(("", port))
return port
except OSError:
port += 1
return None
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **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."""
parsed = urlparse(path).path
path = parsed
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
if path == '/templates.json':
return os.path.join(DIRECTORY, 'templates.json')
if path == "/templates.json":
return os.path.join(DIRECTORY, "templates.json")
# Nur /templates/* Pfade erlauben
if not path.startswith('/templates/'):
if not path.startswith("/templates/"):
return None
# Extrahiere den relativen Pfad nach /templates/
rel_path = path[len('/templates/'):] # 'system/test.json' oder '../etc/passwd'
rel_path = path[len("/templates/") :] # 'system/test.json' oder '../etc/passwd'
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):
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):
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)
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'))
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:
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):
# Nur PUT auf /templates/* Pfade erlauben
file_path = self._validate_template_path(self.path)
file_path = self._validate_template_path(urlparse(self.path).path)
if file_path is None:
self.send_error(403, "Forbidden: Invalid path")
return
@ -83,26 +99,30 @@ class Handler(http.server.SimpleHTTPRequestHandler):
return
# Content-Type prüfen
content_type = self.headers.get('Content-Type', '')
if 'text/plain' not in content_type and not content_type.startswith('text/'):
content_type = self.headers.get("Content-Type", "")
if "text/plain" not in content_type and not content_type.startswith("text/"):
self.send_error(400, "Unsupported content type")
return
# Inhalt lesen und speichern
content_length = int(self.headers.get('Content-Length', 0))
MAX_BODY = 10 * 1024 * 1024
content_length = int(self.headers.get("Content-Length", 0))
if content_length <= 0:
self.send_error(400, "No content provided")
return
if content_length > MAX_BODY:
self.send_error(413, "Request body too large")
return
try:
file_content = self.rfile.read(content_length)
# Datei schreiben
with open(file_path, 'wb') as f:
with open(file_path, "wb") as f:
f.write(file_content)
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"File saved successfully")
except Exception as e:
@ -110,42 +130,50 @@ class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
# Für Root-Pfad: index.html servieren
if self.path == '/' or self.path == '/index.html':
self.path = '/index.html'
parsed_path = urlparse(self.path).path
if parsed_path == "/" or parsed_path == "/index.html":
self.path = "/index.html"
return super().do_GET()
# Anfragen für /templates.json oder /templates/* umleiten
file_path = self._validate_template_path(self.path)
if file_path is not None:
if os.path.exists(file_path) and not os.path.isdir(file_path):
try:
with open(file_path, 'rb') as f:
with open(file_path, "rb") as f:
self.send_response(200)
self.send_header('Content-type', 'text/plain' if file_path.endswith('.md') else 'application/json')
self.send_header(
"Content-type",
"text/plain"
if file_path.endswith(".md")
else "application/json",
)
self.end_headers()
self.wfile.write(f.read())
return
except FileNotFoundError:
self.send_error(404, "File not found")
return
except Exception as e:
self.send_error(500, f"Error serving file: {e}")
return
else:
self.send_error(404, "File not found")
return
return super().do_GET()
def main():
PORT = 8081
print(f"Serving on http://localhost:{PORT}")
logging.info("Serving on http://localhost:%s", PORT)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving Prompt Templates on http://localhost:{PORT}")
print(f"Press Ctrl+C to stop")
print(f"Directory: {DIRECTORY}")
logging.info("Serving Prompt Templates on http://localhost:%s", PORT)
logging.info("Press Ctrl+C to stop")
logging.info("Directory: %s", DIRECTORY)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
logging.info("\nServer stopped")
if __name__ == "__main__":
main()

View file

@ -1,24 +1,24 @@
[
{
"path": "../templates/custom/brainstorming.md",
"path": "templates/custom/brainstorming.md",
"type": "custom",
"name": "Brainstorming Assistent",
"description": "",
"description": "Dient zur Ideengenerierung und Kreativitätsförderung",
"version": "1.0",
"tags": [],
"format": "md"
},
{
"path": "../templates/user/email_draft.md",
"path": "templates/user/email_draft.md",
"type": "user",
"name": "Email Entwurf Assistent",
"description": "",
"description": "Entwirft professionelle E-Mail-Entwürfe mit konfigurierbarem Tonfall",
"version": "1.0",
"tags": [],
"format": "md"
},
{
"path": "../templates/system/code_reviewer.json",
"path": "templates/system/code_reviewer.json",
"type": "system",
"name": "Code Reviewer",
"description": "Analysiert Code auf Qualität, Best Practices und potenzielle Bugs",
@ -33,7 +33,7 @@
"format": "json"
},
{
"path": "../templates/system/commit_analysis.json",
"path": "templates/system/commit_analysis.json",
"type": "system",
"name": "Git Commit Deep Analysis",
"description": "Erstellt eine tiefe Analyse der letzten Git-Commits mit technischer und fachlicher Bewertung",
@ -49,7 +49,7 @@
"format": "json"
},
{
"path": "../templates/system/summarizer.json",
"path": "templates/system/summarizer.json",
"type": "system",
"name": "Text Summarizer",
"description": "Erstellt präzise Zusammenfassungen von Texten mit konfigurierbarer Länge",