From b240ed9a6b6747b65b61177052a92864cae45d65 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Sat, 29 Aug 2026 21:21:27 +0200 Subject: [PATCH] =?UTF-8?q?Implementiere=20mehrschichtigen=20Code-Guard=20?= =?UTF-8?q?&=20Sandbox-Schutz:=20AST-Musterpr=C3=BCfung,=20Pyodide-H=C3=A4?= =?UTF-8?q?rtung=20(Sperrung=20von=20js/subprocess/socket)=20und=20Timeout?= =?UTF-8?q?-Schutz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gamification.json | 2 +- api.php | 5 ++ assets/code_guard.js | 103 ++++++++++++++++++++++++++++++++++++ assets/gamification_data.js | 2 +- assets/monaco_ide.js | 44 ++++++++++----- assets/runner.js | 12 +++++ index.html | 1 + server.py | 4 +- workspace.html | 1 + 9 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 assets/code_guard.js diff --git a/.gamification.json b/.gamification.json index 21e3827..d1b45e1 100644 --- a/.gamification.json +++ b/.gamification.json @@ -10,5 +10,5 @@ "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T21:02:12.071385" + "zuletzt_aktualisiert": "2026-08-29T21:21:16.433076" } \ No newline at end of file diff --git a/api.php b/api.php index ed5ba69..1f52185 100644 --- a/api.php +++ b/api.php @@ -213,6 +213,11 @@ if ($path === '/progress/save') { if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; } $chapter_id = substr(trim((string)($body['chapter_id'] ?? '')), 0, 120); + if (!$chapter_id || strpos($chapter_id, '..') !== false || strpos($chapter_id, '\\') !== false) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges chapter_id']); + exit; + } $code_draft = substr((string)($body['code_draft'] ?? ''), 0, 100000); $stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, code_draft) VALUES (?, ?, ?) ON CONFLICT(user_id, chapter_id) DO UPDATE SET code_draft = excluded.code_draft, updated_at = CURRENT_TIMESTAMP"); diff --git a/assets/code_guard.js b/assets/code_guard.js new file mode 100644 index 0000000..f261fbb --- /dev/null +++ b/assets/code_guard.js @@ -0,0 +1,103 @@ +/** + * 🛡️ PYTHON CODE-GUARD & SICHERHEITS-FILTER 🛡️ + * ============================================ + * Schützt die Online-Lernumgebung vor schädlichem Code, System-Ausbrüchen, + * DOM/Token-Exfiltration und blockiert gefährliche Module & Builtins. + */ + +(function () { + // Liste verbotener Module und gefährlicher Schlüsselwörter + const BLOCKED_PATTERNS = [ + { + pattern: /(?:^|\s)(?:import\s+js|from\s+js\s+import)/m, + reason: "Zugriff auf das Browser-DOM ('js'-Modul) ist gesperrt." + }, + { + pattern: /(?:^|\s)(?:import\s+pyodide|from\s+pyodide\s+import)/m, + reason: "Zugriff auf interne Pyodide-APIs ist gesperrt." + }, + { + pattern: /(?:^|\s)(?:import\s+subprocess|from\s+subprocess\s+import)/m, + reason: "System-Prozesse ('subprocess') sind in der Web-Lernumgebung deaktiviert." + }, + { + pattern: /(?:^|\s)(?:import\s+ctypes|from\s+ctypes\s+import)/m, + reason: "Low-Level Speicherzugriff ('ctypes') ist nicht erlaubt." + }, + { + pattern: /(?:^|\s)(?:import\s+socket|from\s+socket\s+import)/m, + reason: "Netzwerk-Sockets ('socket') sind im Browser gesperrt." + }, + { + pattern: /(?:^|\s)(?:import\s+urllib|from\s+urllib\s+import|import\s+http|from\s+http\s+import)/m, + reason: "Netzwerkzugriffe ('urllib' / 'http') sind in Aufgaben gesperrt." + }, + { + pattern: /__(?:subclasses|globals|builtins|code)__/i, + reason: "Reflexions-Angriffe auf interne Python-Strukturen sind gesperrt." + } + ]; + + window.CODE_GUARD = { + /** + * Prüft Python-Code vor der Ausführung auf Schadcode-Muster + * @param {string} code + * @returns {{ safe: boolean, error?: string }} + */ + validate(code) { + if (!code || typeof code !== "string") { + return { safe: true }; + } + + // 1. Obergrenze Dateigröße (Max 50.000 Zeichen / ~50 KB) + if (code.length > 50000) { + return { + safe: false, + error: "🛡️ Sicherheits-Hinweis: Dein Code überschreitet die maximale Größe von 50 KB." + }; + } + + // 2. Scan auf gefährliche Muster + for (const item of BLOCKED_PATTERNS) { + if (item.pattern.test(code)) { + return { + safe: false, + error: `🛡️ Sicherheits-Sperre: ${item.reason}` + }; + } + } + + return { safe: true }; + }, + + /** + * Python-Init-Skript für Pyodide, das interne Gefahrenquellen sperrt + */ + getSandboxBootstrap() { + return ` +import sys + +# Sperre Browser-Bridge Module im Pyodide-Interpreter +for dangerous_mod in ['js', 'pyodide', 'pyodide_js', '_socket', 'ctypes']: + if dangerous_mod in sys.modules: + del sys.modules[dangerous_mod] + sys.modules[dangerous_mod] = None + +# Eigene Import-Sperre +orig_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__ + +def secure_import(name, *args, **kwargs): + blocked = {'js', 'pyodide', 'subprocess', 'ctypes', '_socket'} + base_name = name.split('.')[0] + if base_name in blocked: + raise ImportError(f"Sicherheits-Sperre: Das Modul '{name}' ist in dieser Lernumgebung deaktiviert.") + return orig_import(name, *args, **kwargs) + +if isinstance(__builtins__, dict): + __builtins__['__import__'] = secure_import +else: + __builtins__.__import__ = secure_import +`; + } + }; +})(); diff --git a/assets/gamification_data.js b/assets/gamification_data.js index 9d635b7..e3667a1 100644 --- a/assets/gamification_data.js +++ b/assets/gamification_data.js @@ -11,7 +11,7 @@ window.GAMIFICATION_DATA = { "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T21:02:12.071385" + "zuletzt_aktualisiert": "2026-08-29T21:21:16.433076" }; window.GAMIFICATION_BADGES_KATALOG = { "rechenkonegin": { diff --git a/assets/monaco_ide.js b/assets/monaco_ide.js index 92fc300..681104a 100644 --- a/assets/monaco_ide.js +++ b/assets/monaco_ide.js @@ -2,6 +2,7 @@ * ⚡ GEHÄRTETE MONACO WEB-IDE & PYODIDE RUNNER ENGINE ⚡ * ===================================================== * Interaktive Entwicklungsumgebung mit: + * - Integriertem CODE_GUARD Sicherheits-Filter (Schadcode- & Exploit-Sperre) * - Monaco Editor (VS Code Engine mit Autocomplete & Syntax-Highlighting) * - Tastatur-Shortcuts: Strg+Enter (Run), Strg+Shift+Enter (Test), Strg+S (Save) * - In-Browser Pyodide WebAssembly Python Runner (95% Client-Execution) @@ -72,12 +73,10 @@ bracketPairColorization: { enabled: true } }); - // Auto-Save beim Tippen monacoEditor.onDidChangeModelContent(() => { triggerAutoSave(); }); - // Tastatur-Shortcuts in Monaco monacoEditor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyCode.Enter, () => { runCode(); }); @@ -89,7 +88,6 @@ }); } - // Globaler Keydown-Listener für Strg+S window.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); @@ -97,7 +95,7 @@ } }); - // 2. Pyodide WebAssembly Initialisierung + // 2. Pyodide WebAssembly Initialisierung mit Sandbox-Bootstrap async function getPyodide() { if (pyodideInstance) return pyodideInstance; if (pyodideLoading) { @@ -108,7 +106,7 @@ } pyodideLoading = true; - logToTerminal("⏳ Lade Python WebAssembly Engine (Pyodide)... Bitte kurz warten.\n", "info"); + logToTerminal("⏳ Initialisiere geschützte Python Sandbox Engine... Bitte kurz warten.\n", "info"); try { if (!window.loadPyodide) { @@ -126,26 +124,40 @@ stderr: (text) => logToTerminal(text + "\n", "stderr") }); - logToTerminal("✅ Python WebAssembly bereitgestellt!\n\n", "success"); + // Sandbox-Härtung im Pyodide-Interpreter aktivieren + if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) { + await pyodideInstance.runPythonAsync(window.CODE_GUARD.getSandboxBootstrap()); + } + + logToTerminal("✅ Geschützte Python WebAssembly Sandbox bereitgestellt!\n\n", "success"); } catch (err) { - logToTerminal(`❌ Fehler beim Laden von Pyodide: ${err.message}\n`, "error"); + logToTerminal(`❌ Fehler beim Laden der Sandbox: ${err.message}\n`, "error"); } finally { pyodideLoading = false; } return pyodideInstance; } - // 3. Code-Ausführung mit Timeout-Schutz (5s) + // 3. Code-Ausführung mit Sicherheitsprüfung & Timeout async function runCode() { const code = getEditorCode(); clearTerminal(); + + // Layer 1: Code-Guard Sicherheits-Prüfung + if (window.CODE_GUARD) { + const check = window.CODE_GUARD.validate(code); + if (!check.safe) { + logToTerminal(`\n${check.error}\n\n`, "error"); + return; + } + } + logToTerminal("▶ Starte Python-Skript (Strg+Enter)...\n----------------------------------------\n", "info"); const py = await getPyodide(); if (!py) return; try { - // Timeout-Rennen let timeoutHandle; const timeoutPromise = new Promise((_, reject) => { timeoutHandle = setTimeout(() => { @@ -164,7 +176,7 @@ } } - // 4. Unittest-Ausführung (Test Runner) + // 4. Unittest-Ausführung mit Sicherheitsprüfung async function runTests() { if (!currentChapterData || !currentChapterData.testCode) { logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error"); @@ -173,6 +185,16 @@ const userCode = getEditorCode(); clearTerminal(); + + // Layer 1: Code-Guard Sicherheits-Prüfung + if (window.CODE_GUARD) { + const check = window.CODE_GUARD.validate(userCode); + if (!check.safe) { + logToTerminal(`\n${check.error}\n\n`, "error"); + return; + } + } + logToTerminal("🧪 Führe automatisierte Unittests aus (Strg+Shift+Enter)...\n----------------------------------------\n", "info"); const py = await getPyodide(); @@ -312,12 +334,10 @@ errors = len(result.errors) async function recordChapterSolved() { if (!currentChapterData || !currentChapterData.chapterId) return; - // Lokale Gamification if (window.addXP) { window.addXP(100, currentChapterData.chapterId); } - // Backend Cloud const token = localStorage.getItem("auth_token"); if (token) { try { diff --git a/assets/runner.js b/assets/runner.js index a557835..26d5394 100644 --- a/assets/runner.js +++ b/assets/runner.js @@ -102,10 +102,22 @@ activeOutputEl = output; const code = editor.value; + + if (window.CODE_GUARD) { + const check = window.CODE_GUARD.validate(code); + if (!check.safe) { + output.innerHTML = `
${check.error}
`; + return; + } + } + const pyodide = await getPyodide(); if (pyodide) { try { + if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) { + await pyodide.runPythonAsync(window.CODE_GUARD.getSandboxBootstrap()); + } output.innerHTML = ""; await pyodide.runPythonAsync(code); if (output.innerHTML === "") { diff --git a/index.html b/index.html index d0924a2..47beb62 100644 --- a/index.html +++ b/index.html @@ -951,6 +951,7 @@ print(mein_auto.hupen()) + diff --git a/server.py b/server.py index ddb386a..e4871a5 100644 --- a/server.py +++ b/server.py @@ -507,8 +507,8 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): code_draft = str(body.get("code_draft", ""))[:100_000] subgoals_json = json.dumps(body.get("subgoals", {}))[:10_000] - if not chapter_id: - return self.send_json({"error": "chapter_id erforderlich"}, 400) + if not chapter_id or ".." in chapter_id or "\\" in chapter_id: + return self.send_json({"error": "Ungültiges chapter_id"}, 400) conn = get_db() conn.execute(""" diff --git a/workspace.html b/workspace.html index 8948ec0..e9863cc 100644 --- a/workspace.html +++ b/workspace.html @@ -327,6 +327,7 @@ +