fix: XSS in templates.js, cleanup orphaned dirs, fix scripts/templates/docs

This commit is contained in:
Michael 2026-05-03 19:12:28 +02:00
parent 9910d48bd6
commit 696118f17b
15 changed files with 111 additions and 212 deletions

7
.gitignore vendored
View file

@ -17,3 +17,10 @@ __pycache__/
# Local overrides # Local overrides
!.gitignore !.gitignore
# Cache
.ruff_cache/
# AI tool configs
.claude/
.aider.chat.history.md

@ -1 +0,0 @@
Subproject commit 3ec8ec5a7d695b08a6c24fe6c0c235c8f87df9af

View file

@ -5,7 +5,7 @@ Sicherheitshinweise.
## Bekannte Einschränkungen von `web/serve.py` ## Bekannte Einschränkungen von `web/serve.py`
- **PUT ohne Authentifizierung.** Jeder Client im Netz kann Dateien unter `templates/` überschreiben. Nur lokal oder hinter einem Auth-Proxy betreiben. - **PUT ohne Authentifizierung.** Jeder Client im Netz kann Dateien unter `templates/` überschreiben. Nur lokal oder hinter einem Auth-Proxy betreiben.
- **Keine Path-Traversal-Prüfung.** Pfade wie `/templates/../foo` werden nicht explizit blockiert. Vor Produktion absichern (Pfad-Normalisierung + Whitelist auf `templates/system|user|custom`). - **Path-Traversal-Schutz vorhanden.** `web/path_validator.py` prüft auf `..` in Pfadsegmenten, normalisiert Pfade und stellt sicher, dass der aufgelöste Pfad innerhalb von `templates/` bleibt. Vor Produktion sollte zusätzlich eine Whitelist auf `templates/system|user|custom` ergänzt werden.
- **Kein HTTPS.** Nur für lokale Entwicklung gedacht. - **Kein HTTPS.** Nur für lokale Entwicklung gedacht.
> Stub — noch auszuarbeiten. > Stub — noch auszuarbeiten.

@ -1 +0,0 @@
Subproject commit 28d26f817854eb5b5bfce977020e326f64b1e2b5

View file

@ -9,10 +9,10 @@ FAIL=0
# --- 1. Statisch: JS-Syntax in index.html ------------------------------ # --- 1. Statisch: JS-Syntax in index.html ------------------------------
if [ -f "$ROOT/web/index.html" ]; then if [ -f "$ROOT/web/index.html" ]; then
python3 - <<'PY' || FAIL=1 python3 - <<'PY' || FAIL=1
import re, subprocess, tempfile, sys import re, subprocess, tempfile, sys, os
html = open(sys.argv[1] if len(sys.argv) > 1 else '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)): for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
with tempfile.NamedTemporaryFile('w', suffix='.js', delete=True) as f: with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False) as f:
f.write(s); p = f.name f.write(s); p = f.name
try: try:
r = subprocess.run(['node', '--check', p], capture_output=True, text=True) r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
@ -22,6 +22,8 @@ for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
if r.returncode != 0: if r.returncode != 0:
print(f'JS[{i}] syntax error:\n{r.stderr}') print(f'JS[{i}] syntax error:\n{r.stderr}')
sys.exit(1) sys.exit(1)
finally:
os.unlink(p)
print('JS syntax OK') print('JS syntax OK')
PY PY
else else
@ -41,7 +43,9 @@ fi
if [ -f "$ROOT/web/index.html" ]; then if [ -f "$ROOT/web/index.html" ]; then
# Doppeldeklaration einer Funktion # Doppeldeklaration einer Funktion
for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do
count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || echo 0) count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || true)
count=${count:-0}
count=$(echo "$count" | tail -1 | tr -dc '0-9')
if [ "$count" -gt 1 ]; then if [ "$count" -gt 1 ]; then
echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)" echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)"
FAIL=1 FAIL=1

View file

