Tampilan
Kenapa penting
Sampai bab sebelumnya kamu sudah punya autentikasi JWT dan otorisasi per role serta per pemilik data. Tetapi API yang "sudah pakai login" masih bisa bocor lewat banyak pintu lain: field yang tidak seharusnya bisa diubah, konfigurasi CORS yang terlalu longgar, endpoint lama yang lupa dimatikan, secret yang ikut ter-commit, atau library dengan celah yang sudah diumumkan. Di tempat kerja, daftar OWASP API Security Top 10 sering dipakai sebagai bahasa bersama saat review keamanan dan pentest. Bab ini memetakan daftar itu ke praktik konkret di Spring Boot sehingga kamu bisa memeriksa API-mu sendiri sebelum orang lain menemukannya.
Konsep inti
OWASP API Security Top 10 (2023) dalam versi Spring
| Kode | Risiko | Wujud umum di Spring | Pertahanan utama |
|---|---|---|---|
| API1 | Broken Object Level Authorization | findById(id) tanpa cek pemilik | Syarat pemilik di query, test akses silang (bab Otorisasi) |
| API2 | Broken Authentication | JWT tidak divalidasi iss/exp, login tanpa batas | Resource server bawaan, rate limit login, MFA di IdP |
| API3 | Broken Object Property Level Authorization | Entity dipakai sebagai request/response | DTO record terpisah untuk input dan output |
| API4 | Unrestricted Resource Consumption | size paginasi tanpa batas, upload tanpa limit | Batas halaman, ukuran request, timeout, rate limiting |
| API5 | Broken Function Level Authorization | Endpoint admin hanya "disembunyikan" dari UI | hasRole di URL/method, anyRequest().authenticated() |
| API6 | Unrestricted Access to Sensitive Business Flows | Bot memborong stok promo atau voucher | Limit per akun/perangkat, deteksi otomasi, antrean |
| API7 | Server Side Request Forgery | Fitur webhook memanggil URL apa pun dari user | Allowlist host/skema, tolak IP internal |
| API8 | Security Misconfiguration | Actuator terbuka, stack trace di respons, CORS * | Konfigurasi minimal, security header, review per environment |
| API9 | Improper Inventory Management | /v1 lama masih hidup, Swagger UI terbuka di produksi | Daftar endpoint & versi, matikan yang usang |
| API10 | Unsafe Consumption of APIs | Respons API pihak ketiga dipercaya mentah-mentah | Validasi respons, timeout, TLS, batasi redirect |
API1 dan API5 sudah dibahas di bab Otorisasi. Bagian berikut fokus pada yang paling sering muncul saat kamu menulis kode sehari-hari.
DTO untuk mencegah mass assignment dan kebocoran data (API3)
Entity JPA berisi semua kolom: passwordHash, role, balance, deletedAt. Bila entity dipakai langsung sebagai @RequestBody, client bisa mengisi field yang seharusnya hanya diubah sistem. Bila dipakai sebagai respons, field internal ikut terkirim.
java
// Potongan — input dan output dipisah dari entity
public record UpdateProfileRequest(
@NotBlank @Size(max = 100) String name,
@Pattern(regexp = "^\\+?[0-9]{8,15}$") String phone) {}
public record ProfileResponse(String id, String name, String phone) {
static ProfileResponse from(UserAccount user) {
return new ProfileResponse(user.getId(), user.getName(), user.getPhone());
}
}
@PutMapping("/api/users/me")
ProfileResponse update(@AuthenticationPrincipal Jwt jwt, @Valid @RequestBody UpdateProfileRequest request) {
return profiles.update(jwt.getSubject(), request); // service hanya menyalin name & phone
}Karena UpdateProfileRequest tidak punya field role, JSON berisi "role":"ADMIN" tidak punya tempat untuk mendarat. Tergantung konfigurasi Jackson, field tak dikenal itu diabaikan atau ditolak — keduanya aman, karena role tidak berubah.
CORS yang benar
CORS (Cross-Origin Resource Sharing) adalah mekanisme browser: ia menentukan apakah JavaScript dari origin lain boleh membaca respons API-mu. CORS bukan autentikasi — curl dan Postman tidak peduli CORS. Di Spring Security, aktifkan lewat http.cors(...) dan sediakan CorsConfigurationSource:
java
// Potongan
@Bean
CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://shop.example.com", "https://admin.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setMaxAge(Duration.ofHours(1));
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}Daftar origin sebaiknya dibaca dari konfigurasi per environment (localhost:5173 hanya di profil dev). Hindari wildcard, apalagi dikombinasikan dengan setAllowCredentials(true).
CSRF: cookie sesi vs bearer token
CSRF memanfaatkan fakta bahwa browser otomatis melampirkan cookie ke request menuju domain tersebut, bahkan bila request itu dipicu dari situs jahat. Keputusannya:
| Cara autentikasi | CSRF |
|---|---|
Cookie sesi (JSESSIONID) atau cookie berisi token | Aktif (default Spring Security) — client kirim token CSRF |
Header Authorization: Bearer ... saja, stateless | Boleh dimatikan |
java
// Potongan — API stateless berbasis bearer token
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));Mematikan CSRF hanya aman bila tidak ada autentikasi berbasis cookie sama sekali. Bila tim frontend memindahkan token ke cookie "agar aman dari XSS", CSRF wajib dinyalakan kembali.
Rate limiting dan batas sumber daya (API4)
Rate limiting membatasi berapa banyak request yang boleh dikirim satu client dalam rentang waktu, dan menjawab 429 Too Many Requests bila terlampaui. Algoritma yang paling umum adalah token bucket: setiap client punya ember berkapasitas tertentu yang terisi ulang dengan kecepatan tetap. Logikanya dalam Java murni:
java
import java.util.HashMap;
import java.util.Map;
public class TokenBucketDemo {
// Satu bucket per client: kapasitas = burst maksimum, refill = token per detik.
static final class TokenBucket {
private final int capacity;
private final double refillPerSecond;
private double tokens;
private long lastRefillMillis;
TokenBucket(int capacity, double refillPerSecond, long nowMillis) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = capacity;
this.lastRefillMillis = nowMillis;
}
boolean tryConsume(long nowMillis) {
double elapsedSeconds = (nowMillis - lastRefillMillis) / 1000.0;
tokens = Math.min(capacity, tokens + elapsedSeconds * refillPerSecond);
lastRefillMillis = nowMillis;
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
}
public static void main(String[] args) {
Map<String, TokenBucket> buckets = new HashMap<>();
long now = 0; // jam simulasi agar output pasti
for (int i = 1; i <= 5; i++) {
var bucket = buckets.computeIfAbsent("api-key-A", k -> new TokenBucket(3, 1.0, 0));
System.out.println("t=0s request " + i + " -> " + (bucket.tryConsume(now) ? 200 : 429));
}
now = 2_000; // 2 detik kemudian: 2 token terisi kembali
var bucket = buckets.get("api-key-A");
for (int i = 6; i <= 8; i++) {
System.out.println("t=2s request " + i + " -> " + (bucket.tryConsume(now) ? 200 : 429));
}
}
}text
t=0s request 1 -> 200
t=0s request 2 -> 200
t=0s request 3 -> 200
t=0s request 4 -> 429
t=0s request 5 -> 429
t=2s request 6 -> 200
t=2s request 7 -> 200
t=2s request 8 -> 429Contoh ini hanya untuk satu instance dan tidak thread-safe. Di produksi dengan banyak instance, rate limiting biasanya dipasang di API gateway atau memakai penyimpanan bersama seperti Redis — topik itu muncul lagi di level Microservices. Lengkapi dengan batas lain: ukuran halaman maksimum, ukuran body/upload, dan timeout ke layanan luar.
Security header dan konfigurasi minimal (API8)
Spring Security sudah mengirim header aman secara default: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Cache-Control: no-cache, no-store, max-age=0, must-revalidate, dan Strict-Transport-Security (hanya lewat HTTPS). Jangan matikan header ini tanpa alasan. Untuk API murni JSON kamu bisa menambah Content Security Policy yang ketat:
java
// Potongan
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'none'; frame-ancestors 'none'")));Konfigurasi lain yang sering terlupa:
yaml
# application-prod.yml — potongan
management:
endpoints:
web:
exposure:
include: health,info # jangan expose env, heapdump, configprops ke publik
server:
error:
include-stacktrace: never
include-message: neverSecret management
Password database, client secret OAuth2, dan API key pembayaran tidak boleh ada di repository, termasuk di application.yml. Tulis placeholder dan isi dari environment saat runtime:
yaml
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
payment:
api-key: ${PAYMENT_API_KEY}Di server, nilai itu datang dari environment variable, Kubernetes Secret, atau secret manager (misalnya Vault). Spring Boot juga bisa membaca secret yang di-mount sebagai file lewat spring.config.import: configtree:/run/secrets/. Bila secret terlanjur ter-commit, rotasi secret-nya — menghapus commit tidak cukup karena riwayat Git sudah tersebar.
Dependency scanning
Sebagian besar kode di aplikasimu berasal dari library. Celah seperti Log4Shell menunjukkan bahwa dependency usang bisa sama berbahayanya dengan bug sendiri. Praktik minimal:
bash
# Lihat dependency transitif yang sebenarnya ikut terbawa
./mvnw dependency:tree
# Pindai CVE yang diketahui dengan OWASP Dependency-Check (butuh NVD API key agar cepat)
./mvnw org.owasp:dependency-check-maven:checkJalankan pemindaian di CI dan aktifkan pembaruan otomatis (misalnya Dependabot atau Renovate). Mengikuti versi patch Spring Boot terbaru adalah cara termurah untuk ikut mendapat perbaikan keamanan semua library yang dikelolanya.
Logging keamanan tanpa data sensitif
Log adalah alat deteksi: login gagal beruntun, 403 yang melonjak, atau akses ke ID yang bukan milik user adalah tanda serangan. Catat siapa (user ID/subject), dari mana (IP), apa (endpoint, ID resource), dan hasilnya. Jangan pernah mencatat password, header Authorization, token, nomor kartu, atau data pribadi lengkap. Waspadai juga log injection: input user yang berisi karakter baris baru bisa memalsukan baris log, jadi pakai placeholder {} dan logging terstruktur alih-alih menggabungkan string mentah.
Contoh di dunia backend
Konfigurasi keamanan API toko online yang dipakai frontend SPA dengan bearer token dari Keycloak:
java
// Potongan — Spring Boot 4.1 / Spring Security 7
@Configuration
@EnableMethodSecurity
class ApiSecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // aman: tidak ada cookie autentikasi
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(Customizer.withDefaults())
.headers(h -> h.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'none'")))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
@RestController
@RequestMapping("/api/products")
class ProductController {
private static final int MAX_PAGE_SIZE = 100;
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
private final ProductService products;
ProductController(ProductService products) {
this.products = products;
}
@GetMapping
Page<ProductResponse> list(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
int safeSize = Math.clamp(size, 1, MAX_PAGE_SIZE); // API4: batasi ukuran halaman
return products.list(PageRequest.of(Math.max(page, 0), safeSize));
}
@PutMapping("/{sku}/price")
@PreAuthorize("hasRole('ADMIN')")
ProductResponse changePrice(@PathVariable String sku,
@Valid @RequestBody ChangePriceRequest request, // API3: DTO, bukan entity
@AuthenticationPrincipal Jwt jwt) {
log.info("Harga diubah sku={} oleh={}", sku, jwt.getSubject()); // jejak audit tanpa token
return products.changePrice(sku, request.newPrice());
}
}
record ChangePriceRequest(@NotNull @Positive BigDecimal newPrice) {}Pertanyaan interview
1. Apa beda CORS dan CSRF?
CORS adalah aturan browser tentang origin mana yang boleh membaca respons API dari JavaScript; server mengumumkannya lewat header Access-Control-Allow-*. CSRF adalah serangan yang memanfaatkan browser mengirim cookie otomatis untuk melakukan aksi atas nama korban. CORS yang ketat tidak otomatis mencegah CSRF, karena beberapa request (misalnya form POST) tetap terkirim walaupun responsnya tidak bisa dibaca.
2. Kenapa entity JPA sebaiknya tidak dipakai langsung sebagai request dan response?
Sebagai request, semua field bisa diisi client sehingga membuka mass assignment (mengubah role, balance). Sebagai response, field internal seperti passwordHash ikut terkirim dan relasi lazy bisa memicu query tambahan atau error serialisasi. DTO record memberi kontrak API yang eksplisit dan terpisah dari skema database.
3. Secret database ter-commit ke repository publik. Apa langkahmu?
Pertama, rotasi secret-nya segera (ganti password, cabut key lama) karena secret harus dianggap sudah bocor. Lalu pindahkan konfigurasi ke environment variable atau secret manager, bersihkan riwayat bila perlu, periksa log akses untuk penyalahgunaan, dan pasang pemindai secret di CI agar tidak terulang.
Jebakan umum
- Menganggap CORS sebagai pengaman API. CORS hanya berlaku di browser; penyerang dengan
curltidak terhalang. - Mematikan CSRF lalu memakai cookie untuk token. Kombinasi ini membuka CSRF kembali.
- Entity sebagai
@RequestBody. Membuka mass assignment; selalu pakai DTO berisi field yang boleh diubah. - Expose semua endpoint Actuator.
envdanheapdumpbisa membocorkan secret. - Secret di
application.ymlyang ter-commit, atau mencetak konfigurasi lengkap ke log saat startup. - Mencatat header
Authorizationatau body request mentah untuk debugging — lalu lupa dimatikan di produksi.
Ringkasan
- OWASP API Security Top 10 (2023) adalah daftar periksa: BOLA, autentikasi, properti objek, konsumsi sumber daya, fungsi, alur bisnis, SSRF, misconfiguration, inventaris, dan konsumsi API pihak ketiga.
- DTO
recordterpisah untuk request dan response mencegah mass assignment dan kebocoran field internal. - CORS mengatur pembacaan respons di browser — daftarkan origin eksplisit; CSRF wajib aktif bila autentikasi memakai cookie, boleh dimatikan untuk API stateless berbasis bearer token.
- Batasi sumber daya: ukuran halaman, ukuran request, timeout, dan rate limiting (429).
- Pertahankan security header default, persempit Actuator, simpan secret di luar repository, dan pindai dependency.
- Log keamanan mencatat siapa, dari mana, apa, dan hasilnya — tanpa password, token, atau data pribadi.