Upload file besar terlihat sederhana di level API, tetapi pada praktiknya sering menjadi sumber masalah performa, keamanan, dan stabilitas layanan. Jika endpoint upload tidak dibatasi dengan benar, satu request dapat menghabiskan memori besar, memenuhi disk sementara, atau membuat worker sibuk terlalu lama. Pada trafik tinggi, masalah ini dapat berkembang menjadi bottleneck serius atau bahkan membuka peluang serangan denial of service (DoS).

Di Go Fiber v3, penanganan upload perlu dilihat sebagai kombinasi beberapa lapisan: batas ukuran body di aplikasi, konfigurasi reverse proxy, mekanisme parsing multipart, penyimpanan sementara, validasi tipe file, sanitasi nama file, dan bila diperlukan, pemeriksaan antivirus. Artikel ini membahas pendekatan yang praktis untuk menerima file CSV dan Excel secara aman, tanpa mengorbankan efisiensi memori.

Memahami Risiko Upload File Besar

Sebelum masuk ke implementasi, penting memahami kenapa upload file perlu diperlakukan khusus. Upload multipart bukan hanya soal membaca byte dari request. Server harus menangani parsing boundary, metadata file, kemungkinan banyak part dalam satu request, serta penyimpanan data sebelum aplikasi memproses isinya.

  • Memori berlebih: jika seluruh body dibaca ke memori, request besar dapat memicu lonjakan penggunaan RAM.
  • Disk sementara penuh: parser multipart sering menggunakan file temporer untuk data besar. Jika tidak dipantau, disk dapat habis.
  • File berbahaya: nama file, ekstensi, dan MIME dapat dimanipulasi.
  • DoS aplikatif: banyak upload besar paralel dapat mengunci CPU, I/O, dan goroutine.
  • Salah asumsi validasi: memeriksa ekstensi file saja tidak cukup untuk memastikan format aman.

Karena itu, desain upload yang baik biasanya menerapkan beberapa lapisan proteksi sekaligus, bukan hanya satu validasi di handler.

Membatasi Ukuran Body di Fiber dan Reverse Proxy

Body limit di aplikasi

Langkah pertama adalah membatasi ukuran request body sedini mungkin. Di Fiber, pembatasan ukuran body mencegah request yang terlalu besar diproses lebih lanjut. Ini penting agar aplikasi tidak menghabiskan resource untuk request yang sejak awal tidak akan diterima.

package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
)

func main() {
    app := fiber.New(fiber.Config{
        BodyLimit: 50 * 1024 * 1024, // 50 MB
    })

    app.Post("/upload", func(c fiber.Ctx) error {
        return c.SendStatus(fiber.StatusOK)
    })

    log.Fatal(app.Listen(":3000"))
}

Jika API hanya menerima CSV dan Excel untuk impor data, tentukan limit yang realistis. Jangan set terlalu besar “untuk berjaga-jaga”. Batas yang terlalu longgar justru memperbesar permukaan serangan.

Catatan: limit sebaiknya ditentukan berdasarkan kebutuhan bisnis, kapasitas instance, dan pola trafik. Misalnya, untuk impor laporan bulanan, 20–50 MB mungkin cukup. Untuk file yang lebih besar, pertimbangkan alur upload ke object storage, bukan langsung ke aplikasi.

Batas di reverse proxy

Body limit di aplikasi saja tidak cukup. Request besar sebaiknya ditolak lebih awal di reverse proxy seperti Nginx agar koneksi dan bandwidth tidak membebani aplikasi.

