Dasbor log keamanan server menampilkan peringatan pemblokiran eksekusi file shell PHP pada folder upload

Celah Keamanan Fatal File Upload di CI4: Autopsi Injeksi Shell PHP

Malam jumat kliwon bulan lalu, alarm monitoring server saya meledak. Bukan karena lonjakan trafik organik, tapi CPU usage tiba-tiba mentok 100% dan ada ribuan request aneh mengarah ke folder direktori upload aplikasi klien saya. Aplikasi ini dibangun pakai CodeIgniter 4. Waktu saya bedah log servernya, saya lemas. Ada file bernama foto_profil.jpg.php nangkring dengan manis di dalam folder /public/uploads/users/.

Hacker berhasil melakukan Remote Code Execution (RCE). Server klien saya yang spesifikasinya lumayan gahar itu mendadak jadi budak mesin penambang crypto (cryptojacking). Sistem admin yang saya tangani hampir rata tanah. Persis seperti kejadian Peretasan Massal Server Hosting B2B yang pernah saya bahas beberapa waktu lalu. Dan Anda tahu apa yang paling bikin sakit hati? Kode validasi upload di CI4 yang saya tulis saat itu sudah mengecek ekstensi dan MIME type.

Kenyataan pahitnya begini. Mengandalkan validasi MIME type bawaan framework itu seperti menyewa satpam kompleks yang cuma ngecek sampul KTP tamu tanpa pernah menggeledah isi bagasi mobilnya. Siapapun bisa mencetak KTP palsu. Siapapun bisa memanipulasi header HTTP. Di dunia cybersecurity, kita menyebutnya MIME Spoofing.

Banyak developer pemula merasa aman setelah pakai aturan is_image[avatar] atau ext_in[avatar,png,jpg,jpeg] di CodeIgniter. Kalau aplikasi Anda cuma buat tugas kampus, silakan. Tapi kalau Anda menangani data sensitif korporasi, membiarkan fitur file upload berjalan tanpa pengamanan level biner adalah sebuah bunuh diri massal. Kita harus membedah operasi pengamanan ini sampai ke level DNA filenya.

Mengapa Validasi MIME-Type CI4 Saja Adalah Lelucon

Mari kita bicara teknis kotornya. Saat browser mengirim file ke server melalui form multipart/form-data, browser akan menyertakan informasi header Content-Type. Misalnya image/jpeg. Fungsi bawaan PHP dan banyak fungsi framework membaca header ini untuk menentukan apakah file tersebut valid.

Masalahnya, HTTP header itu dikontrol sepenuhnya oleh sisi klien (pengguna). Seorang attacker amatir sekalipun hanya butuh tool gratis seperti Burp Suite atau Postman untuk mencegat (intercept) request tersebut sebelum sampai ke server Anda.

Penyerang membuat file bernama shell.php yang isinya skrip ganas untuk mengambil alih server. Lalu dia mengganti namanya menjadi avatar.png.php. Saat di-upload, dia mencegat request tersebut pakai Burp Suite dan mengubah baris Content-Type: application/x-httpd-php menjadi Content-Type: image/png.

Apa yang terjadi di server? CI4 akan melihat Content-Type tersebut, mencocokkannya dengan aturan validasi, dan berkata “Oh, ini file gambar PNG. Silakan masuk!” Boom. Server Anda tamat. File tersebut akan dieksekusi oleh mesin PHP begitu ada orang yang mengakses URL filenya.

Manipulasi HTTP Request header MIME type menggunakan tool Burp Suite untuk bypass keamanan upload
Manipulasi HTTP Request header MIME type menggunakan tool Burp Suite untuk bypass keamanan upload

Inilah yang membuat saya muak melihat tutorial-tutorial usang di YouTube yang hanya mengajarkan validasi ekstensi. Langkah ini sangat sejalan dengan prinsip Keamanan Dewa PHP Native & CI4 yang selalu saya tekankan. Anda tidak bisa percaya pada input dari luar. Titik.

Standar Keamanan File Upload Berdasarkan OWASP

Standar keamanan file upload mewajibkan validasi berlapis untuk mencegah eksekusi kode berbahaya. Menurut riset dari OWASP File Upload Cheat Sheet versi terbaru, validasi ekstensi dan MIME type sangat mudah dimanipulasi oleh penyerang.

