API security itu bukan "satu fitur" — lebih mirip layered defense. Setiap lapisan menangani ancaman berbeda.
CORS (Cross-Origin Resource Sharing)
Browser otomatis memblokir request dari domain A ke domain B — kecuali server B explicit bilang "OK". Ini yang disebut CORS.
Tanpa CORS properly set, API kamu tidak bisa dipanggil dari frontend di domain berbeda.
// Express.js — setup CORS simple
import cors from "cors";
app.use(cors({
origin: ["https://app.kamu.com", "https://admin.kamu.com"],
credentials: true, // allow cookies
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}));
⚠️ Jangan pakai Access-Control-Allow-Origin: * dengan credentials. Itu berarti siapapun bisa pakai cookie user dari request mereka — data breach siap terjadi.
Preflight request: browser otomatis kirim OPTIONS dulu untuk request "non-simple" (PUT, DELETE, custom headers). Server harus respond dengan Access-Control-Allow-Methods, Access-Control-Allow-Headers.
CSRF (Cross-Site Request Forgery) — Masih Relevan?
CSRF: attacker menjebak user yang sudah login untuk memicu aksi di situs target. Contoh: user login di bank, attacker kirim email dengan image yang src-nya https://bank.com/transfer?to=attacker&amount=1000 — browser ikut kirim cookie, bank kira legit.
CSRF hanya relevan kalau autentikasi pakai cookie. Kalau pakai Bearer token di header (JWT), CSRF otomatis mitigated — JavaScript attacker tidak bisa baca atau kirim token dari origin lain.
3 lapis perlindungan kalau pakai cookie:
-
SameSite cookie (paling simple, paling efektif):
res.cookie("session", token, { httpOnly: true, secure: true, sameSite: "lax", // "strict" untuk max security, "lax" untuk tetap berfungsi dengan cross-site navigation }); -
CSRF token (double submit cookie atau synchronizer pattern):
- Server kirim token random di cookie + form
- Setiap POST/PUT/DELETE cek cocok
- Laravel, Django, Rails otomatis handle ini
-
Origin/Referer validation — cek
OriginatauRefererheader cocok domain kamu:if (req.method !== "GET" && req.headers.origin !== "https://app.kamu.com") { return res.status(403).send("Forbidden"); }
Input Sanitization
Jangan pernah percaya input dari client. Semua yang masuk harus di-validasi + di-sanitize.
SQL Injection — parameterized queries wajib:
// ❌ BAHAYA: string interpolation
db.query(`SELECT * FROM users WHERE email = "${req.body.email}"`);
// attacker kirim: [email protected]"; DROP TABLE users; --
// ✅ Parameterized
db.query("SELECT * FROM users WHERE email = ?", [req.body.email]);
// driver escape nilai secara aman
XSS — sanitize sebelum output:
import DOMPurify from "isomorphic-dompurify";
// User posting comment dengan HTML
const sanitizedContent = DOMPurify.sanitize(req.body.content, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
ALLOWED_ATTR: ["href"],
});
Command Injection — avoid shell execution dengan user input:
import { execFile } from "child_process";
// ❌ shell=true dengan user input
exec(`convert ${req.body.filename} output.png`);
// ✅ execFile dengan array args (no shell parsing)
execFile("convert", [req.body.filename, "output.png"]);
Path Traversal — validate path:
import path from "path";
const filename = path.basename(req.body.filename); // buang ../
const safePath = path.join("/safe/dir", filename);
if (!safePath.startsWith("/safe/dir")) return res.status(400).end();
Rate Limiting per Endpoint Sensitivity
Login dan signup lebih sensitif daripada list produk. Pasang rate limit yang berbeda:
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 5, // 5 attempt per IP
message: "Terlalu banyak percobaan login, coba lagi 15 menit lagi",
});
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 menit
max: 60,
});
app.post("/auth/login", loginLimiter, handleLogin);
app.use("/api", apiLimiter);
Kombinasi per-IP + per-email penting untuk login — attacker bisa pakai 1000 IP, tapi target 1 email saja. Limit "5 attempt per email per 15 min" mencegah credential stuffing.
TLS / HTTPS — Basics
Pakai HTTPS untuk semua API production. Tanpa TLS, cookie/token bisa di-sniff di WiFi publik.
- HSTS header — paksa browser selalu HTTPS:
res.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - Certificate pinning untuk mobile app — mobile app embed cert hash, reject kalau server cert berbeda (mencegah MITM dengan cert palsu)
Security Headers Wajib
app.use((req, res, next) => {
res.set({
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY", // anti-clickjacking
"Content-Security-Policy": "default-src 'self'",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=(), camera=()",
});
next();
});
// Atau pakai Helmet.js — semua header sekaligus
import helmet from "helmet";
app.use(helmet());
Checklist Production
- HTTPS + HSTS enforced
- CORS dengan specific origin, bukan
* - Cookie dengan
httpOnly + Secure + SameSite - CSRF protection (SameSite + token kalau pakai cookie auth)
- Semua query DB pakai parameterized
- Semua user input di-sanitize sebelum output
- Rate limit ketat di auth endpoints
- Security headers lewat Helmet
- Secrets di env vars, bukan hardcoded
- Dependency audit rutin (
npm audit, Snyk, Dependabot) - Security log monitored (failed login spike, unusual patterns)