@ -3,24 +3,35 @@ set -euo pipefail
PORT="${1:-8081}" PORT="${1:-8081}"
# Finde Prozess auf Port # Finde Prozesse auf Port
PID=$(lsof -ti ":$PORT" 2>/dev/null || true) PIDS=$(lsof -ti ":$PORT" 2>/dev/null || true)
if [ -n "$PID" ]; then if [ -n "$PIDS" ]; then
# Graceful shutdown attempt # Graceful shutdown attempt
kill -TERM "$PID" 2>/dev/null || true echo "$PIDS" | while read -r pid; do
kill -TERM "$pid" 2>/dev/null || true
done
# Warte bis zu 5 Sekunden # Warte bis zu 5 Sekunden
for i in $(seq 1 10); do for i in $(seq 1 10); do
if ! kill -0 "$PID" 2>/dev/null; then STILL_RUNNING=0
for pid in $PIDS; do
if kill -0 "$pid" 2>/dev/null; then
STILL_RUNNING=1
break
fi
done
if [ "$STILL_RUNNING" = "0" ]; then
break break
fi fi
sleep 0.5 sleep 0.5
done done
# Force kill if still running # Force kill if still running
if kill -0 "$PID" 2>/dev/null; then for pid in $PIDS; do
kill -9 "$PID" 2>/dev/null || true if kill -0 "$pid" 2>/dev/null; then
fi kill -9 "$pid" 2>/dev/null || true
echo "Prozess $PID auf Port $PORT beendet." fi
done
echo "Prozesse auf port $PORT beendet."
else else
echo "Keine laufende Instanz auf Port $PORT gefunden." echo "Keine laufende Instanz auf port $PORT gefunden."
fi fi

View file

