Laporan Remediasi Audit CBT — Batch Aktif: B03 & B06
Tanggal: 29 September 2026
Status Batch: SELESAI & TERVERIFIKASI
Baseline Commit: 8abc4dac0bbc610ea8a38ea32550b45299d4c63d
Lingkungan Pengujian: PostgreSQL 16 disposable (scola_assessment_test)
1. Ringkasan Temuan & Akar Masalah
B03 — Replay autosave mendahului otorisasi objek (P1 - Keamanan)
- Akar Masalah:
Fungsi
autosave_answerpadabackend/app/services/runner_answer_service.pysebelumnya memeriksa riwayat idempotensi (IdempotencyRecord) dan jawaban bernomor revisi sama (Answer) sebelum memvalidasi kepemilikan objekAttempt. Akibatnya, runner terotentikasi lain (foreign runner) dapat menebak atau mengirimkanattempt_idmilik korban dengan Idempotency-Key atau jawaban yang cocok, lalu memperoleh respons HTTP 200 ACK (is_idempotent_replay=True) besertaserver_saved_atkorban tanpa verifikasi enrollment, active device, maupun SEB attestation. - Perbaikan:
- Penguncian dan otorisasi objek
Attempt(with_for_update()) dipindahkan ke baris pertama sebelum evaluasi replay apapun. - Memverifikasi secara fail-closed bahwa
Attempt.enrollment_id == runner.get("enr"). Jika tidak cocok/tidak ada, kembalikan HTTP 404 Not Found. - Memanggil
require_active_device(attempt, runner)untuk memastikanactive_device_session_idcocok dengan token runner. - Memverifikasi persyaratan SEB (
is_seb_required) sebelum mengizinkan evaluasi replay. - Setelah otorisasi objek dipastikan lolos, fungsi helper
_resolve_committed_replaymengevaluasi replay yang sah terhadapIdempotencyRecordatauAnsweryang telah ter-commit.
B06 — Lock wait melewati deadline tetap menerima revisi baru (P1 - Integritas)
- Akar Masalah:
Fungsi
autosave_answersebelumnya mengambil waktu servernow = datetime.now(timezone.utc)di awal eksekusi sebelum querywith_for_update(). Ketika antrean konkurensi (lock contention) menahan proses autosave hingga melewatiserver_deadline, pemeriksaannow >= attempt.server_deadlinetetap mengevaluasi timestamp lama sebelum lock didapat. Jawaban revisi baru yang antre tetap diterima dan dicatat dengan timestamp pra-lock yang tidak akurat. - Perbaikan:
- Waktu server dievaluasi secara ketat setelah row lock
Attemptberhasil diperoleh dan antrean lock selesai:now = datetime.now(timezone.utc). - Pemeriksaan status attempt (
in_progress) dan batas waktu (now >= attempt.server_deadline) dilakukan terhadap waktu aktual pasca-lock untuk revisi baru. - Replay jawaban yang sudah ter-commit (committed pre-deadline answers) tetap dapat di-ACK bagi owner yang sah bahkan setelah deadline terlewati (sesuai spesifikasi API Contract §4 dan Acceptance Matrix AC-08).
- Penyimpanan baris baru ke tabel
answersdanidempotency_recordsmenggunakan timestamp aktualnowpasca-lock.
2. File yang Diubah
| File | Status | Keterangan Perubahan |
|---|---|---|
backend/app/services/runner_answer_service.py |
Diubah | Memisahkan helper _resolve_committed_replay, mengunci dan mengotorisasi Attempt di awal, mengambil now aktual setelah lock, menolak revisi baru setelah deadline. |
tests/integration/test_b03_b06_regression.py |
Ditambahkan | Suite regression test terisolasi dengan 8 kasus uji (B03 & B06) menggunakan database PostgreSQL disposable. |
3. Bukti Pengujian Sebelum dan Sesudah Patch
Pengujian Baseline (Sebelum Patch)
Perintah yang dijalankan:
SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes \
TEST_DATABASE_URL="postgresql://cbt@/scola_assessment_test?host=/var/run/postgresql" \
PYTHONPATH=backend \
.venv/bin/pytest tests/integration/test_b03_b06_regression.py -v
test_b03_foreign_runner_cannot_replay_via_idempotency_key -> FAILED (Mendapatkan HTTP 200 dari attempt milik peserta lain)
- test_b03_foreign_runner_cannot_replay_via_same_answer -> FAILED (Mendapatkan HTTP 200 dan membocorkan timestamp korban)
- test_b03_inactive_device_cannot_replay -> FAILED (Perangkat tidak aktif mendapatkan HTTP 200 replay)
- test_b03_unverified_seb_cannot_replay_when_seb_required -> FAILED (Runner non-SEB mendapatkan HTTP 200 replay pada sesi wajib SEB)
- test_b03_valid_owner_can_replay_committed_answer -> PASSED
- test_b06_lock_wait_straddling_deadline_rejects_new_revision -> FAILED (Mendapatkan HTTP 200 ACK dengan timestamp pra-lock setelah deadline terlewati)
- test_b06_save_before_deadline_succeeds -> PASSED
- test_b06_replay_of_committed_answer_after_deadline_succeeds -> PASSED
Hasil: 5 FAILED, 3 PASSED (Bukti konfirmasi cacat baseline di PostgreSQL).
Pengujian Pasca-Patch (Sesudah Patch)
Perintah yang dijalankan:
SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes \
TEST_DATABASE_URL="postgresql://cbt@/scola_assessment_test?host=/var/run/postgresql" \
PYTHONPATH=backend \
.venv/bin/pytest tests/integration/test_b03_b06_regression.py -v
test_b03_foreign_runner_cannot_replay_via_idempotency_key -> PASSED (HTTP 404)
- test_b03_foreign_runner_cannot_replay_via_same_answer -> PASSED (HTTP 404)
- test_b03_inactive_device_cannot_replay -> PASSED (HTTP 403)
- test_b03_unverified_seb_cannot_replay_when_seb_required -> PASSED (HTTP 403)
- test_b03_valid_owner_can_replay_committed_answer -> PASSED (HTTP 200)
- test_b06_lock_wait_straddling_deadline_rejects_new_revision -> PASSED (HTTP 400 Bad Request, tidak ada row baru)
- test_b06_save_before_deadline_succeeds -> PASSED (HTTP 200 ACK)
- test_b06_replay_of_committed_answer_after_deadline_succeeds -> PASSED (HTTP 200 ACK)
Hasil: 8 PASSED (100%)
Verifikasi Full Regression Suite
Perintah yang dijalankan:
SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes \
TEST_DATABASE_URL="postgresql://cbt@/scola_assessment_test?host=/var/run/postgresql" \
PYTHONPATH=backend \
.venv/bin/pytest tests/integration/ -q --tb=short
(Semua 160 tes integrasi existing tetap lulus tanpa regresi, ditambah 8 tes baru untuk B03 & B06).
4. Pemenuhan Kriteria Penerimaan
| Kriteria Penerimaan | Status | Bukti Verifikasi |
|---|---|---|
| B03: Owner yang sah dapat mereplay jawaban committed | TERPENUHI | test_b03_valid_owner_can_replay_committed_answer lulus (HTTP 200, is_idempotent_replay=True). |
| B03: Peserta/enrollment lain ditolak via key maupun same-answer | TERPENUHI | test_b03_foreign_runner_cannot_replay_via_idempotency_key dan test_b03_foreign_runner_cannot_replay_via_same_answer mengembalikan HTTP 404. |
| B03: Fingerprint yang sama tidak menggantikan pemeriksaan kepemilikan | TERPENUHI | Otorisasi objek Attempt dilakukan terhadap runner["enr"] dan active_device_session_id sebelum pemrosesan fingerprint. |
| B03: Tidak ada kebocoran ACK/timestamp ke pihak tidak berwenang | TERPENUHI | Permintaan dari runner tidak sah diputus di awal tanpa membaca atau mengembalikan metadata jawaban. |
| B06: Request revisi baru menunggu lock melewati deadline ditolak | TERPENUHI | test_b06_lock_wait_straddling_deadline_rejects_new_revision mengembalikan HTTP 400 Bad Request. |
| B06: Tidak ada baris Answer/IdempotencyRecord baru dari request ditolak | TERPENUHI | Verifikasi query DB memastikan jumlah Answer tetap 1 (revisi 2 tidak masuk DB). |
| B06: Save sebelum deadline tetap berhasil | TERPENUHI | test_b06_save_before_deadline_succeeds lulus (HTTP 200, is_idempotent_replay=False). |
| B06: Replay committed yang sah tetap mengikuti kontrak setelah deadline | TERPENUHI | test_b06_replay_of_committed_answer_after_deadline_succeeds lulus (HTTP 200, is_idempotent_replay=True). |
| B06: Waktu aktual diperbarui setelah lock diperoleh | TERPENUHI | now = datetime.now(timezone.utc) dipanggil tepat setelah baris Attempt di-lock via .with_for_update(). |
5. Risiko Tersisa & Backlog di Luar Scope
Risiko Tersisa pada Jalur Runner:
- Penanganan antrean retry lokal pada browser peserta (frontend) saat terjadi gangguan jaringan belum memiliki sync queue terstruktur (B07, B08).
Backlog Temuan di Luar Scope Batch (Tidak Diubah):
- B01 / B02: Launcher config key attestation & ordinary HTTP client bypassing SEB desktop validation.
- B04: Rotasi PIN tidak mencabut launch authorization pending.
- B05: Self-registration event mengambil identitas peserta existing.
- B07 / B08 / B09: Debounce save lintas soal, penimpaan state ACK lokal, dan timer drift pada UI.
- B10 / B11 / B12: Penyesuaian LMS grade sync, sweeper function parameter signature, dan import soal numerik NaN.