server {
    listen 80;
    server_name example.com;

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_request_buffering on;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

client_max_body_size menolak request yang melebihi batas sebelum diteruskan ke aplikasi. proxy_read_timeout dan proxy_send_timeout perlu disesuaikan untuk upload lambat atau file besar. Namun, timeout yang terlalu longgar juga dapat dipakai penyerang untuk menahan koneksi lama-lama.

Trade-off buffering di proxy

Beberapa arsitektur mengandalkan buffering di reverse proxy, yang berarti body request diterima dan mungkin disimpan sementara oleh proxy sebelum dikirim ke backend. Ini bisa membantu melindungi aplikasi dari klien lambat, tetapi menambah beban disk atau memori di layer proxy. Pada trafik tinggi, Anda perlu mengukur apakah buffering ini membantu atau justru menjadi bottleneck baru.

Streaming, Penyimpanan Sementara, dan Menghindari Lonjakan Memori

Jangan membaca seluruh file ke memori

Kesalahan umum adalah mengambil file upload lalu memanggil io.ReadAll atau menyalin seluruh kontennya ke []byte. Untuk file besar, pola ini tidak efisien dan berbahaya. Pendekatan yang lebih baik adalah memproses file sebagai stream, atau menyalinnya ke file sementara di disk lalu memproses bertahap.

Untuk workflow impor CSV/Excel, pola aman umumnya seperti ini:

  1. Terima upload multipart.
  2. Validasi metadata dasar dan tipe file.
  3. Simpan ke file sementara dengan nama acak.
  4. Lakukan scanning antivirus opsional.
  5. Proses file secara bertahap atau kirim ke worker/background job.
  6. Hapus file sementara setelah selesai atau jika terjadi error.

Contoh handler upload yang aman untuk CSV/Excel

Contoh berikut menekankan validasi file, sanitasi nama, pembatasan format, dan penyimpanan ke direktori sementara tanpa memuat seluruh isi file ke memori.

package main

import (
    "crypto/rand"
    "encoding/hex"
    "errors"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
    "strings"

    "github.com/gofiber/fiber/v3"
)

var allowedExt = map[string]bool{
    ".csv":  true,
    ".xlsx": true,
    ".xls":  true,
}

var allowedMIMEs = map[string]bool{
    "text/csv": true,
    "application/vnd.ms-excel": true,
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": true,
    "application/octet-stream": true,
}

func randomName() (string, error) {
    b := make([]byte, 16)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return hex.EncodeToString(b), nil
}

func sanitizeExt(filename string) (string, error) {
    ext := strings.ToLower(filepath.Ext(filepath.Base(filename)))
    if !allowedExt[ext] {
        return "", errors.New("ekstensi file tidak diizinkan")
    }
    return ext, nil
}

func detectContentType(file multipart.File) (string, error) {
    buf := make([]byte, 512)
    n, err := file.Read(buf)
    if err != nil && err != io.EOF {
        return "", err
    }
    if _, err := file.Seek(0, io.SeekStart); err != nil {
        return "", err
    }
    return http.DetectContentType(buf[:n]), nil
}

func main() {
    app := fiber.New(fiber.Config{
        BodyLimit: 50 * 1024 * 1024,
    })

    app.Post("/upload", func(c fiber.Ctx) error {
        fh, err := c.FormFile("file")
        if err != nil {
            return fiber.NewError(fiber.StatusBadRequest, "file tidak ditemukan")
        }

        ext, err := sanitizeExt(fh.Filename)
        if err != nil {
            return fiber.NewError(fiber.StatusBadRequest, err.Error())
        }

        src, err := fh.Open()
        if err != nil {
            return fiber.NewError(fiber.StatusInternalServerError, "gagal membuka file upload")
        }
        defer src.Close()

        mimeType, err := detectContentType(src)
        if err != nil {
            return fiber.NewError(fiber.StatusBadRequest, "gagal mendeteksi MIME")
        }
        if !allowedMIMEs[mimeType] {
            return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("MIME tidak diizinkan: %s", mimeType))
        }

        if _, err := src.Seek(0, io.SeekStart); err != nil {
            return fiber.NewError(fiber.StatusInternalServerError, "gagal reset stream file")
        }

        if err := os.MkdirAll("./tmp/uploads", 0o750); err != nil {
            return fiber.NewError(fiber.StatusInternalServerError, "gagal membuat direktori sementara")
        }

        id, err := randomName()
        if err != nil {
            return fiber.NewError(fiber.StatusInternalServerError, "gagal membuat nama file")
        }

        tempPath := filepath.Join("./tmp/uploads", id+ext)
        dst, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o640)
        if err != nil {
            return fiber.NewError(fiber.StatusInternalServerError, "gagal membuat file sementara")
        }
        defer dst.Close()

        if _, err := io.Copy(dst, src); err != nil {
            _ = os.Remove(tempPath)
            return fiber.NewError(fiber.StatusInternalServerError, "gagal menyimpan file")
        }

        return c.Status(fiber.StatusAccepted).JSON(fiber.Map{
            "message":   "file diterima",
            "temp_path": tempPath,
            "mime":      mimeType,
            "size":      fh.Size,
        })
    })

    app.Listen(":3000")
}