Untuk mengamankan fitur unggah file, pengembang wajib mematuhi aturan mitigasi absolut berikut:

  • Verifikasi ekstensi file menggunakan daftar putih (whitelist) yang ketat.
  • Validasi konten file secara mendalam berdasarkan signature hex atau magic bytes.
  • Simpan file yang diunggah di luar direktori root web publik.
  • Ubah nama file secara acak (randomized string) saat disimpan di server.

Bedah Forensik: Pengecekan Signature File Hex (Magic Bytes)

Jika kita tidak bisa percaya pada nama file, dan kita tidak bisa percaya pada MIME type dari HTTP header, lalu siapa yang bisa kita percaya? Jawabannya ada pada struktur biner file itu sendiri. Inilah yang disebut dengan Magic Bytes atau File Signature.

Setiap file digital yang sah memiliki serangkaian byte spesifik di bagian paling awal (header) file tersebut yang mengidentifikasi format aslinya. Tidak peduli Anda mengganti nama dokumen.pdf menjadi lagu.mp3, magic bytes di dalam file tersebut tidak akan berubah kecuali Anda merusaknya pakai hex editor.

Berikut adalah beberapa contoh Magic Bytes (dalam format Hexadecimal) untuk file gambar umum:

  • JPEG / JPG: FF D8 FF E0 atau FF D8 FF E1
  • PNG: 89 50 4E 47 0D 0A 1A 0A
  • GIF: 47 49 46 38 37 61 (GIF87a) atau 47 49 46 38 39 61 (GIF89a)
  • PDF: 25 50 44 46 2D (%PDF-)

Tugas kita sebagai developer yang paranoid adalah membaca beberapa byte pertama dari file yang di-upload (saat masih berada di folder temporary server), mencocokkannya dengan daftar magic bytes yang kita izinkan, dan menendang file tersebut jika tidak cocok.

Snippet Kode Validasi Ketat di CodeIgniter 4 (Copy-Paste Ready)

Jujur saja, fitur file validation bawaan framework apapun itu seperti satpam kompleks yang cuma ngecek KTP tanpa periksa isi bagasi. Buat saya pribadi, kalau aplikasi Anda menangani data rahasia klien B2B, percaya 100% pada fungsi bawaan adalah sebuah kelalaian fatal. Anda harus main kotor di level bits dan bytes.

Berikut adalah contoh implementasi pada Controller CI4 yang menggabungkan validasi bawaan dengan pengecekan magic bytes tingkat rendah.

<?php

namespace App\Controllers;
use CodeIgniter\Controller;

class UploadSecurity extends Controller
{
    public function processUpload()
    {
        $file = $this->request->getFile('dokumen_penting');

        // Lapis 1: Validasi Bawaan CI4 (Hanya sebagai filter awal yang murah)
        $validationRule = [
            'dokumen_penting' => [
                'label' => 'File Dokumen',
                'rules' => 'uploaded[dokumen_penting]'
                    . '|is_image[dokumen_penting]'
                    . '|mime_in[dokumen_penting,image/jpg,image/jpeg,image/png]'
                    . '|ext_in[dokumen_penting,jpg,jpeg,png]'
                    . '|max_size[dokumen_penting,2048]',
            ],
        ];

        if (! $this->validate($validationRule)) {
            return $this->response->setJSON(['status' => 'error', 'message' => $this->validator->getErrors()]);
        }

        if (! $file->hasMoved()) {
            
            $tempPath = $file->getTempName();
            
            // Lapis 2: Pengecekan Magic Bytes (Hex Signature)
            if (!$this->verifyMagicBytes($tempPath)) {
                // Hapus file temp bodong tersebut
                @unlink($tempPath);
                return $this->response->setJSON(['status' => 'error', 'message' => 'Injeksi terdeteksi. Signature biner file tidak valid!']);
            }

            // Lapis 3: Randomize nama file untuk mencegah tebakan path (Path Traversal)
            $newName = $file->getRandomName();
            
            // Lapis 4: Simpan di LUAR folder public (Karantina)
            // WRITEPATH mengarah ke direktori /writable di CI4, yang tidak bisa diakses langsung via URL browser
            $file->move(WRITEPATH . 'uploads/secure_docs/', $newName);

            return $this->response->setJSON(['status' => 'success', 'message' => 'File bersih dan berhasil diamankan.']);
        }
    }