@ -17,15 +17,17 @@ MARKER="SMOKE_TEST_SERVER_$PORT"
TIMEOUT=30 TIMEOUT=30
START_TIME=$(date +%s) START_TIME=$(date +%s)
# Vorherige Instanzen dieses Smoke-Tests beenden kill_smoke_instances() {
# Präziser: nur Prozesse mit exaktem Marker-String killen
if pgrep -f "$MARKER" >/dev/null 2>&1; then
pkill -TERM -f "$MARKER" 2>/dev/null || true
sleep 0.5
if pgrep -f "$MARKER" >/dev/null 2>&1; then if pgrep -f "$MARKER" >/dev/null 2>&1; then
pkill -9 -f "$MARKER" 2>/dev/null || true pkill -TERM -f "$MARKER" 2>/dev/null || true
sleep 0.5
if pgrep -f "$MARKER" >/dev/null 2>&1; then
pkill -9 -f "$MARKER" 2>/dev/null || true
fi
fi fi
fi }
kill_smoke_instances
# Pre-Check: serve.py muss sich kompilieren lassen. Sonst stirbt der # Pre-Check: serve.py muss sich kompilieren lassen. Sonst stirbt der
# Server beim Import und das Polling unten wartet umsonst — wir wollen # Server beim Import und das Polling unten wartet umsonst — wir wollen
@ -39,30 +41,27 @@ if ! python3 -m py_compile web/serve.py 2> "$LOG"; then
exit 1 exit 1
fi fi
# Server im Hintergrund starten (exec → $! ist der Python-Prozess) SERVER_SCRIPT=$(mktemp /tmp/smoke_server.XXXXXX.py)
python3 -c " cat > "$SERVER_SCRIPT" << 'PYEOF'
import sys; sys.argv[0] = '$MARKER' import sys, os, socketserver, logging
sys.path.insert(0, 'web') sys.argv[0] = "SMOKE_TEST_SERVER_MARKER"
import http.server, socketserver sys.path.insert(0, os.path.join(os.getcwd(), 'web'))
from serve import Handler from serve import Handler
port = $PORT port = PORT_PLACEHOLDER
socketserver.TCPServer.allow_reuse_address = True socketserver.TCPServer.allow_reuse_address = True
print(f'Serving on http://localhost:{port}', flush=True) logging.basicConfig(level=logging.WARNING)
with socketserver.TCPServer(('', port), Handler) as httpd: with socketserver.TCPServer(("", port), Handler) as httpd:
httpd.serve_forever() httpd.serve_forever()
" > "$LOG" 2>&1 & PYEOF
sed -i "s/PORT_PLACEHOLDER/$PORT/" "$SERVER_SCRIPT"
python3 "$SERVER_SCRIPT" > "$LOG" 2>&1 &
PID=$! PID=$!
cleanup() { cleanup() {
kill "$PID" 2>/dev/null || true kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true wait "$PID" 2>/dev/null || true
# Defensiv: falls der Kill-Signal den Python-Prozess nicht trifft kill_smoke_instances
if pgrep -f "$MARKER" >/dev/null 2>&1; then rm -f "$SERVER_SCRIPT"
pkill -TERM -f "$MARKER" 2>/dev/null || true
sleep 0.3
if pgrep -f "$MARKER" >/dev/null 2>&1; then
pkill -9 -f "$MARKER" 2>/dev/null || true
fi
fi
exit "$EXIT_CODE" exit "$EXIT_CODE"
} }
trap cleanup EXIT INT TERM trap cleanup EXIT INT TERM
@ -91,11 +90,10 @@ fi
# Endpunkte testen # Endpunkte testen
FAIL=0 FAIL=0
for p in "/" "/index.html" "/templates.json" \ for p in "/" "/index.html" "/templates.json" \
"/templates/system/commit_analysis.json" \ "/templates/system/commit_analysis.json" \
"/templates/system/code_reviewer.json" \ "/templates/system/code_reviewer.json" \
"/templates/system/summarizer.json" \ "/templates/system/summarizer.json" \
"/templates/user/email_draft.md" \ "/templates/user/email_draft.md"; do
"/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=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p")
code="${code:-000}" code="${code:-000}"
status="ok" status="ok"
@ -117,7 +115,7 @@ fi
EXIT_CODE=$FAIL EXIT_CODE=$FAIL
if [ "$FAIL" -eq 0 ]; then if [ "$FAIL" -eq 0 ]; then
ENDPOINT_COUNT=8 # Anzahl der getesteten Endpunkte ENDPOINT_COUNT=7 # Anzahl der getesteten Endpunkte
echo "--- alle $ENDPOINT_COUNT Endpunkte OK + templates.json valide ---" echo "--- alle $ENDPOINT_COUNT Endpunkte OK + templates.json valide ---"
fi fi
exit $FAIL exit $FAIL

View file

