8.7 KiB
8.7 KiB
SECURITY_MODEL.md — Speech2Text MBIP
Prinsip Asas Keselamatan
Sistem ini direka untuk menyimpan data sensitif kerajaan/PBT. Prinsip berikut mesti dipatuhi di setiap lapisan:
- Privacy by Design — Admin ialah pentadbir akaun, bukan pentadbir kandungan.
- Defense in Depth — Authorization dikuatkuasakan di Policy, Controller, Query, dan UI.
- Least Privilege — Setiap role hanya mendapat akses minimum yang diperlukan.
- Audit Everything — Semua tindakan sensitif direkodkan dengan lengkap.
- 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
// 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
view() → owner ATAU collaborator (bukan admin)
create() → owner ATAU collaborator
delete() → owner komen sendiri ATAU owner projek
TranscriptVersionPolicy
view() → owner ATAU collaborator
restore() → owner ATAU collaborator
UserPolicy
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_activecheck di middlewareEnsureUserIsActive- 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_MBdalam.env - Rate limit upload: 10 upload per jam per user
L4: Storage Security
- Fail audio tidak disimpan dalam
public/ataustorage/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: inlineuntuk 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
// 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
// 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
// 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:
// Model TranscriptionProject
protected $casts = [
'transcript_text' => 'encrypted',
];
- Data dienkripsi menggunakan
APP_KEYsebelum simpan dalam database. - Decrypt berlaku secara automatik apabila diakses melalui Eloquent.
- Tanpa
APP_KEY, data tidak boleh dibaca walaupun database dicuri. - Penting: Simpan
APP_KEYdalam.envdan jangan commit ke git.
Semakan Keselamatan Deployment
Sebelum production, semak:
APP_DEBUG=falseAPP_ENV=production.envtidak dalam git repository (ada dalam.gitignore)APP_KEYtelah 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:linkhanya 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
- Backup
APP_KEY— JikaAPP_KEYhilang, semuatranscript_textyang dienkripsi tidak boleh dibaca. - Backup database secara berkala — Termasuk
transcript_versionsyang menyimpan history. - Log rotation — Pastikan
storage/logs/dirotate supaya tidak penuh. - Retention policy — Tentukan berapa lama fail audio disimpan selepas projek deleted.
- Monitor disk space — Audio files boleh membesar. Tetapkan alert.