This commit is contained in:
DIGIDEV\corer committed 2026-10-20 13:16:38 +09:00
commit 3e4a056b14
67 files changed
+13926

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
# Keamanan — Kalkulator Cashback SERUBI
Ditulis untuk: admin server yang men-deploy aplikasi ke internet.
## Sudah ditangani di aplikasi (berlaku otomatis setelah file diunggah)
| Area | Perlindungan |
|---|---|
| Header keamanan | CSP ketat dengan nonce (tanpa `unsafe-inline`), `X-Frame-Options: DENY`, `frame-ancestors 'none'`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`, COOP/CORP. HSTS aktif otomatis saat diakses lewat HTTPS. |
| XSS | Semua output di-escape (`e()`); input bertipe array dibuang; nama admin menolak `< >` dan karakter kontrol. |
| SQL injection | Semua query memakai PDO prepared statement, dengan emulasi prepare dimatikan. |
| CSRF | Token per sesi di setiap form POST, dibandingkan dengan `hash_equals`; cookie `SameSite=Strict`. Token diganti saat login. |
| Login | CAPTCHA self-hosted (gambar dari server, sekali pakai, 5 menit) diperiksa sebelum password. Kunci 15 menit setelah 5 kali gagal per IP. Tidak ada kunci per username (mencegah akun admin dikunci orang lain); kegagalan beruntun dicatat di Riwayat dan diberitahukan ke pemilik akun saat login berikutnya. Pesan error sama untuk username atau password salah; hash palsu supaya waktu respons tidak membocorkan username. |
| Dua faktor (2FA) | Wajib untuk semua admin: TOTP (RFC 6238) dari Google/Microsoft Authenticator. Diaktifkan saat login pertama (QR code dibuat lokal, tanpa layanan luar). Rahasia OTP dienkripsi AES-256-GCM dengan `app_key`; kode tidak bisa dipakai ulang; 5 kode salah membatalkan login; 10 kode pemulihan sekali pakai. Sesi admin baru dibuat setelah OTP benar. |
| Hak akses admin | Peran **super admin** dan **admin**. Hanya super admin yang dapat menambah, menonaktifkan, me-reset password/OTP, dan mengubah peran admin lain, dan setiap aksi itu meminta password super admin (re-autentikasi). Password hasil reset wajib diganti saat login berikutnya. Super admin aktif terakhir tidak dapat dinonaktifkan atau diturunkan. |
| Parameter bank | Halaman publik menghitung di browser (instan, tetap jalan offline) dengan **model koefisien**: biaya internal dilebur menjadi satu angka margin per tenor (`engine_client_model()`, `assets/js/engine-public.js`). GWM, kas, porsi PUAB/kredit, rate PUAB, bunga kredit, OHC, dan rate deposito tidak dikirim. **Cetak struk** dan **Salin ringkasan** mengambil angka resmi dari server (`calc.php`); tanpa koneksi ditandai "belum terverifikasi server". Rincian perhitungan hanya untuk admin yang login. |
| Kredensial | Tidak ada password di `config.php`. Kredensial database dan `app_key` dibaca dari environment variable atau berkas `.serubi/secrets.php`: di luar document root bila memungkinkan, atau di folder `.serubi/` dalam folder aplikasi yang ditolak dari akses HTTP (403). Contoh: `database/secrets.example.php`. |
| Beban database | Tidak ada migrasi otomatis per request; parameter di-cache (5 menit, dihapus otomatis setiap admin menyimpan).
| Password | 10–72 karakter, berisi huruf, angka, dan simbol, dan tidak boleh memuat username. Disimpan dengan bcrypt (`password_hash`). |
| Sesi | ID baru setiap kali login dan setiap 15 menit; habis setelah 30 menit tidak aktif atau 8 jam sejak login; `HttpOnly`, `Secure` (di HTTPS), strict mode. **Satu sesi aktif per akun**: login di browser/perangkat lain otomatis mengakhiri sesi sebelumnya (dengan pesan). Mengganti password juga mengakhiri sesi lain akun tersebut. |
| Admin pertama | Form "buat admin pertama" mati secara default. Admin pertama dibuat lewat konsol (`--create-admin`), atau form diaktifkan sementara dengan `setup_key` minimal 16 karakter. |
| Bocoran informasi | Error PHP tidak pernah ditampilkan (hanya dicatat di log); halaman error polos; `X-Powered-By` dihapus. |
| Method HTTP | Hanya GET/HEAD/POST yang diterima; PUT/DELETE/OPTIONS/PATCH/TRACK ditolak. |
| File internal | `config.php`, `includes/`, `database/`, `tests/`, `docs/`, file `.md/.sql/.json/.bak/~`, dan semua dotfile (`.git`, `.env`, …) diblokir. Listing folder dimatikan. |
| Input berlebih | Maksimal 30 baris per tabel; nominal maksimal Rp1.000 triliun. |
| Cache | Halaman admin dikirim dengan `no-store`. |
## WAJIB dikerjakan di server
Bagian ini **tidak bisa** diatur dari aplikasi. Biasanya inilah sumber temuan pentest yang tersisa.
1. **HTTPS.** Pasang sertifikat TLS, lalu aktifkan pengalihan HTTP→HTTPS lewat environment `SERUBI_FORCE_HTTPS=1` atau `'force_https' => true` di `config.php`. Izinkan hanya TLS 1.2/1.3:
```apache
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLHonorCipherOrder off
```
Kalau aplikasi berada di belakang reverse proxy atau load balancer, isi IP proxy tersebut di `SERUBI_TRUSTED_PROXIES`.
2. **Sembunyikan versi dan matikan TRACE** di `httpd.conf`. Pengaturan ini tidak bisa lewat `.htaccess`:
```apache
ServerTokens Prod
ServerSignature Off
TraceEnable Off
```
3. **php.ini produksi:**
```ini
expose_php = Off
display_errors = Off
log_errors = On
session.cookie_secure = 1
session.use_strict_mode = 1
```
4. **Perbarui software.** PHP 8.1 sudah *end-of-life* sejak 31 Desember 2025, dan OpenSSL 1.1.1 sejak September 2023. Pentest akan melaporkan versi yang usang. Gunakan PHP 8.3 atau 8.4, Apache 2.4 versi terbaru, dan OpenSSL 3.x. Aplikasi ini kompatibel dengan PHP 7.4 sampai 8.4.
5. **Jangan pakai user `root` untuk MySQL.** Jalankan sekali dengan akun root:
```
php database/migrate.php --create-db-user
```
Perintah ini membuat user `serubi_app` yang hanya punya hak SELECT/INSERT/UPDATE/DELETE pada database `calcserubi`. Salin user dan password yang dicetak ke `config.php` atau ke environment `SERUBI_DB_USER`/`SERUBI_DB_PASS`. Setelah itu ganti juga password `root` MySQL, karena password yang sekarang pernah tertulis di config.
6. **MySQL jangan terbuka ke internet.** Set `bind-address = 127.0.0.1` dan tutup port 3306/3309 di firewall.
7. **Hak akses file.** File aplikasi dibuat read-only untuk user web server. `config.php` hanya bisa dibaca oleh user web server (misalnya `chmod 640`).
8. **Folder yang tidak perlu ada di server produksi** (opsional, karena sudah diblokir): `tests/`, `docs/`, `.impeccable/`, `PRODUCT.md`, `DESIGN.md`.
9. **AllowOverride.** Pastikan `AllowOverride All` (atau minimal `AuthConfig FileInfo Indexes Limit Options`) untuk folder aplikasi. Tanpa itu `.htaccess` diabaikan dan file internal bisa terbuka. Cek dengan membuka `https://<server>/config.php`; hasilnya harus 403/404.
## Cek cepat setelah deploy
```
curl -sI https://<server>/ | grep -iE "server|x-powered|content-security|strict-transport"
curl -s -o /dev/null -w "%{http_code}\n" https://<server>/config.php # 403
curl -s -o /dev/null -w "%{http_code}\n" -X TRACE https://<server>/ # 403/405
curl -s -o /dev/null -w "%{http_code}\n" https://<server>/.git/config # 404
```
Hasil yang diharapkan:
- `Server: Apache`, tanpa nomor versi.
- Tidak ada `X-Powered-By`.
- Ada `Content-Security-Policy` dan `Strict-Transport-Security`.
## Upgrade ke versi 2.3.0 (sekali)
1. Jalankan `database/upgrade-2.3.0.sql` di phpMyAdmin (tab SQL). Admin aktif paling lama menjadi super admin; periksa dengan `SELECT id, username, role FROM admins;`.
2. Buat `secrets.php` dari `database/secrets.example.php` (chmod 600), isi kredensial database dan `app_key`. Letakkan di `/home/<akun>/.serubi/` bila ada akses, atau di `<folder aplikasi>/.serubi/` (folder ini sudah berisi `.htaccess` yang menolak akses HTTP). Cek: `https://<server>/.serubi/secrets.php` harus 403.
3. Batasi user database aplikasi di cPanel menjadi SELECT, INSERT, UPDATE, DELETE saja.
4. Unggah file aplikasi. Setiap admin akan diminta mengaktifkan authenticator saat login berikutnya.
5. Uji: `php tests/validate_engine.php` dan `node tests/validate.js` harus 4.805/4.805; `node tests/validate_public.js` harus 3.100/3.100.
6. Mode offline: `sw.js` (service worker) menyimpan halaman kalkulator. Model berumur lebih dari 30 hari ditolak; perangkat harus online untuk memperbarui.
## Tindakan untuk akun admin yang sudah ada
Aturan password baru berlaku saat password dibuat atau diganti. Akun admin yang dibuat sebelum update ini sebaiknya **mengganti password** lewat menu *Pengguna admin → Password Anda*. Setelah update, semua admin yang sedang login akan diminta login ulang satu kali.
+52
View File
@@ -0,0 +1,52 @@
# Validasi Perhitungan vs Excel
Sumber acuan: **CALCULATOR CASHBACK SERUB KANTOR CABANG 22092026.xlsx** (sheet `COB` dan `CALCULATOR CASHBACK SERUBI`).
## Metode
1. `tests/export-excel-fixtures.ps1` membuka salinan workbook di **Microsoft Excel**, mengisi nominal (A6) dan jangka waktu (B6), lalu membiarkan Excel menghitung ulang. Ada 31 nominal × 5 tenor = **155 skenario**, termasuk batas-batas tier (49.999.999 / 50.000.000, 250.000.000 / 250.000.001, 1.999.999.999 / 2.000.000.000, dan seterusnya). Hasilnya disimpan di `tests/excel-fixtures.json`.
2. `tests/validate.js` menjalankan engine aplikasi (`assets/js/engine.js`) dengan **parameter yang dibaca dari MySQL**, lalu membandingkan 31 angka per skenario: cashback bruto/neto, bunga neto tiap produk, deposito, total diterima, rate, total rate, pinalti, porsi, pendapatan, dan spread.
- Nominal rupiah harus sama sampai rupiah (selisih < Rp0,01).
- Rate harus sama sampai 1e-9.
Cara menjalankan:
```
powershell -ExecutionPolicy Bypass -File tests\export-excel-fixtures.ps1 # perlu Excel
node tests\validate.js --legacy
```
## Hasil
| Yang diuji | Cocok dengan Excel |
|---|---|
| Aplikasi baru (parameter dari MySQL) | **4.805 / 4.805 cek (100%)** |
| Aplikasi HTML lama | 2.870 / 3.410 cek |
Contoh: Rp55.000.000, 6 bulan, perorangan. Angka Excel sama persis dengan aplikasi.
| | Excel | Aplikasi |
|---|---|---|
| Cashback bruto | 915.963,68 | 915.963,68 |
| Cashback neto | 732.770,94 | 732.770,94 |
| Ekuivalen per tahun | 2,7016% | 2,7016% |
| Total SIMANJA | 895.510,14 | 895.510,14 |
| Deposito neto | 694.356 | 694.356 |
| Total kewajiban pinalti | 1.099.156,42 | 1.099.156,42 |
## Temuan pada aplikasi HTML lama (sudah diperbaiki)
1. **Baris DEPOSITO, kolom "Bunga Neto" menampilkan bunga kotor.** Salah di 140 dari 155 skenario. Contoh Rp55 jt / 6 bulan: tertulis Rp867.945, padahal bunga neto (Excel H6) Rp694.356.
2. **Bagian pinalti cashback (Excel A15:B22) tidak ada.** Sekarang tampil di bagian "Bila dicairkan sebelum jatuh tempo".
3. **Pilihan "Jenis Nasabah" tidak berpengaruh.** Sekarang memakai pajak cashback perorangan atau badan usaha (keduanya 20%, sesuai U85/U86).
4. Semua parameter tertanam di kode. Sekarang semuanya ada di database dan bisa diubah lewat halaman admin.
Perhitungan cashback bruto/neto pada aplikasi lama sendiri sudah benar.
## Catatan pada file Excel (tidak diubah, mohon dikonfirmasi)
- **Premi LPS dihitung penuh 0,20% tanpa melihat jangka waktu** (`COB!J27 = MIN(nominal; 2 M) × 0,2%`), padahal premi ini bersifat tahunan. Aplikasi mengikuti Excel apa adanya.
- **Pajak cashback di `COB!H8` ditulis langsung 20%,** tidak merujuk ke U85/U86. Aplikasi memakai parameter per jenis nasabah. Nilainya saat ini sama.
- Untuk tenor 1 bulan atau nominal < Rp50 jt, Excel menghasilkan `#VALUE!`. Aplikasi menampilkan "Tanpa cashback" beserta alasannya.
- Parameter **LPS 3,75% (U79) tidak dipakai** di rumus mana pun.
- Giro dengan nominal tepat Rp50.000.000 mendapat rate 0% (tier "≤ 50 jt"), padahal cashback mulai berlaku di nominal yang sama.