Lewati ke isi

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_answer pada backend/app/services/runner_answer_service.py sebelumnya memeriksa riwayat idempotensi (IdempotencyRecord) dan jawaban bernomor revisi sama (Answer) sebelum memvalidasi kepemilikan objek Attempt. Akibatnya, runner terotentikasi lain (foreign runner) dapat menebak atau mengirimkan attempt_id milik korban dengan Idempotency-Key atau jawaban yang cocok, lalu memperoleh respons HTTP 200 ACK (is_idempotent_replay=True) beserta server_saved_at korban 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 memastikan active_device_session_id cocok dengan token runner.
  • Memverifikasi persyaratan SEB (is_seb_required) sebelum mengizinkan evaluasi replay.
  • Setelah otorisasi objek dipastikan lolos, fungsi helper _resolve_committed_replay mengevaluasi replay yang sah terhadap IdempotencyRecord atau Answer yang telah ter-commit.

B06 — Lock wait melewati deadline tetap menerima revisi baru (P1 - Integritas)

  • Akar Masalah: Fungsi autosave_answer sebelumnya mengambil waktu server now = datetime.now(timezone.utc) di awal eksekusi sebelum query with_for_update(). Ketika antrean konkurensi (lock contention) menahan proses autosave hingga melewati server_deadline, pemeriksaan now >= attempt.server_deadline tetap 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 Attempt berhasil 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 answers dan idempotency_records menggunakan timestamp aktual now pasca-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
Hasil: - 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
Hasil: - 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
Hasil: 168 passed, 5 warnings in 77.77s (0:01:17)
(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):

  1. B01 / B02: Launcher config key attestation & ordinary HTTP client bypassing SEB desktop validation.
  2. B04: Rotasi PIN tidak mencabut launch authorization pending.
  3. B05: Self-registration event mengambil identitas peserta existing.
  4. B07 / B08 / B09: Debounce save lintas soal, penimpaan state ACK lokal, dan timer drift pada UI.
  5. B10 / B11 / B12: Penyesuaian LMS grade sync, sweeper function parameter signature, dan import soal numerik NaN.