Laporan Remediasi Audit CBT — Batch 3: Otorisasi Kredensial, Anti-Hijack, Rate Limit & SSRF
Tanggal: 29 September 2026
Status Batch: SELESAI & TERVERIFIKASI (180 Integration Tests Passed)
Baseline Commit: 8abc4dac0bbc610ea8a38ea32550b45299d4c63d
Lingkungan Pengujian: PostgreSQL 16 disposable (scola_assessment_test) via Alembic e2f3a4b5c6d7
1. Ringkasan Temuan & Solusi
B04 — Rotasi PIN Tidak Mencabut Launch Authorization yang Belum Diredeem (P1)
- Akar Masalah:
Saat kredensial dirotasi (
regenerate_credentials) atau dicabut (revoke_credentials),RunnerSessiondicabut, namunLaunchAuthorizationberstatus"issued"tidak diubah. Otorisasi peluncuran lama yang belum di-redeem tetap dapat di-redeem selama masa berlaku TTL 15 menit, dan menerbitkan sesi runner baru dengan generasi kredensial terbaru. - Perbaikan:
- Menambahkan kolom
credential_generationpada tabellaunch_authorizationsvia migrasi Alembice2f3a4b5c6d7. - Di
seb_service.py:create_launch_authorization: Mengikatcredential_generation=enrollment.credential_generationsaat otorisasi diterbitkan. - Di
participant_service.py: Memperbarui status semuaLaunchAuthorizationpending (status == "issued") menjadi"revoked"secara atomik saatregenerate_credentialsataurevoke_credentials. - Di
seb_service.py:load_launch_authorization: Menolak otorisasi yang berstatus"revoked"dengan HTTP 401 Unauthorized. - Di
seb_service.py:redeem_seb_launch: Memverifikasi kecocokanrecord.credential_generation == enrollment.credential_generation. Jika berbeda, otorisasi dibatalkan (status = "revoked") dan ditolak dengan HTTP 401 Unauthorized.
B05 — Self-Registration Dapat Mengambil Alih Identitas Peserta Existing (P1)
- Akar Masalah:
Pada pendaftaran mandiri publik (
register_participant_public), sistem mencari peserta existing hanya berdasarkanparticipant_numbertanpa mencocokkanemailataufull_name. Pendaftar dapat mendaftar pada Event B dengan nomor peserta korban di Event A, lalu mengaitkan enrollment liar keParticipant.idkorban. - Perbaikan:
- Di
participant_service.py:register_participant_public:- Jika nomor peserta yang diinput sudah ada pada organizer, periksa kesesuaian
emaildanfull_name. Jika terdapat perbedaan, tolak dengan HTTP 409 Conflict ("Nomor peserta telah terdaftar dengan data identitas yang berbeda"). - Jika email yang diinput sudah terdaftar pada peserta lain di organizer dengan nomor peserta berbeda, tolak dengan HTTP 409 Conflict (
"Email telah terdaftar dengan nomor peserta yang berbeda"). - Pendaftaran peserta baru tanpa konflik nomor/email tetap berjalan normal dan menerbitkan kredensial baru.
- Jika nomor peserta yang diinput sudah ada pada organizer, periksa kesesuaian
B10 — Pembatasan Percobaan Tidak Mencakup Launch dan MFA (P1)
- Akar Masalah:
- Endpoint
/api/v1/secure/launch-authorizationsmemverifikasi PIN/kode akses ujian tanpa pembatasan kegagalan, memungkinkan tebakan berulang tanpa lockout. - Endpoint MFA (
/api/v1/auth/mfa/verify,/confirm-enrollment,/recover) tidak memiliki pembatasan percobaan gagal. - Challenge MFA (
temp_token/challenge_id) tidak ditandai sebagai consumed setelah verifikasi berhasil, sehingga rentan digunakan ulang (replay). - Perbaikan:
- Di
rate_limit.py: Menambahkan fungsiconsume_challenge(key: str, ttl_seconds: int)yang bekerja secara atomik pada Redis (dengan fallback ke sliding window memori). - Di
seb_service.py:create_launch_authorization: Memasangcheck_rate_limit(rate_key)danrecord_failure(rate_key)untuk membatasi maksimal 5 kali kegagalan per pasangan event & credential slug. - Di
auth_service.py:- Memasang
check_rate_limit(f"mfa:{user.id}")danrecord_failurepadaverify_mfa,confirm_mfa_enrollment, danrecover_mfa. Percobaan ke-6 diblokir dengan HTTP 429 Too Many Requests beserta headerRetry-After. - Mengonsumsi
challenge_idsetelah verifikasi OTP berhasil melaluiconsume_challenge. Percobaan replay token yang sama seketika ditolak dengan HTTP 401 Unauthorized ("Token MFA telah digunakan sebelumnya").
- Memasang
R03 — Webhook URL LMS Terbuka Terhadap SSRF (P1)
- Akar Masalah:
Staff dengan peran admin tenant dapat memberikan
webhook_urlarbitrer pada sinkronisasi nilai ke LMS. URL tersebut langsung dipanggil viahttpx.Client.posttanpa validasi asal, skema, atau alamat IP tujuan, membuka risiko SSRF terhadap localhost, port Salus Tryton (8003), dan metadata cloud instance (169.254.169.254). - Perbaikan:
- Membuat modul keamanan jaringan mandiri:
backend/app/core/network_security.pydengan fungsivalidate_webhook_url. - Memvalidasi skema (
http/https), memeriksa hostname, melakukan resolusi DNS, dan memblokir seluruh alamat IP privat, loopback (127.0.0.0/8,::1), link-local metadata (169.254.0.0/16), multicast, dan reserved. Pelanggaran langsung ditolak dengan HTTP 400 Bad Request. - Mengintegrasikan validator ke
lms_integration_service.py:sync_session_grades_to_lmssebelum koneksi HTTP keluar dibuat. - Memperbaiki resolusi peserta dari
att.participantmenjadiatt.enrollment.participant if att.enrollment else Noneuntuk mencegahAttributeError.
2. File yang Diubah & Dibuat
| File | Status | Keterangan Perubahan |
|---|---|---|
backend/alembic/versions/e2f3a4b5c6d7_b04_launch_auth_credential_generation.py |
Baru | Migrasi Alembic untuk kolom credential_generation pada launch_authorizations. |
backend/app/models/secure.py |
Dimodifikasi | Menambahkan credential_generation = Column(Integer, default=1, nullable=False) pada LaunchAuthorization. |
backend/app/core/network_security.py |
Baru | Validator URL outbound webhook dengan pencegahan SSRF (DNS resolution & IP range guard). |
backend/app/core/rate_limit.py |
Dimodifikasi | Helper consume_challenge untuk verifikasi token sekali pakai (single-use token guard). |
backend/app/services/seb_service.py |
Dimodifikasi | Rate limiting launch authorization, verifikasi credential generation pada redeem, dan penolakan status revoked. |
backend/app/services/participant_service.py |
Dimodifikasi | Pembatalan launch auth saat rotasi/pencabutan PIN, dan validasi silang identitas pada self-registration (B05). |
backend/app/services/auth_service.py |
Dimodifikasi | Rate limiting MFA (5 attempts $\rightarrow$ 429) dan pencegahan replay challenge MFA (single-use). |
backend/app/services/lms_integration_service.py |
Dimodifikasi | Validasi SSRF webhook URL dan perbaikan referensi peserta att.enrollment.participant. |
tests/integration/test_batch3_regression.py |
Baru | 11 kasus uji regresi komprehensif untuk B04, B05, B10, dan R03 (100% lulus). |
3. Bukti Verifikasi Pengujian
A. Test Suite Regresi Batch 3 (tests/integration/test_batch3_regression.py)
==================== test session starts =====================
collected 11 items
tests/integration/test_batch3_regression.py::test_b04_pin_rotation_revokes_unredeemed_launch_auth PASSED [ 9%]
tests/integration/test_batch3_regression.py::test_b04_credential_revocation_also_revokes_launch_auth PASSED [ 18%]
tests/integration/test_batch3_regression.py::test_b04_race_redemption_with_mismatched_generation PASSED [ 27%]
tests/integration/test_batch3_regression.py::test_b05_self_register_rejects_existing_participant_number_mismatch PASSED [ 36%]
tests/integration/test_batch3_regression.py::test_b05_self_register_rejects_existing_email_with_different_number PASSED [ 45%]
tests/integration/test_batch3_regression.py::test_b05_self_register_clean_new_participant_succeeds PASSED [ 54%]
tests/integration/test_batch3_regression.py::test_b10_launch_auth_rate_limiting PASSED [ 63%]
tests/integration/test_batch3_regression.py::test_b10_mfa_verify_rate_limiting PASSED [ 72%]
tests/integration/test_batch3_regression.py::test_b10_mfa_challenge_single_use PASSED [ 81%]
tests/integration/test_batch3_regression.py::test_r03_webhook_ssrf_blocks_private_and_metadata_ips PASSED [ 90%]
tests/integration/test_batch3_regression.py::test_r03_webhook_allows_valid_external_domain PASSED [100%]
=============== 11 passed, 4 warnings in 5.31s ===============
B. Full Backend Integration Test Suite
tests/integration/ -q --tb=short
180 passed, 5 warnings in 88.77s (0:01:28)
C. Frontend Typecheck & Build
npm run build (vue-tsc && vite build)
✓ built in 1.31s with 0 errors