Ada beberapa hal penting dari contoh di atas:

  • Ekstensi divalidasi, tetapi tidak dijadikan satu-satunya indikator keamanan.
  • MIME diperiksa dari isi awal file, bukan hanya dari header request klien.
  • Nama file asli tidak dipakai sebagai path penyimpanan. Ini menghindari masalah path traversal, karakter aneh, dan benturan nama file.
  • File disimpan sebagai stream dengan io.Copy, bukan dibaca penuh ke memori.

Keterbatasan validasi MIME

http.DetectContentType berguna sebagai validasi awal, tetapi bukan alat forensik format file. File Excel modern .xlsx sebenarnya adalah arsip ZIP dengan struktur tertentu. Ada kemungkinan MIME terdeteksi generik. Karena itu, validasi ideal untuk file bisnis biasanya berlapis:

  • cek ekstensi yang diizinkan,
  • cek MIME awal,
  • cek apakah file benar-benar bisa diparsing oleh library CSV/Excel yang dipakai,
  • batasi ukuran dan jumlah sheet/baris jika diproses lebih lanjut.

Sanitasi Nama File dan Penyimpanan Sementara

Menyimpan file dengan nama asli dari pengguna adalah praktik yang berisiko. Nama file dapat berisi karakter tak terduga, spasi berlebihan, path separator, atau pola seperti ../../etc/passwd. Walau fungsi path tertentu dapat menormalkan nama, pendekatan terbaik tetap sederhana: jangan gunakan nama asli sebagai nama file simpanan.

Gunakan nama acak atau UUID, simpan ekstensi yang telah divalidasi, dan letakkan file di direktori khusus upload yang hak aksesnya ketat. Jika nama asli perlu disimpan untuk tujuan audit atau ditampilkan di UI, simpan sebagai metadata di database, bukan sebagai path file fisik.

Untuk direktori sementara:

  • pastikan permission tidak terlalu longgar,
  • pisahkan dari direktori yang bisa diakses publik oleh web server,
  • lakukan pembersihan berkala untuk file gagal proses atau file kadaluarsa,
  • monitor kapasitas disk dan inode.

Antivirus Hook Opsional dan Workflow Asinkron

Pada sistem yang menerima file dari pihak eksternal, antivirus scanning sering menjadi kebutuhan kepatuhan atau keamanan tambahan. Integrasi ini biasanya lebih baik diposisikan sebagai hook setelah file berhasil disimpan sementara.

func scanWithClamAV(path string) error {
    // Contoh placeholder.
    // Implementasi nyata bisa memanggil clamd melalui TCP/socket
    // atau menjalankan command scanner dalam sandbox terpisah.
    return nil
}

Jangan menjalankan antivirus berat langsung di request sinkron jika file besar dan trafik tinggi, kecuali memang volume kecil dan SLA longgar. Pola yang lebih aman adalah:

  1. request upload diterima,
  2. file disimpan ke lokasi sementara,
  3. job scanning/validasi dikirim ke antrean,
  4. status file ditandai pending, clean, atau rejected,
  5. hanya file berstatus aman yang boleh diproses lebih lanjut.

Pola asinkron ini mengurangi waktu respons API dan menghindari worker HTTP tertahan lama.

Pencegahan DoS dan Kontrol Resource

Batasi paralelisme upload

Walau setiap upload diproses sebagai stream, banyak upload besar secara bersamaan tetap bisa membebani disk throughput dan CPU parser. Karena itu, pembatasan concurrency perlu dipertimbangkan. Ini bisa dilakukan di beberapa level:

  • rate limiting per IP atau per API key,
  • semaphore internal untuk membatasi jumlah job parsing besar aktif,
  • queue background worker agar proses berat tidak langsung dieksekusi di request handler,
  • quota tenant/user untuk membatasi total volume upload.

