89 lines
8.1 KiB
Markdown
89 lines
8.1 KiB
Markdown
# 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.
|