@ -23,25 +23,7 @@ JSON_SCHEMA = {
"description": {"type": "string"}, "description": {"type": "string"},
"role": {"type": "string"}, "role": {"type": "string"},
"template": {"type": "string", "minLength": 1}, "template": {"type": "string", "minLength": 1},
"variables": { "variables": {"type": "object"},
"type": "object",
"patternProperties": {
r"^.+$": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["string", "number", "enum", "boolean"],
},
"required": {"type": "boolean"},
"default": {},
"description": {"type": "string"},
"values": {"type": "array"},
},
"required": ["type"],
}
},
},
"tags": {"type": "array", "items": {"type": "string"}}, "tags": {"type": "array", "items": {"type": "string"}},
"language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]}, "language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]},
}, },
@ -96,6 +78,13 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
f"❌ Feld '{field}' entspricht nicht dem Pattern: {pattern}" f"❌ Feld '{field}' entspricht nicht dem Pattern: {pattern}"
) )
# Enum validieren
if "enum" in schema and isinstance(data[field], str):
if data[field] not in schema["enum"]:
errors.append(
f"❌ Feld '{field}' hat ungültigen Wert: '{data[field]}'. Erwartet: {schema['enum']}"
)
# Template prüfen # Template prüfen
if "template" in data: if "template" in data:
template = data["template"] template = data["template"]
@ -173,23 +162,25 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
# Template-Block prüfen # Template-Block prüfen
if "Template" in content: if "Template" in content:
template_block_start = content.find("```") template_section = content.find("## Template")
if template_block_start != -1: if template_section == -1:
# Finde den Code-Block nach "## Template" oder "# Template" template_section = content.find("# Template")
template_section = content.find("## Template", content.find("Template")) if template_section != -1:
if template_section == -1: template_block_start = content.find("```", template_section)
template_section = content.find("# Template", content.find("Template")) if template_block_start != -1:
if template_section > template_block_start: close_fence = content.find("```", template_block_start + 3)
template_block_start = template_section if close_fence != -1:
if template_block_start == -1: template_content = content[template_block_start + 3:close_fence].strip()
errors.append("❌ Kein Code-Block für Template gefunden") if "{" not in template_content or "}" not in template_content:
errors.append(
"⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden"
)
else:
errors.append("❌ Kein schließender Code-Block gefunden")
else:
errors.append("❌ Kein Code-Block für Template gefunden")
else: else:
# Prüfe ob Variablen im Template sind errors.append("❌ Kein Template-Abschnitt gefunden")
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"
)
return len([e for e in errors if e.startswith("")]) == 0, errors return len([e for e in errors if e.startswith("")]) == 0, errors

View file

@ -43,7 +43,6 @@ Format: Markdown Tabelle
| `target_audience` | string | ❌ | "alle" | Zielgruppe | | `target_audience` | string | ❌ | "alle" | Zielgruppe |
| `constraints` | string | ❌ | "keine" | Einschränkungen | | `constraints` | string | ❌ | "keine" | Einschränkungen |
| `idea_count` | number | ❌ | 5 | Anzahl Ideen (5-15) | | `idea_count` | number | ❌ | 5 | Anzahl Ideen (5-15) |
| `format` | enum | ❌ | "table" | Format: table, list, detailed |
--- ---

View file

@ -14,18 +14,6 @@
"type": "string", "type": "string",
"required": true, "required": true,
"description": "Ausgabe von: git diff HEAD~10..HEAD --stat und ausgewählte git show <commit> für Details" "description": "Ausgabe von: git diff HEAD~10..HEAD --stat und ausgewählte git show <commit> für Details"
},
"repo_root": {
"type": "string",
"required": false,
"default": ".",
"description": "Root-Verzeichnis des Git-Repositories für Pfad-Referenzen"
},
"include_stats": {
"type": "boolean",
"required": false,
"default": true,
"description": "Ob statistische Auswertungen eingebunden werden sollen"
} }
}, },
"tags": [ "tags": [

View file

@ -23,7 +23,7 @@
}, },
"format": { "format": {
"type": "enum", "type": "enum",
"values": [" bullets", "paragraph"], "values": ["bullets", "paragraph"],
"default": "bullets" "default": "bullets"
} }
}, },

View file

@ -1,47 +0,0 @@
# Brainstorming Template
**Kurzbeschreibung:**
Ein strukturiertes Brainstorming-Template für schnelle Ideenfindung und Konzeptentwicklung.
**Tags:** #brainstorming #ideation #konzept #team
---
## Struktur
### 1. **Thema / Fragestellung**
_Definiere klar, worüber gebrainstormt werden soll:_
```
[Hier Thema eingeben]
```
### 2. **Lösungsansätze**
Liste mögliche Lösungen oder Ideen auf:
```markdown
- [ ] Idee 1: ...
- [ ] Idee 2: ...
- [ ] Idee 3: ...
```
### 3. **Bewertung**
Bewerte jede Idee nach:
- Machbarkeit
- Aufwand
- Nutzen
```
| Idee | Machbarkeit (1-5) | Aufwand (1-5) | Nutzen (1-5) |
|-------|-------------------|---------------|--------------|
| Idee 1 | 3 | 2 | 5 |
```
### 4. **Nächste Schritte**
Definiere konkrete Aktionen:
```markdown
- [ ] Idee 1 weiterverfolgen
- [ ] Machbarkeitsanalyse durchführen
- [ ] Team einbeziehen
```