    /**
     * Fungsi brutal untuk membedah anatomi file
     */
    private function verifyMagicBytes($filePath)
    {
        // Buka file dalam mode read binary
        $handle = fopen($filePath, 'rb');
        if ($handle === false) {
            return false;
        }

        // Baca 8 byte pertama saja (sudah cukup untuk identifikasi kebanyakan file)
        $bytes = fread($handle, 8);
        fclose($handle);

        // Ubah biner jadi representasi string hex
        $hex = strtoupper(bin2hex($bytes));

        // Daftar putih signature yang diizinkan
        $allowedSignatures = [
            'FFD8FFE0', // JPEG
            'FFD8FFE1', // JPEG (EXIF)
            'FFD8FFE2', // JPEG
            'FFD8FFE3', // JPEG
            'FFD8FFE8', // JPEG
            '89504E47', // PNG (4 byte pertama)
        ];

        foreach ($allowedSignatures as $sig) {
            // Cek apakah hex file diawali dengan salah satu signature yang sah
            if (strpos($hex, $sig) === 0) {
                return true;
            }
        }

        return false;
    }
}

Saya kadang tertawa sendiri kalau lihat kode developer yang cuma pakai pathinfo() untuk ngecek ekstensi. Dengan metode verifyMagicBytes di atas, walau hacker me-rename c99shell.php menjadi foto.png dan mengakali MIME type di Burp Suite, skrip PHP kita akan tetap mengunyah file tersebut, melihat byte pertamanya (misal 3C 3F 70 68 70 yang berarti <?php), dan langsung menendangnya karena tidak ada di daftar whitelist hex kita.

Snippet kode PHP CodeIgniter 4 membaca byte biner file menggunakan fopen dan bin2hex untuk verifikasi magic bytes
Snippet kode PHP CodeIgniter 4 membaca byte biner file menggunakan fopen dan bin2hex untuk verifikasi magic bytes

Arsitektur Karantina: Memisahkan Direktori Upload dari Public

Katakanlah hacker menemukan zero-day exploit baru yang bisa menyisipkan kode PHP ke dalam metadata EXIF gambar asli (ini sangat mungkin terjadi). File tersebut adalah JPEG asli secara visual dan secara magic bytes, tapi mengandung payload PHP di kolom “Author” atau “Description”.

Jika Anda menyimpan file tersebut di folder /public/uploads/, hacker tinggal mengetik URL situsanda.com/uploads/foto_injeksi.jpg.php. Web server (Apache/Nginx) akan melihat akhiran .php dan mengeksekusinya.

Solusi arsitektur paling brutal dan paling aman adalah: Jangan pernah simpan file upload di direktori web root.

Di CodeIgniter 4, kita diberkahi dengan direktori /writable/ yang letaknya sejajar dengan /public/. Folder ini sama sekali tidak terekspos ke internet. Jika hacker berhasil mengupload file shell, mereka tidak akan pernah bisa mengakses URL filenya untuk mengeksekusi (trigger) shell tersebut. Mereka hanya menaruh bom waktu yang tidak punya pemantik.

Lalu Bagaimana Cara Menampilkan Gambarnya di Web?

Anda harus membuat Endpoint Controller khusus untuk menyajikan file tersebut (File Serving). Ini bertindak sebagai loket kaca anti peluru antara file asli dan pengunjung web.

public function viewImage($fileName)
{
    // Cegah path traversal attack (misal: user input ../../../etc/passwd)
    if (strpos($fileName, '..') !== false) {
        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }

    $path = WRITEPATH . 'uploads/secure_docs/' . $fileName;

    if (!is_file($path)) {
        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }

    // Set header yang benar secara paksa
    $mime = mime_content_type($path);
    header('Content-Type: ' . $mime);
    header('Content-Disposition: inline; filename="' . $fileName . '"');
    header('X-Content-Type-Options: nosniff'); // Penting untuk cegah MIME sniffing browser
    
    // Baca dan buang output file ke browser
    readfile($path);
    exit;
}

Dengan teknik ini, file diproses melalui engine PHP yang kita atur ketat, bukan dilayani langsung oleh web server. Opsi X-Content-Type-Options: nosniff juga menginstruksikan browser agar tidak sok pintar menebak-nebak tipe file, yang kerap memicu serangan Cross-Site Scripting (XSS) berbasis file.

Web Server Hardening: Sabuk Pengaman Tambahan (Nginx & Apache)

