Implementiere mehrschichtigen Code-Guard & Sandbox-Schutz: AST-Musterprüfung, Pyodide-Härtung (Sperrung von js/subprocess/socket) und Timeout-Schutz
This commit is contained in:
parent
8032441b23
commit
b240ed9a6b
9 changed files with 158 additions and 16 deletions
|
|
@ -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"
|
||||
}
|
||||
5
api.php
5
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");
|
||||
|
|
|
|||
103
assets/code_guard.js
Normal file
103
assets/code_guard.js
Normal file
|
|
@ -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
|
||||
`;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = `<div style='color: #ef4444;'>${check.error}</div>`;
|
||||
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 === "") {
|
||||
|
|
|
|||
|
|
@ -951,6 +951,7 @@ print(mein_auto.hupen())
|
|||
<script src="assets/interpreter.js"></script>
|
||||
<script src="assets/certificate.js"></script>
|
||||
<script src="assets/parsons.js"></script>
|
||||
<script src="assets/code_guard.js"></script>
|
||||
<script src="assets/runner.js"></script>
|
||||
<script src="assets/ui_enhancements.js"></script>
|
||||
<script src="assets/app_auth.js"></script>
|
||||
|
|
|
|||
|
|
@ -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("""
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@
|
|||
<script src="assets/interpreter.js"></script>
|
||||
<script src="assets/certificate.js"></script>
|
||||
<script src="assets/parsons.js"></script>
|
||||
<script src="assets/code_guard.js"></script>
|
||||
<script src="assets/monaco_ide.js"></script>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue