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

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:

  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

// 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_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

// 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_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.