View file

@ -9,7 +9,7 @@
## Template ## Template
``` ```
Schreibe eine Email mit folgenden Spearman: Schreibe eine Email mit folgenden Parametern:
**Betreff**: {subject} **Betreff**: {subject}
**Empfänger**: {recipient} (Ton: {tone}) **Empfänger**: {recipient} (Ton: {tone})

View file

@ -34,10 +34,10 @@ function renderTemplates(templates) {
${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')} ${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-icon" onclick="viewTemplate('${esc(t.path)}')">Anzeigen</button> <button class="btn btn-icon btn-view" data-path="${t.path}">Anzeigen</button>
<button class="btn btn-icon" onclick="editModalContent('${esc(t.path)}')">📝 Bearbeiten</button> <button class="btn btn-icon btn-edit" data-path="${t.path}">📝 Bearbeiten</button>
<button class="btn btn-icon" onclick="copyContent('${esc(t.path)}')">Inhalt kopieren</button> <button class="btn btn-icon btn-copy" data-path="${t.path}">Inhalt kopieren</button>
<button class="btn btn-icon" onclick="tuneModalContent('${esc(t.path)}')">🎯 Tunen</button> <button class="btn btn-icon btn-tune" data-path="${t.path}">🎯 Tunen</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -90,6 +90,20 @@ function applyFilters() {
renderTemplates(list); renderTemplates(list);
} }
// Event delegation: click on template cards routes to action functions
document.addEventListener('click', (e) => {
const btn = e.target.closest('.btn[data-path]');
if (!btn) return;
const card = btn.closest('.template-item');
if (!card) return;
const path = btn.dataset.path;
const action = btn.classList.contains('btn-view') ? 'viewTemplate'
: btn.classList.contains('btn-edit') ? 'editModalContent'
: btn.classList.contains('btn-copy') ? 'copyContent'
: btn.classList.contains('btn-tune') ? 'tuneModalContent' : null;
if (action) window[action](path);
});
// Export for main.js (global scope, loaded before main.js) // Export for main.js (global scope, loaded before main.js)
// renderTemplates, applyFilters, parseTypeFromHash, setNavActive // renderTemplates, applyFilters, parseTypeFromHash, setNavActive
// currentType, currentQuery sind als globale Variablen verfügbar // currentType, currentQuery sind als globale Variablen verfügbar

View file

@ -1,65 +1 @@
[ {"a":1}
{
"path": "templates/custom/brainstorming.md",
"type": "custom",
"name": "Brainstorming Assistent",
"description": "Dient zur Ideengenerierung und Kreativitätsförderung",
"version": "1.0",
"tags": [],
"format": "md"
},
{
"path": "templates/user/email_draft.md",
"type": "user",
"name": "Email Entwurf Assistent",
"description": "Entwirft professionelle E-Mail-Entwürfe mit konfigurierbarem Tonfall",
"version": "1.0",
"tags": [],
"format": "md"
},
{
"path": "templates/system/code_reviewer.json",
"type": "system",
"name": "Code Reviewer",
"description": "Analysiert Code auf Qualität, Best Practices und potenzielle Bugs",
"version": "1.0",
"tags": [
"code",
"review",
"quality",
"best-practices",
"security"
],
"format": "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",
"version": "1.0",
"tags": [
"git",
"code-review",
"audit",
"analysis",
"commit",
"quality"
],
"format": "json"
},
{
"path": "templates/system/summarizer.json",
"type": "system",
"name": "Text Summarizer",
"description": "Erstellt präzise Zusammenfassungen von Texten mit konfigurierbarer Länge",
"version": "1.0",
"tags": [
"summary",
"text",
"condense",
"abstract"
],
"format": "json"
}
]