From e4c68b28941f629723b0b237ceda96799c0e08d5 Mon Sep 17 00:00:00 2001 From: a2nr Date: Wed, 22 Apr 2026 12:57:54 +0700 Subject: [PATCH] feat(security): implement anonymous access with rate limiting and secure proxy, harden authentication and implement session protection Implements multiple layers of security to address high-risk session and authentication vulnerabilities identified in the security review: - Allow code compilation (C, Python, Arduino) for anonymous users. - Enforce a 1-request-per-2-minutes rate limit for anonymous IPs. - Implement a global anonymous compilation queue with 20 concurrent slots. - Proxy Velxio (Arduino) compilation through Flask to prevent API hijacking. - Exempt authenticated users (tokens/cookies) from all rate limits. - Fix networking and DNS resolution in podman-compose. - Fix Svelte a11y warnings and trailing slash routing issues. - Cookie Security: Added dynamic 'secure' flag support via COOKIE_SECURE env variable for HTTPS/Tailscale Funnel compatibility. - Rate Limiting: Integrated Flask-Limiter on /login (50 req/min) to prevent API abuse while accommodating shared school networks (NAT). - Tarpitting: Added 1.5s artificial delay on failed logins to neutralize automated brute-force tools without blocking legitimate users. - Session Invalidation: Implemented an in-memory token blacklist on logout to ensure session tokens cannot be reused. - Documentation: Updated technical docs and proposal status to reflect the current security architecture. Ref: @elemes/proposal.md (Poin 6.1, 6.2, 6.3) --- .env.example | 1 + .gitignore | 3 +- app.py | 4 + config/sinau-c-tail.json | 8 +- documentation.md | 57 ++- elemes.sh | 10 +- extensions.py | 8 + frontend/src/hooks.server.ts | 15 +- frontend/src/lib/components/Navbar.svelte | 2 +- frontend/src/lib/types/compiler.ts | 1 + .../src/routes/lesson/[slug]/+page.svelte | 2 +- frontend/src/routes/progress/+page.svelte | 14 +- frontend/vite.config.ts | 5 + podman-compose.yml | 10 +- proposal.md | 364 ++++++++++++++++-- requirements.txt | 3 +- routes/auth.py | 24 +- routes/compile.py | 151 +++++++- services/token_service.py | 12 + 19 files changed, 621 insertions(+), 73 deletions(-) create mode 100644 extensions.py diff --git a/.env.example b/.env.example index 3e36283..fcfcf48 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ CONTENT_DIR=content TOKENS_FILE=tokens.csv # Domain frontend yang diizinkan untuk CORS (misal: http://localhost:3000) ORIGIN=http://localhost:3000 +COOKIE_SECURE=false COMPILER_WORKER_URL=http://compiler-worker:8080/execute # ── Tailscale (opsional, untuk jaringan P2P) ─── diff --git a/.gitignore b/.gitignore index d5eecd9..4f3b05b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ test_data.json load-test/env -load-test/test_data.json \ No newline at end of file +load-test/test_data.json +video_gen diff --git a/app.py b/app.py index 91ae2d7..64d498a 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ import logging from flask import Flask from flask_cors import CORS +from extensions import limiter from services.lesson_service import get_lesson_names from services.token_service import initialize_tokens_file @@ -29,6 +30,9 @@ def create_app(): allowed_origin = os.environ.get('ORIGIN', '*') CORS(app, resources={r"/*": {"origins": allowed_origin}}) + # Initialize extensions + limiter.init_app(app) + # ── Blueprints ──────────────────────────────────────────────────── from routes.auth import auth_bp from routes.compile import compile_bp diff --git a/config/sinau-c-tail.json b/config/sinau-c-tail.json index ae08e8d..b4d6d6b 100644 --- a/config/sinau-c-tail.json +++ b/config/sinau-c-tail.json @@ -13,6 +13,12 @@ "/assets/": { "Proxy": "http://elemes:5000/assets/" }, + "/velxio/api/compile/": { + "Proxy": "http://elemes:5000/velxio-compile/" + }, + "/velxio/api/compile": { + "Proxy": "http://elemes:5000/velxio-compile" + }, "/velxio/": { "Proxy": "http://velxio:80/" } @@ -22,4 +28,4 @@ "AllowFunnel": { "${TS_CERT_DOMAIN}:443": true } -} +} \ No newline at end of file diff --git a/documentation.md b/documentation.md index 7324c9f..1e9482a 100644 --- a/documentation.md +++ b/documentation.md @@ -1,7 +1,7 @@ # Elemes LMS — Dokumentasi Teknis **Project:** LMS-C (Learning Management System untuk Pemrograman C & Arduino) -**Terakhir diupdate:** 8 April 2026 +**Terakhir diupdate:** 22 April 2026 --- @@ -13,9 +13,10 @@ Internet (HTTPS :443) ▼ Tailscale Funnel (elemes-ts) │ - ├── / → SvelteKit Frontend (elemes-frontend :3000) - ├── /assets/ → Flask Backend (elemes :5000) - ├── /velxio/ → Velxio Arduino Simulator (velxio :80) + ├── / → SvelteKit Frontend (elemes-frontend :3000) + ├── /assets/ → Flask Backend (elemes :5000) + ├── /velxio/api/compile → Flask Backend (Rate-limited Proxy :5000) + ├── /velxio/ → Velxio Arduino Simulator (velxio :80) │ ▼ SvelteKit Frontend (elemes-frontend :3000) @@ -28,11 +29,18 @@ SvelteKit Frontend (elemes-frontend :3000) │ ▼ /api/* Flask API Backend (elemes :5000) - ├── Code compilation (gcc / python) + ├── Code compilation (Proxied to Compiler Worker) + ├── Arduino Proxy (/velxio-compile → Velxio :80) ├── Token authentication (CSV) ├── Progress tracking └── Lesson content parsing (markdown) │ + ▼ HTTP +Compiler Worker (compiler-worker :8080) + ├── gVisor Sandbox (runsc runtime) + ├── Gunicorn (4 workers) + └── Isolation: gcc / python3 execution + │ Velxio Arduino Simulator (velxio :80) ├── React + Vite frontend (editor + simulator canvas) ├── FastAPI backend (arduino-cli compile) @@ -44,7 +52,8 @@ Velxio Arduino Simulator (velxio :80) | Container | Image | Port | Fungsi | |-----------|-------|------|--------| -| `elemes` | Python 3.11 + gcc | 5000 | Flask API (compile, auth, lessons, progress) | +| `elemes` | Python 3.11 | 5000 | Flask API (auth, lessons, progress, compile-proxy) | +| `compiler-worker` | Python 3.11 + gcc | 8080 | **Sandboxed** execution engine (gVisor) | | `elemes-frontend` | Node 20 | 3000 | SvelteKit SSR | | `velxio` | Node + Python + arduino-cli | 80 | Simulator Arduino (React + FastAPI) | | `elemes-ts` | Tailscale | 443 | HTTPS Funnel + reverse proxy | @@ -250,7 +259,8 @@ Semua endpoint Flask diakses via SvelteKit proxy (`/api/*` → Flask `:5000`, pr | GET | `/api/lessons` | `/lessons` | Daftar lesson + home content | | GET | `/api/lesson/.json` | `/lesson/.json` | Data lesson lengkap | | GET | `/api/get-key-text/` | `/get-key-text/` | Key text untuk lesson | -| POST | `/api/compile` | `/compile` | Compile & run kode (C/Python) | +| POST | `/api/compile` | `/compile` | Compile & run kode (C/Python) via worker | +| POST | `/velxio/api/compile` | `/velxio-compile` | **Rate-limited** Arduino compile proxy | | POST | `/api/track-progress` | `/track-progress` | Track progress siswa | | GET | `/api/progress-report.json` | `/progress-report.json` | Data progress semua siswa | | GET | `/api/progress-report/export-csv` | `/progress-report/export-csv` | Export CSV | @@ -456,6 +466,38 @@ Diaktifkan via prop `noPaste={true}`. --- +## Keamanan & Autentikasi (Security) + +Sistem pengamanan untuk melindungi data siswa dan mencegah penyalahgunaan API. + +### 1. Eksekusi Terisolasi (gVisor Sandbox) +Seluruh kode yang dikirim oleh siswa (C dan Python) tidak dieksekusi di dalam container utama, melainkan diteruskan ke **Compiler Worker** yang berjalan menggunakan **runtime gVisor (`runsc`)**. Ini mencegah serangan *Remote Code Execution* (RCE) yang menargetkan kernel sistem operasi host. + +### 2. Anonymous Access & Rate Limiting +Sistem mengizinkan pengguna tanpa token (anonim) untuk mencoba pembelajaran dengan batasan ketat: +- **Rate Limit**: Pengguna anonim dibatasi **1 kali kompilasi setiap 2 menit** per IP. +- **Global Queue**: Maksimal **20 slot** antrean kompilasi anonim secara global untuk menjaga stabilitas server. +- **Bypass**: Pengguna yang telah login (memiliki token/cookie valid) **bebas** dari batasan rate limit ini. +- **Velxio Protection**: Kompilasi Arduino diproteksi melalui proxy backend Flask untuk mencegah *hijacking* API dari sisi klien. + +### 3. Cookie Security (Dynamic Secure Flag) +LMS menggunakan cookie `student_token` untuk mengelola sesi. Keamanannya diatur secara dinamis melalui environment variable. +- **`httponly: true`**: Mencegah akses cookie via JavaScript (proteksi XSS). +- **`secure: COOKIE_SECURE`**: Jika `true`, cookie hanya dikirim via HTTPS. Sangat penting saat dideploy via Tailscale Funnel. +- **`samesite: 'Lax'`**: Proteksi dasar terhadap CSRF. + +### 2. Proteksi Brute-Force & Anti-Spam +Untuk mencegah penebakan token secara massal, terutama pada jaringan WiFi sekolah yang menggunakan satu IP publik (NAT): +- **Rate Limiting**: Endpoint `/api/login` dibatasi maksimal **50 request per menit per IP**. Angka ini diatur untuk mengakomodasi satu kelas (50 siswa) yang login bersamaan tanpa saling memblokir. +- **Tarpitting (Login Delay)**: Setiap percobaan login yang **gagal** akan ditahan selama **1.5 detik** sebelum server memberikan respons. Ini melumpuhkan efektivitas alat brute-force otomatis tanpa mengganggu pengalaman siswa asli yang hanya sesekali salah ketik. + +### 3. Session Invalidation (Token Blacklist) +Mekanisme logout server-side untuk memastikan token tidak bisa digunakan kembali setelah sesi berakhir. +- **Mekanisme**: Menggunakan `LOGOUT_BLACKLIST` (in-memory set) di Flask backend. +- **Flow**: Saat user klik logout, token ditambahkan ke blacklist. Semua request berikutnya dengan token tersebut akan ditolak oleh `validate_token()`. + +--- + ## Touch Crosshair System (CircuitJS) Overlay untuk presisi interaksi di iframe CircuitJS pada perangkat sentuh. Aktif hanya pada touch device (CSS media query `hover: none` + `pointer: coarse`). @@ -560,6 +602,7 @@ Redo (Ctrl+Shift+Z / toolbar button) - [x] Velxio integration di Elemes (bridge, parsing, UI, evaluasi) - [x] Mobile wiring UX (pinch-zoom preserve wire, crosshair alignment) - [x] Wire undo/redo (snapshot-based, Ctrl+Z/Ctrl+Shift+Z, toolbar button, mobile-friendly) +- [x] Security Review & Hardening (Cookie security, Rate limiting, Tarpitting, Blacklisting) - [x] Contoh lesson Arduino (LED Blink) - [ ] PWA (service worker, offline caching) - [ ] Contoh lesson Arduino tambahan (2-3 lagi) diff --git a/elemes.sh b/elemes.sh index 019cde7..84d9ef1 100755 --- a/elemes.sh +++ b/elemes.sh @@ -63,7 +63,7 @@ stop | run | runbuild | runclearbuild) echo "🛑 Menghentikan container yang sedang berjalan..." podman-compose -p "$PROJECT_NAME" --env-file ../.env down ;;& -stop) +stop) echo "✅ Container berhasil dihentikan." ;; runclearbuild) @@ -83,7 +83,7 @@ runbuild | runclearbuild) ;; run) echo "🚀 Menjalankan container..." - podman-compose -p "$PROJECT_NAME" --env-file ../.env up -d + podman-compose -p "$PROJECT_NAME" --env-file ../.env up echo "✅ Elemes berhasil dijalankan!" ;; generatetoken) @@ -109,7 +109,7 @@ exportall) echo "" echo "💾 Menyatukan semua image menjadi 1 file tar: $TAR_FILE..." - podman save lms-c-backend:latest lms-c-frontend:latest lms-c-velxio:latest > "$TAR_FILE" + podman save lms-c-backend:latest lms-c-frontend:latest lms-c-velxio:latest >"$TAR_FILE" if [ $? -eq 0 ]; then FILESIZE=$(du -h "$TAR_FILE" | cut -f1) @@ -190,7 +190,7 @@ loadtest) echo "⚙️ Mengaktifkan environment & menginstall requirements..." source env/bin/activate - pip install -r requirements.txt > /dev/null 2>&1 + pip install -r requirements.txt >/dev/null 2>&1 echo "⚙️ Mempersiapkan Test Data & menginjeksi akun Bot..." python3 content_parser.py --num-tokens 50 @@ -201,7 +201,7 @@ loadtest) echo "👉 Masukkan URL backend Elemes sebagai Host (contoh: http://localhost:5000)" echo "👉 Tekan CTRL+C di terminal ini untuk menghentikan test." echo "" - + locust -f locustfile.py ;; *) diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..074756e --- /dev/null +++ b/extensions.py @@ -0,0 +1,8 @@ +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address + +# Initialize Limiter without app for the app factory pattern +limiter = Limiter( + key_func=get_remote_address, + storage_uri="memory://", +) diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index eab7f92..2bcc0ae 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -19,11 +19,18 @@ export const handle: Handle = async ({ event, resolve }) => { // Proxy /api/* and /assets/* to Flask backend const isApi = event.url.pathname.startsWith('/api/'); const isAsset = event.url.pathname.startsWith('/assets/'); + const isVelxioCompile = event.url.pathname === '/velxio/api/compile' || event.url.pathname === '/velxio/api/compile/'; + + if (isApi || isAsset || isVelxioCompile) { + let backendPath = ''; + if (isVelxioCompile) { + backendPath = '/velxio-compile/'; + } else if (isApi) { + backendPath = event.url.pathname.replace(/^\/api/, ''); + } else { + backendPath = event.url.pathname; + } - if (isApi || isAsset) { - const backendPath = isApi - ? event.url.pathname.replace(/^\/api/, '') - : event.url.pathname; // /assets/* kept as-is const backendUrl = `${API_BACKEND}${backendPath}${event.url.search}`; try { diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 9c8ddd3..07837ab 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -31,7 +31,7 @@ } -