Files
speech2text/SECURITY_MODEL.md
2026-06-02 17:35:45 +08:00

278 lines
8.7 KiB
Markdown

# 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.