This commit is contained in:
Saufi
2026-06-02 17:35:45 +08:00
commit 4ef99b1f81
148 changed files with 21134 additions and 0 deletions

277
SECURITY_MODEL.md Normal file
View File

@@ -0,0 +1,277 @@
# SECURITY_MODEL.md — Speech2Text MBIP
## Prinsip Asas Keselamatan
Sistem ini direka untuk menyimpan data sensitif kerajaan/PBT. Prinsip berikut mesti dipatuhi di setiap lapisan:
1. **Privacy by Design** — Admin ialah pentadbir akaun, bukan pentadbir kandungan.
2. **Defense in Depth** — Authorization dikuatkuasakan di Policy, Controller, Query, dan UI.
3. **Least Privilege** — Setiap role hanya mendapat akses minimum yang diperlukan.
4. **Audit Everything** — Semua tindakan sensitif direkodkan dengan lengkap.
5. **No Cloud Egress** — Data tidak keluar dari Docker network.
---
## Model Kebenaran (Authorization)
### Admin — APA yang BOLEH dan TIDAK BOLEH
| Tindakan | Admin |
|---|---|
| Daftar pengguna baru | ✓ |
| Lihat senarai pengguna | ✓ |
| Activate/deactivate pengguna | ✓ |
| Delete pengguna (belum guna) | ✓ |
| Tukar emel pengguna | ✓ |
| Lihat statistik penggunaan (tanpa kandungan) | ✓ |
| Lihat metadata projek (tajuk, owner, status, saiz, tarikh) | ✓ |
| Transfer ownership projek (dengan justifikasi) | ✓ |
| Lihat audit log pentadbiran | ✓ |
| **Dengar / download audio** | ✗ |
| **Baca transcript** | ✗ |
| **Lihat komen dalam projek** | ✗ |
| **Masuk halaman detail projek (kandungan)** | ✗ |
| **Edit transcript** | ✗ |
### Owner (Pengguna Jabatan — pemilik projek)
| Tindakan | Owner |
|---|---|
| Cipta projek | ✓ |
| Upload audio | ✓ |
| Lihat / dengar audio sendiri | ✓ |
| Lihat / edit transcript sendiri | ✓ |
| Tambah / buang collaborator | ✓ |
| Delete projek / audio / teks | ✓ |
| Retry transcription | ✓ |
| Lihat versi history | ✓ |
| Restore versi lama | ✓ |
| Buat komen | ✓ |
### Collaborator (Pengguna yang dikongsi projek)
| Tindakan | Collaborator |
|---|---|
| Lihat projek yang dikongsi | ✓ |
| Dengar audio asal | ✓ |
| Lihat / edit transcript | ✓ |
| Buat komen | ✓ |
| Lihat versi history | ✓ |
| Restore versi lama | ✓ |
| **Delete projek / audio / teks** | ✗ |
| **Transfer ownership** | ✗ |
| **Urus collaborator lain** | ✗ |
---
## Laravel Policies
### `TranscriptionProjectPolicy`
```php
// Setiap method semak sama ada user adalah owner ATAU collaborator
// Admin sengaja TIDAK diberikan akses ke content
view() → owner ATAU collaborator (bukan admin)
create() → user aktif (bukan admin)
update() → owner ATAU collaborator
delete() → owner sahaja
viewAudio() → owner ATAU collaborator
downloadAudio()→ owner ATAU collaborator
retryTranscription() → owner sahaja
manageCollaborators() → owner sahaja
transferOwner() → admin sahaja (metadata action, bukan content)
viewMetadata() → admin ATAU owner (metadata minimum sahaja untuk admin)
```
### `CommentPolicy`
```php
view() → owner ATAU collaborator (bukan admin)
create() → owner ATAU collaborator
delete() → owner komen sendiri ATAU owner projek
```
### `TranscriptVersionPolicy`
```php
view() → owner ATAU collaborator
restore() → owner ATAU collaborator
```
### `UserPolicy`
```php
create() → admin sahaja
update() → admin sahaja
delete() → admin sahaja (dengan semak tiada usage)
activate() → admin sahaja
deactivate() → admin sahaja
changeEmail()→ admin sahaja
```
---
## Lapisan Keselamatan
### L1: Authentication
- Laravel Breeze/custom auth dengan session
- `is_active` check di middleware `EnsureUserIsActive`
- Rate limit login: 5 percubaan per minit per IP
- Session expire: dikonfig dalam `.env`
- CSRF protection aktif untuk semua form
### L2: Authorization (Policy)
- Setiap controller action wajib panggil `$this->authorize()`
- Jangan rely pada UI hide/show sahaja
- Gunakan `abort(403)` jika policy gagal
### L3: Input Validation
- Semua input melalui Form Request class
- File upload: validate MIME type dari magic bytes (bukan extension sahaja)
- Allowed audio types: `audio/mpeg`, `audio/wav`, `audio/mp4`, `audio/x-m4a`, `video/mp4`
- Max file size: dikonfig `PRIVATE_AUDIO_MAX_MB` dalam `.env`
- Rate limit upload: 10 upload per jam per user
### L4: Storage Security
- Fail audio **tidak** disimpan dalam `public/` atau `storage/app/public/`
- Path simpan: `storage/app/private/transcriptions/{project_uuid}/audio/{filename}`
- Semua akses audio melalui controller yang semak policy
- Guna `Storage::disk('private')->get()` untuk stream audio
- Response header `Content-Disposition: inline` untuk audio player
- Response header `X-Content-Type-Options: nosniff`
### L5: IDOR Prevention
- Gunakan UUID dalam URL projek, bukan integer ID
- Route: `/projects/{project:uuid}` bukan `/projects/{id}`
- Policy semak ownership/collaborator, bukan hanya URL
- Semua query pakai `where('owner_user_id', auth()->id())` atau policy scope
### L6: Output Sanitization
- Escape semua output Blade dengan `{{ }}` (bukan `{!! !!}`)
- Sanitize comment sebelum display dengan `htmlspecialchars()`
- Transcript display menggunakan `{{ }}` escape
- Content-Security-Policy header via middleware
### L7: Audit Trail
- Semua tindakan sensitif log ke `audit_logs`
- Log IP address dan user agent
- Log nilai lama dan nilai baru (kecuali transcript content)
- Audit log adalah append-only (tiada update/delete pada audit_logs)
### L8: Admin Content Isolation
```php
// Middleware atau scope yang enforce admin tidak dapat content
class AdminProjectScope {
public function apply($builder, $model) {
if (auth()->user()->role === 'admin') {
// Admin hanya dapat metadata sahaja
$builder->select([
'id', 'uuid', 'title', 'owner_user_id',
'transcription_status', 'created_at',
'file_size', 'duration_seconds', 'deleted_at'
// TIADA transcript_text, stored_audio_path
]);
}
}
}
```
---
## Keselamatan Fail Audio
### Upload Flow
```
1. Validate MIME type (magic bytes, bukan extension)
2. Generate nama fail baru yang random (jangan guna nama asal)
3. Simpan ke path private: transcriptions/{uuid}/audio/{random_name}.{ext}
4. Simpan nama asal dalam database (original_filename)
5. Jangan simpan path dalam session atau cookie
```
### Serve/Stream Audio
```php
// AudioController@stream
public function stream(TranscriptionProject $project)
{
$this->authorize('viewAudio', $project);
$path = $project->stored_audio_path;
// Verify file exists
abort_unless(Storage::disk('private')->exists($path), 404);
return response()->stream(function () use ($path) {
$stream = Storage::disk('private')->readStream($path);
fpassthru($stream);
}, 200, [
'Content-Type' => $project->mime_type,
'Content-Length' => Storage::disk('private')->size($path),
'Content-Disposition' => 'inline',
'Cache-Control' => 'no-store, no-cache, private',
'X-Content-Type-Options' => 'nosniff',
]);
}
```
---
## Konfigurasi Security Header
```php
// middleware/SecurityHeaders.php
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Content-Security-Policy' => "default-src 'self'; ..."
```
---
## Enkripsi Data Sensitif
### Transcript Text Encryption
Gunakan Laravel `encrypted` cast untuk `transcript_text`:
```php
// Model TranscriptionProject
protected $casts = [
'transcript_text' => 'encrypted',
];
```
- Data dienkripsi menggunakan `APP_KEY` sebelum simpan dalam database.
- Decrypt berlaku secara automatik apabila diakses melalui Eloquent.
- Tanpa `APP_KEY`, data tidak boleh dibaca walaupun database dicuri.
- **Penting:** Simpan `APP_KEY` dalam `.env` dan jangan commit ke git.
---
## Semakan Keselamatan Deployment
Sebelum production, semak:
- [ ] `APP_DEBUG=false`
- [ ] `APP_ENV=production`
- [ ] `.env` tidak dalam git repository (ada dalam `.gitignore`)
- [ ] `APP_KEY` telah dijanakan (`php artisan key:generate`)
- [ ] Database password kuat dan berbeza dari default
- [ ] `storage/app/private/` tidak accessible dari web
- [ ] Nginx tidak serve `storage/` secara direct
- [ ] HTTPS/TLS aktif
- [ ] Rate limiting dikonfig
- [ ] Fail upload di luar `public_html`
- [ ] `storage:link` hanya untuk assets (bukan audio/transcript)
- [ ] Queue worker berjalan dengan user yang restricted
- [ ] Transcription worker tidak accessible dari internet (internal only)
- [ ] Ollama tidak accessible dari internet (internal only)
---
## Nota untuk Pentadbir Sistem
1. **Backup `APP_KEY`** — Jika `APP_KEY` hilang, semua `transcript_text` yang dienkripsi tidak boleh dibaca.
2. **Backup database secara berkala** — Termasuk `transcript_versions` yang menyimpan history.
3. **Log rotation** — Pastikan `storage/logs/` dirotate supaya tidak penuh.
4. **Retention policy** — Tentukan berapa lama fail audio disimpan selepas projek deleted.
5. **Monitor disk space** — Audio files boleh membesar. Tetapkan alert.