Lewati ke isi

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), RunnerSession dicabut, namun LaunchAuthorization berstatus "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_generation pada tabel launch_authorizations via migrasi Alembic e2f3a4b5c6d7.
  • Di seb_service.py:create_launch_authorization: Mengikat credential_generation=enrollment.credential_generation saat otorisasi diterbitkan.
  • Di participant_service.py: Memperbarui status semua LaunchAuthorization pending (status == "issued") menjadi "revoked" secara atomik saat regenerate_credentials atau revoke_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 kecocokan record.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 berdasarkan participant_number tanpa mencocokkan email atau full_name. Pendaftar dapat mendaftar pada Event B dengan nomor peserta korban di Event A, lalu mengaitkan enrollment liar ke Participant.id korban.
  • Perbaikan:
  • Di participant_service.py:register_participant_public:
    • Jika nomor peserta yang diinput sudah ada pada organizer, periksa kesesuaian email dan full_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.

B10 — Pembatasan Percobaan Tidak Mencakup Launch dan MFA (P1)

  • Akar Masalah:
  • Endpoint /api/v1/secure/launch-authorizations memverifikasi 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 fungsi consume_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: Memasang check_rate_limit(rate_key) dan record_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}") dan record_failure pada verify_mfa, confirm_mfa_enrollment, dan recover_mfa. Percobaan ke-6 diblokir dengan HTTP 429 Too Many Requests beserta header Retry-After.
    • Mengonsumsi challenge_id setelah verifikasi OTP berhasil melalui consume_challenge. Percobaan replay token yang sama seketika ditolak dengan HTTP 401 Unauthorized ("Token MFA telah digunakan sebelumnya").

R03 — Webhook URL LMS Terbuka Terhadap SSRF (P1)

  • Akar Masalah: Staff dengan peran admin tenant dapat memberikan webhook_url arbitrer pada sinkronisasi nilai ke LMS. URL tersebut langsung dipanggil via httpx.Client.post tanpa 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.py dengan fungsi validate_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_lms sebelum koneksi HTTP keluar dibuat.
  • Memperbaiki resolusi peserta dari att.participant menjadi att.enrollment.participant if att.enrollment else None untuk mencegah AttributeError.

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)
Zero regression: Seluruh 180 tes integrasi backend lulus tanpa kegagalan.

C. Frontend Typecheck & Build

npm run build (vue-tsc && vite build)
✓ built in 1.31s with 0 errors