Eksploitasi endpoint presigned URL pada backend Node.js sering kali menjadi celah utama serangan Denial of Wallet (DoW). Dalam skenario ini, pelaku mengeksploitasi ketiadaan kuota upload, longgarnya validasi ukuran file, atau lemahnya proteksi endpoint untuk membanjiri Amazon S3 dengan jutaan request PUT dan file sampah berukuran gigabyte. Dampaknya langsung terasa pada tagihan bulanan AWS melalui biaya API call (S3 PUT/LIST) dan akumulasi biaya penyimpanan (S3 Standard Storage).
Root Cause: Mengapa Presigned URL Menjadi Vektor Serangan Lonjakan Tagihan
Secara default, presigned URL memberikan izin sementara kepada klien untuk berinteraksi langsung dengan S3 tanpa mengekspos kredensial AWS. Namun, kerentanan fatal muncul jika backend mengimplementasikan pola berikut:
- TTL Terlalu Panjang: URL dibuat dengan masa berlaku berjam-jam (misalnya default SDK hingga 900–3600 detik), memberi waktu pelaku membagikan atau menggunakan ulang URL.
- Ketiadaan Validasi Ukuran (Content-Length): Backend tidak membatasi ukuran payload, sehingga klien bebas mengunggah objek berukuran puluhan gigabyte via presigned PUT URL.
- Endpoint Generator Terbuka Tanpa Rate Limit: Klien dapat meminta ratusan ribu presigned URL per menit tanpa restriksi IP atau akun.
- Orphan Objects: Klien meminta upload URL dan mengirim file ke S3, tetapi tidak pernah menyelesaikan transaksi di database aplikasi, meninggalkan file tak bertuan yang terus ditagih selamanya.
1. Enforce Constraints saat Generate Presigned URL
Saat menggunakan @aws-sdk/s3-request-presigner dengan PutObjectCommand, backend harus menandatangani header ContentType dan ContentLength ke dalam signature. Jika klien mencoba mengunggah payload dengan ukuran atau tipe yang berbeda dari yang disepakati, AWS S3 akan menolak request tersebut dengan status error 403 SignatureDoesNotMatch.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import crypto from "crypto";
const s3 = new S3Client({ region: process.env.AWS_REGION });
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB ceiling
const ALLOWED_MIME_TYPES = new Set(["image/jpeg", "image/png", "application/pdf"]);
export async function generatePresignedUpload(userId, { filename, mimeType, byteSize }) {
// 1. Boundary validation
if (!ALLOWED_MIME_TYPES.has(mimeType)) {
throw new Error("INVALID_MIME_TYPE");
}
if (byteSize <= 0 || byteSize > MAX_FILE_SIZE) {
throw new Error("FILE_SIZE_OUT_OF_BOUNDS");
}
// 2. Isolate via staging prefix and unguessable object key
const safeHash = crypto.randomBytes(16).toString("hex");
const key = `staging/${userId}/${safeHash}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: key,
ContentType: mimeType,
ContentLength: byteSize, // Signs the exact payload size into AWS SigV4
});
// 3. Ultra-short TTL: 60 detik cukup untuk memulai transfer
const url = await getSignedUrl(s3, command, { expiresIn: 60 });
return { url, key };
}Catatan: Jika klien Anda memerlukan rentang ukuran dinamis (misalnya 1 KB hingga 10 MB) tanpa mengetahui ukuran pasti di awal, gunakan
createPresignedPostdari package@aws-sdk/s3-presigned-postyang mendukung kondisi["content-length-range", min, max].
2. Terapkan Rate Limiting dan Kontrol Kuota
Membatasi ukuran file di S3 tidak menghentikan penyerang menghabiskan kuota request API dengan meminta jutaan presigned URL. Generator URL harus dilindungi oleh rate limiter di layer HTTP backend sebelum memanggil AWS SDK.
import express from "express";
import rateLimit from "express-rate-limit";
const app = express();
app.use(express.json());
// Batasi 10 permintaan upload per 15 menit per user/IP
const uploadRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
message: { error: "TOO_MANY_UPLOAD_REQUESTS" },
});
app.post("/api/uploads/presign", uploadRateLimiter, async (req, res) => {
try {
const { filename, mimeType, byteSize } = req.body;
const result = await generatePresignedUpload(req.user.id, { filename, mimeType, byteSize });
res.json(result);
} catch (err) {
res.status(400).json({ error: err.message });
}
});3. Otomasi Pembersihan Objek Sampah via S3 Lifecycle Rules
File yang diunggah ke S3 tetapi gagal diverifikasi oleh server akan menjadi objek yatim (orphan) yang menguras biaya penyimpanan tanpa henti. Solusi paling efisien dan hemat komputasi adalah mendelegasikan pembersihan ini ke S3 Lifecycle Engine.
Simpan konfigurasi berikut ke file lifecycle.json dan terapkan ke bucket target:
{
"Rules": [
{
"ID": "AbortIncompleteMultipartUploads",
"Status": "Enabled",
"Filter": {},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 1
}
},
{
"ID": "PurgeUnverifiedStagingFiles",
"Status": "Enabled",
"Filter": {
"Prefix": "staging/"
},
"Expiration": {
"Days": 1
}
}
]
}Terapkan rule menggunakan AWS CLI:
aws s3api put-bucket-lifecycle-configuration \
--bucket NAMA_BUCKET_ANDA \
--lifecycle-configuration file://lifecycle.jsonMekanisme Staging: Upload file awal selalu masuk ke direktori staging/. Ketika user menekan tombol "Simpan" di aplikasi, backend memverifikasi file tersebut dan memindahkannya (CopyObject + DeleteObject) ke prefix permanen (misalnya production/). File yang tidak pernah diverifikasi akan otomatis dihapus oleh AWS dalam 24 jam tanpa biaya komputasi tambahan.
Checklist Audit Konfigurasi S3 dan IAM
Gunakan checklist berikut untuk memverifikasi bucket S3 Anda telah terkunci dari potensi eksploitasi biaya:
- S3 Block Public Access: Pastikan opsi Block all public access aktif di level bucket dan account. Akses hanya boleh melalui presigned URL atau CloudFront OAC (Origin Access Control).
- Prinsip Least Privilege IAM: Kredensial yang digunakan oleh Node.js tidak boleh memiliki izin
s3:*. Batasi IAM Policy hanya padas3:PutObjectdans3:GetObjectuntuk prefix yang relevan:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::NAMA_BUCKET_ANDA/*"
}
]
}- AWS Budgets & CloudWatch Alarms: Buat Cost Anomaly Detection dan pasang threshold budget bulanan. Siapkan notifikasi SNS/email saat realisasi tagihan mencapai 80% dari batas estimasi.
- CORS Configuration Ketat: Izinkan metode
PUThanya dari domain origin aplikasi Anda untuk mencegah eksekusi upload presigned URL langsung dari situs pihak ketiga.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!