Jika karena alasan legacy atau tuntutan bos Anda terpaksa harus menyimpan file upload di folder /public/, maka Anda harus menghukum direktori tersebut di level web server. Matikan engine PHP di dalam folder upload.

Untuk pengguna Nginx: Tambahkan blok location ini di file konfigurasi server block Anda.

location ^~ /uploads/ {
    # Matikan eksekusi script apapun
    location ~ \.(php|php3|php4|php5|phtml|sh|cgi)$ {
        deny all;
    }
}

Untuk pengguna Apache: Buat file .htaccess dan letakkan tepat di dalam folder /public/uploads/.

# Mematikan engine PHP
php_flag engine off

# Mencegah eksekusi file berbasis ekstensi
<FilesMatch "\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$">
    Require all denied
</FilesMatch>

Sedikit cerita aneh, saya pernah menangani klien yang ngotot aplikasinya mengunakan direktori public karna alasan kecepatan load gambar katalog. Saya terapkan script Nginx di atas. Seminggu kemudian hacker nyerang, upload shell.php sukses. Tapi waktu hacker buka URL shell-nya, yang muncul bukan terminal layar hitam, melainkan cuma teks mentah (source code) dari shell tersebut, atau malah dapat error 403 Forbidden. Hacker-nya pasti frustrasi berat.

Tabel Komparasi Strategi Validasi File Upload

Berikut adalah matriks analisis efektivitas dari berbagai metode yang sering dipakai programmer. Tabel ini merangkum kenapa Anda harus mengkombinasikan semuanya.

Metode ValidasiTingkat Kesulitan Bypass bagi HackerTujuan Utama MencegahKelemahan Fatal (Blind Spot)
Ekstensi File (ext_in)Sangat Rendah (Cukup ganti nama)Kesalahan upload pengguna awamEkstensi ganda (file.php.jpg) atau Null Byte Injection (file.php%00.jpg)
MIME Type HeaderSangat Rendah (Cegat via Burp Suite)Filter dasar tipe dokumenHeader 100% dikontrol oleh input dari penyerang (Client-side manipulation).
Hex Signature (Magic Bytes)Sangat Tinggi (Harus merubah struktur biner)Upload file shell/malware palsuPolyglot files (file yang memvalidasi dua format sekaligus, misal gambar + skrip).
Karantina Direktori (Non-Public)Absolut (Bypass butuh exploit kernel/server)Eksekusi file berbahaya (RCE)Menambah beban I/O server karena file di-serve melalui PHP (readfile).

Membangun sistem itu gampang, mengamankannya dari manusia-manusia jahil di luar sana adalah seni berperang. Jangan sampai kelalaian lima menit saat menulis kode berujung pada kerugian ratusan juta akibat data klien yang bocor dan server yang disandera ransomware.

FAQ

Apa itu serangan Polyglot File dalam konteks upload gambar?

Polyglot file adalah file hasil rekayasa tingkat tinggi yang valid di dua atau lebih format berbeda. Misalnya, sebuah file bisa menjadi gambar JPEG yang valid saat dibuka di image viewer (lolos validasi magic bytes), namun di dalamnya tersisip kode PHP atau Javascript valid yang akan tereksekusi jika diproses oleh interpreter. Ini adalah alasan mengapa menyimpan file di luar folder publik sangat krusial.

Apakah fungsi getimagesize() di PHP cukup aman untuk validasi gambar?

Tidak. Banyak pengembang berasumsi getimagesize() aman karena akan mengembalikan nilai false jika file bukan gambar. Namun, hacker bisa menyisipkan tag <?php phpinfo(); ?> ke dalam baris metadata komentar EXIF dari gambar asli. Fungsi getimagesize() akan menganggapnya sebagai gambar valid, padahal ia membawa racun (payload).

Bagaimana cara mengamankan file upload berupa dokumen PDF atau Excel?

Prinsipnya sama dengan gambar. Gunakan pengecekan Magic Bytes (contoh PDF diawali dengan %PDF-). Namun untuk dokumen Office modern (seperti .xlsx atau .docx), file tersebut sebenarnya adalah file ZIP yang diarsipkan. Validasi biner untuk ZIP sedikit lebih kompleks. Selalu gunakan direktori karantina untuk menyimpan file jenis dokumen bisnis ini.

Similar Posts

Leave a Reply