Batasi waktu dan ukuran pemrosesan lanjutan

Setelah file diterima, bahaya belum selesai. CSV dengan jutaan baris atau file Excel dengan banyak sheet dapat menguras memori pada tahap parsing. Untuk itu:

  • gunakan parser streaming bila tersedia,
  • hindari memuat seluruh worksheet ke memori,
  • batasi jumlah baris, sheet, atau kolom yang diproses,
  • set timeout untuk job parsing,
  • tolak file terkompresi yang berpotensi menjadi zip bomb jika workflow Anda menerima arsip.

Jangan percaya header dari klien

Header seperti Content-Type dan nama file sepenuhnya dapat dimanipulasi oleh klien. Selalu lakukan validasi di server. Kesalahan ini umum terjadi pada implementasi cepat: jika front-end sudah membatasi accept=.csv,.xlsx, pengembang menganggap backend aman. Padahal kontrol di browser hanya membantu UX, bukan keamanan.

Konfigurasi Tambahan yang Relevan di Produksi

Object storage untuk file sangat besar

Jika ukuran file bisa ratusan MB atau lebih, pertimbangkan memindahkan upload langsung ke object storage seperti S3-compatible storage menggunakan pre-signed URL. Dengan pola ini, aplikasi backend tidak menjadi jalur utama transfer byte. Backend cukup menerbitkan URL upload, menerima callback atau metadata, lalu memproses file secara asinkron.

Pendekatan ini cocok saat:

  • ukuran file besar dan sering,
  • trafik upload tinggi,
  • aplikasi ingin meminimalkan beban bandwidth dan disk lokal.

Namun, trade-off-nya adalah alur menjadi lebih kompleks: validasi tertentu berpindah setelah upload selesai, dan integrasi front-end menjadi sedikit lebih rumit.

Logging dan observability

Untuk endpoint upload, log minimal yang sebaiknya ada:

  • ukuran file,
  • MIME hasil deteksi,
  • durasi upload dan durasi pemrosesan,
  • hasil validasi/scanning,
  • alasan penolakan.

Hindari mencatat path sensitif atau nama file mentah jika mengandung data pengguna. Di level metrics, pantau jumlah request ditolak karena body terlalu besar, ruang disk sementara, jumlah job pending, dan error parsing.

Kesalahan Umum dan Tips Debugging

  • 413 Request Entity Too Large: cek konsistensi limit antara Fiber, Nginx, ingress, dan load balancer. Sering kali aplikasi sudah benar, tetapi proxy lebih ketat.
  • Upload lambat timeout: cek timeout proxy dan server, tetapi jangan asal membesarkannya tanpa rate limit.
  • MIME tidak sesuai ekspektasi: beberapa file Excel atau CSV dari tool tertentu bisa terdeteksi generik. Tambahkan validasi parse aktual.
  • Disk cepat penuh: audit direktori temporer, file gagal proses, dan buffering proxy.
  • Memori melonjak saat parsing: biasanya masalah ada di tahap pasca-upload, bukan saat menyimpan file. Tinjau library parser yang dipakai.

Penutup

Menangani upload file besar di Go Fiber v3 bukan sekadar menerima multipart form lalu menyimpannya. Implementasi yang aman dan efisien membutuhkan pembatasan ukuran body, validasi berlapis, penyimpanan sementara yang terkendali, sanitasi nama file, dan kontrol resource untuk menghadapi trafik tinggi. Untuk file CSV/Excel, pendekatan terbaik adalah menerima file secara terbatas, simpan dengan nama acak, verifikasi tipe dan kemampuan parse, lalu proses secara bertahap atau asinkron.

Jika kebutuhan berkembang ke ukuran file yang lebih besar atau volume yang lebih tinggi, pertimbangkan arsitektur upload langsung ke object storage dan worker terpisah untuk scanning serta parsing. Dengan begitu, API tetap responsif, penggunaan memori lebih stabil, dan permukaan serangan dapat diperkecil.