Pada API produksi, endpoint upload adalah salah satu area yang paling sering disalahgunakan. Masalah umumnya bukan hanya file terlalu besar, tetapi juga MIME spoofing, nama file berbahaya, path traversal, validasi field yang longgar, dan penyimpanan file langsung ke lokasi publik tanpa kontrol.
Di Go Fiber, hardening endpoint upload perlu dilakukan berlapis: batasi ukuran request, validasi field input di server, periksa tipe file berdasarkan isi file, gunakan nama file acak, simpan file di lokasi non-public, tambahkan rate limit, dan catat aktivitas penting ke audit log. Artikel ini fokus pada implementasi praktis yang bisa langsung dipakai.
Ancaman umum pada endpoint upload
Sebelum menulis handler, pastikan model ancaman Anda jelas. Ini beberapa risiko yang paling sering muncul:
- File berbahaya: penyerang mengunggah skrip, executable, atau dokumen berisi payload berbahaya.
- MIME spoofing: header
Content-Typeatau ekstensi file dipalsukan agar terlihat aman. - Ukuran file berlebih: request besar menghabiskan memori, disk, bandwidth, atau worker.
- Path traversal: nama file seperti
../../etc/passwddipakai untuk menimpa file lain. - Nama file tidak aman: karakter aneh, spasi, unicode ambigu, atau ekstensi ganda seperti
invoice.pdf.exe. - Validasi field lemah: metadata seperti
user_id,document_type, atau deskripsi tidak divalidasi secara ketat. - Abuse endpoint: upload berulang untuk memenuhi storage atau membebani CPU.
Prinsip dasarnya: jangan percaya input dari klien, termasuk nama file, ekstensi, MIME, ukuran yang diklaim, dan field form lain.
Arsitektur alur upload yang aman
Alur upload yang lebih aman biasanya seperti ini:
- Klien mengirim multipart request ke endpoint upload.
- Middleware membatasi ukuran request dan laju request per IP atau per token.
- Handler memvalidasi field non-file terlebih dahulu.
- Handler mengambil metadata file dari multipart form.
- Server membuka stream file dan membaca sebagian byte awal untuk mendeteksi tipe file sebenarnya.
- Server memeriksa ukuran file terhadap batas yang diizinkan.
- Server membuat nama file acak, bukan memakai nama asli dari klien.
- Server menyimpan file ke direktori non-public atau object storage private.
- Server mencatat audit log: siapa yang upload, kapan, ukuran, tipe hasil deteksi, hasil validasi, dan alasan penolakan bila ada.
- Bila perlu, file diproses lanjutan secara asynchronous, misalnya scan antivirus atau ekstraksi metadata.
Jika file harus dapat diakses publik, jangan expose direktori upload mentah. Lebih aman sajikan file melalui endpoint terkontrol atau signed URL dari storage private.
Prinsip validasi input dan upload file aman di Go Fiber
1. Batasi ukuran request sejak awal
Pembatasan ukuran harus dilakukan sedini mungkin agar server tidak memproses request besar yang jelas tidak valid. Selain itu, tetapkan juga batas per file di level handler, karena ukuran total request dan ukuran satu file adalah dua hal yang berbeda.
Contoh kebijakan yang umum:
- Maksimum request multipart: misalnya 10 MB.
- Maksimum file per upload: misalnya 5 MB.
- Maksimum jumlah file: misalnya 1 atau 3 file per request.
Jangan hanya bergantung pada validasi di frontend. Batas final harus diputuskan server.
2. Gunakan allowlist, bukan blocklist
Untuk tipe file, lebih aman mendefinisikan daftar yang diizinkan, misalnya hanya JPEG, PNG, dan PDF. Blocklist biasanya mudah dilewati karena format file dan variasi payload sangat banyak.
Validasi jangan hanya berdasarkan ekstensi. File .jpg bisa saja sebenarnya bukan JPEG. Header Content-Type dari klien juga tidak cukup andal karena bisa dipalsukan.
3. Verifikasi MIME dari isi file
Cara yang lebih aman adalah membaca beberapa byte awal file dan mendeteksi tipenya dari konten. Ini tidak sempurna untuk semua format, tetapi jauh lebih baik daripada hanya melihat nama file atau header dari klien.
Setelah tipe file lolos, Anda tetap bisa menyimpan ekstensi berdasarkan hasil deteksi server, bukan berdasarkan nama file asli.
4. Jangan gunakan nama file dari klien sebagai nama simpan
Nama file asli boleh dicatat sebagai metadata untuk kebutuhan audit atau tampilan, tetapi jangan dipakai sebagai path penyimpanan. Gunakan identifier acak, misalnya UUID atau token acak kriptografis, lalu tambahkan ekstensi yang ditentukan server.
Ini mencegah:
- tabrakan nama file,
- path traversal,
- karakter berbahaya pada nama file,
- ekstensi ganda yang menyesatkan.
5. Simpan file di lokasi non-public
Menyimpan file langsung di bawah folder yang diserve web server mempermudah akses tidak terkontrol. Simpan file di direktori private atau object storage private, lalu akses file melalui layer aplikasi yang memeriksa otorisasi.
Jika file harus diunduh, gunakan endpoint download yang melakukan:
- autentikasi,
- otorisasi,
- pengaturan header yang aman,
- opsional signed URL dengan masa berlaku pendek.
6. Validasi field form secara ketat
Sering kali upload file disertai field seperti document_type, owner_id, atau notes. Semua field ini harus divalidasi di server:
- field wajib tidak boleh kosong,
- nilai enum harus berasal dari daftar yang diizinkan,
- panjang string dibatasi,
- ID harus diverifikasi terhadap user/session saat ini, bukan dipercaya dari klien mentah.
Jangan biarkan field pendukung menjadi jalan masuk abuse logika bisnis.
7. Tambahkan rate limit dasar
Upload adalah operasi mahal dibanding request JSON biasa. Rate limiting dasar per IP, per user, atau per API key membantu menahan abuse. Ini bukan pengganti autentikasi atau WAF, tetapi lapisan penting untuk menurunkan risiko flood sederhana.
8. Catat audit log
Untuk keperluan debugging, forensik, dan kepatuhan, catat minimal:
- user ID atau client identity,
- IP atau sumber request,
- nama file asli,
- ukuran file,
- MIME hasil deteksi,
- hasil akhir: diterima atau ditolak,
- alasan penolakan.
Hindari menyimpan data sensitif berlebihan di log. Audit log harus cukup untuk investigasi tanpa menciptakan kebocoran baru.
Contoh implementasi handler upload aman di Go Fiber
Berikut contoh struktur handler yang rapi dan cukup realistis. Fokus utamanya adalah validasi server-side, deteksi MIME dari isi file, nama file acak, dan penyimpanan private.
package main
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
type UploadForm struct {
DocumentType string
Notes string
}
var allowedMIMEs = map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"application/pdf": ".pdf",
}
const (
maxFileSize = 5 * 1024 * 1024
uploadDir = "./private_uploads"
)
func main() {
app := fiber.New(fiber.Config{
BodyLimit: 10 * 1024 * 1024,
})
app.Post("/upload", uploadHandler)
app.Listen(":3000")
}
func uploadHandler(c *fiber.Ctx) error {
form := UploadForm{
DocumentType: c.FormValue("document_type"),
Notes: c.FormValue("notes"),
}
if err := validateUploadForm(form); err != nil {
auditLog(c, "upload_rejected", map[string]any{
"reason": err.Error(),
})
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
fh, err := c.FormFile("file")
if err != nil {
auditLog(c, "upload_rejected", map[string]any{
"reason": "file field is required",
})
return fiber.NewError(fiber.StatusBadRequest, "file field is required")
}
if fh.Size <= 0 || fh.Size > maxFileSize {
auditLog(c, "upload_rejected", map[string]any{
"reason": "invalid file size",
"file_size": fh.Size,
})
return fiber.NewError(fiber.StatusBadRequest, "file size exceeds limit")
}
src, err := fh.Open()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to open uploaded file")
}
defer src.Close()
detectedMIME, err := detectMIME(src)
if err != nil {
auditLog(c, "upload_rejected", map[string]any{
"reason": "failed to detect mime",
})
return fiber.NewError(fiber.StatusBadRequest, "unable to validate file type")
}
ext, ok := allowedMIMEs[detectedMIME]
if !ok {
auditLog(c, "upload_rejected", map[string]any{
"reason": "mime not allowed",
"detected_mime": detectedMIME,
})
return fiber.NewError(fiber.StatusBadRequest, "file type is not allowed")
}
if err := os.MkdirAll(uploadDir, 0o750); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare storage")
}
randomName, err := randomHex(16)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate file name")
}
storedName := randomName + ext
dstPath := filepath.Join(uploadDir, storedName)
if err := saveUploadedFile(fh, dstPath); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to save file")
}
auditLog(c, "upload_accepted", map[string]any{
"stored_name": storedName,
"original_name": fh.Filename,
"file_size": fh.Size,
"detected_mime": detectedMIME,
"document_type": form.DocumentType,
})
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"message": "upload successful",
"file": fiber.Map{
"name": storedName,
"type": detectedMIME,
"size": fh.Size,
},
})
}
func validateUploadForm(f UploadForm) error {
allowedDocumentTypes := map[string]bool{
"avatar": true,
"identity": true,
"report": true,
}
if !allowedDocumentTypes[f.DocumentType] {
return errors.New("invalid document_type")
}
if len(f.Notes) > 500 {
return errors.New("notes is too long")
}
return nil
}
func detectMIME(file multipart.File) (string, error) {
header := make([]byte, 512)
n, err := file.Read(header)
if err != nil && err != io.EOF {
return "", err
}
if seeker, ok := file.(io.Seeker); ok {
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
return "", err
}
} else {
return "", errors.New("file is not seekable")
}
mimeType := http.DetectContentType(header[:n])
return mimeType, nil
}
func saveUploadedFile(fh *multipart.FileHeader, dstPath string) error {
src, err := fh.Open()
if err != nil {
return err
}
defer src.Close()
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, src)
return err
}
func randomHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func auditLog(c *fiber.Ctx, event string, fields map[string]any) {
var b strings.Builder
b.WriteString("event=")
b.WriteString(event)
b.WriteString(" ip=")
b.WriteString(c.IP())
b.WriteString(" path=")
b.WriteString(c.Path())
b.WriteString(" at=")
b.WriteString(time.Now().UTC().Format(time.RFC3339))
for k, v := range fields {
b.WriteString(" ")
b.WriteString(k)
b.WriteString("=")
b.WriteString(fmt.Sprint(v))
}
fmt.Println(b.String())
}Kenapa pola di atas lebih aman?
- BodyLimit membatasi ukuran request total sebelum pemrosesan lanjut.
- Validasi field memeriksa metadata bisnis, bukan hanya file.
- fh.Size dipakai untuk batas file individual.
- http.DetectContentType memeriksa konten file, bukan sekadar ekstensi atau header dari klien.
- Nama acak menghilangkan risiko path traversal dan tabrakan nama.
- Direktori private mencegah file langsung bisa diakses publik.
- Audit log membantu investigasi penolakan dan penyalahgunaan.
Menambahkan rate limit dasar untuk endpoint upload
Upload perlu dibatasi secara terpisah dari endpoint biasa. Anda bisa menerapkan middleware rate limit khusus di route upload. Kebijakannya bergantung pada pola traffic, tetapi pendekatan dasarnya adalah:
- batasi request per IP untuk endpoint anonim,
- batasi per user atau API key untuk endpoint terautentikasi,
- gunakan jendela waktu pendek untuk mencegah burst abuse.
Contoh integrasi route khusus:
// Pseudocode struktur route
// upload := app.Group("/upload", someRateLimitMiddleware)
// upload.Post("/", uploadHandler)Rate limit bukan solusi tunggal. Penyerang di belakang banyak IP masih bisa lolos. Namun, untuk abuse dasar dan kesalahan penggunaan dari klien, lapisan ini sangat membantu.
Penyimpanan file: lokal private vs object storage private
Direktori lokal private
Cocok untuk sistem sederhana atau satu instance server. Keuntungannya implementasi cepat dan mudah didebug. Kekurangannya:
- lebih sulit diskalakan ke banyak instance,
- perlu backup dan rotasi storage yang jelas,
- risiko kehilangan file saat instance diganti jika storage tidak persisten.
Object storage private
Lebih cocok untuk produksi yang butuh skalabilitas dan durabilitas lebih baik. Polanya tetap sama:
- validasi file tetap dilakukan di API server,
- nama object tetap acak,
- bucket tetap private,
- akses file diberikan via signed URL atau endpoint proxy yang terotorisasi.
Jangan memindahkan validasi ke sisi klien hanya karena file akhirnya disimpan di object storage.
Kesalahan umum yang perlu dihindari
1. Hanya mengecek ekstensi file
Ekstensi mudah dipalsukan. File malware.exe bisa diubah menjadi photo.jpg. Selalu cek isi file.
2. Menyimpan file dengan nama asli dari user
Ini membuka peluang karakter berbahaya, nama ambigu, tabrakan, dan path traversal jika path dibangun secara tidak hati-hati.
3. Menyimpan upload di folder publik
Jika web server langsung menyajikan hasil upload, file yang seharusnya privat bisa diakses tanpa kontrol. Ini juga memperbesar dampak bila file berbahaya lolos.
4. Tidak membatasi ukuran request
Validasi ukuran setelah file telanjur diproses terlalu terlambat. Batasi dari middleware atau konfigurasi server sedini mungkin.
5. Terlalu percaya pada metadata multipart
Content-Type, filename, dan field lain dari klien harus dianggap tidak tepercaya sampai diverifikasi.
6. Tidak mencatat alasan penolakan
Tanpa audit log, Anda sulit membedakan bug klien, salah konfigurasi, dan serangan nyata. Logging yang konsisten mempercepat debugging.
7. Tidak memikirkan proses pasca-upload
Untuk kasus sensitif, validasi awal saja mungkin belum cukup. Anda mungkin perlu scan antivirus, verifikasi struktur file yang lebih ketat, atau transcode gambar sebelum file digunakan lebih lanjut.
Checklist implementasi upload aman
- Batasi ukuran request di level Fiber.
- Batasi ukuran file di handler.
- Batasi jumlah file per request bila mendukung multi-upload.
- Validasi field non-file dengan aturan ketat.
- Gunakan allowlist MIME berdasarkan isi file.
- Jangan percaya ekstensi dan Content-Type dari klien.
- Gunakan nama file acak, bukan nama asli.
- Simpan di lokasi non-public atau storage private.
- Tambahkan rate limit untuk endpoint upload.
- Catat audit log untuk accepted dan rejected upload.
- Atur permission file dan direktori secara konservatif.
- Pertimbangkan scanning tambahan untuk use case sensitif.
Debugging dan verifikasi di lingkungan pengembangan
Beberapa pengujian manual yang sebaiknya dilakukan sebelum deploy:
- unggah file valid yang memang diizinkan,
- unggah file dengan ekstensi aman tetapi isi tidak sesuai,
- unggah file melebihi batas ukuran,
- kirim request tanpa field wajib,
- coba nama file aneh dan karakter khusus,
- uji rate limit dengan request berulang.
Contoh pengujian dengan curl:
curl -X POST http://localhost:3000/upload \
-F "document_type=avatar" \
-F "notes=Foto profil" \
-F "file=@./sample.jpg"Saat debugging, periksa tiga hal utama:
- apakah file ditolak karena ukuran, field, atau MIME,
- apakah pointer file di-reset setelah deteksi MIME,
- apakah path penyimpanan benar-benar mengarah ke lokasi private.
Penutup
Mengamankan upload file di Go Fiber bukan soal satu validasi tunggal, tetapi gabungan beberapa kontrol yang saling melengkapi. Untuk API produksi, baseline yang layak adalah: batas ukuran request dan file, validasi field server-side, allowlist tipe file berdasarkan isi file, nama simpan acak, penyimpanan non-public, rate limit dasar, dan audit logging.
Jika Anda menerapkan pola ini secara konsisten, sebagian besar celah umum pada endpoint upload bisa ditekan tanpa membuat handler menjadi terlalu rumit. Mulailah dari alur yang sederhana tetapi ketat, lalu tambahkan scanning atau workflow asynchronous bila kebutuhan keamanan aplikasi Anda lebih tinggi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!