Jebakan DRY pada Validasi Input dan Trust Boundary

Prinsip Don't Repeat Yourself (DRY) sering disalahartikan sebagai kewajiban untuk menyatukan struktur data yang tampak identik secara struktural. Dalam konteks otorisasi API dan validasi input, abstraksi prematur adalah celah keamanan struktural. Seperti yang diuraikan dalam artikel When duplicate code is the better design, duplikasi kode jauh lebih murah daripada abstraksi yang salah (wrong abstraction).

Ketika satu Data Transfer Object (DTO) digunakan bersama oleh konteks otorisasi yang berbeda—seperti endpoint publik (tamu), pengguna terautentikasi (customer), dan operator internal (admin)—skema validasi tersebut kehilangan batas kepercayaan (trust boundary). Perubahan yang dibuat untuk mendukung alur kerja admin dapat secara tidak sengaja membuka akses manipulasi data pada konteks publik.

Anatomi Masalah: Mass Assignment dan Privilege Escalation

Kerentanan muncul ketika aplikasi memetakan payload request HTTP langsung ke domain entity atau model persistensi basis data. Skenario klasik terjadi pada entitas pengguna yang memiliki atribut dasar (name, email) dan atribut otoritatif (role, is_verified, balance, tenant_id).

Jika endpoint pembaruan profil pengguna publik (PATCH /api/v1/profile) dan endpoint manajemen internal (PATCH /api/v1/admin/users/:id) berbagi satu skema DTO yang sama dengan membuat atribut otorisasi menjadi opsional, kerentanan Mass Assignment terjadi. Penyerang cukup mengirimkan atribut yang valid secara tipe data namun terlarang secara konteks.

// Contoh payload berbahaya yang dikirim customer ke endpoint profil publik
{
  "name": "Budi Santoso",
  "role": "SUPERADMIN",
  "is_verified": true
}

Jika DTO bersama tidak menolak field tersebut karena validator menganggap role sah untuk konteks admin, penyerang berhasil melakukan Privilege Escalation.

Perbandingan Implementasi: TypeScript dan Zod

Berikut adalah perbandingan implementasi antara skema DRY yang rentan terhadap bypass dan skema terisolasi yang menerapkan duplikasi eksplisit per boundary.

1. Pendekatan Rentan (Over-DRY Shared Schema)

Menggunakan satu skema dasar untuk semua konteks operasional dengan memanfaatkan operator opsional atau pewarisan tipe parsial yang longgar.

import { z } from "zod";

// Skema bersama yang mencoba melayani semua konteks
export const UserUpdateSchema = z.object({
  name: z.string().min(3).optional(),
  email: z.string().email().optional(),
  role: z.enum(["CUSTOMER", "ADMIN"]).optional(),
  isVerified: z.boolean().optional(),
  balance: z.number().nonnegative().optional(),
});

// Middleware atau handler profil customer
export async function handleCustomerProfileUpdate(input: unknown) {
  // Rentan: Data yang diparsing meloloskan 'role' jika disertakan dalam request body
  const safeData = UserUpdateSchema.parse(input);
  
  // Jika developer lupa memfilter safeData sebelum diteruskan ke database query,
  // penyerang dapat mengubah role mereka sendiri.
  return safeData;
}

2. Pendekatan Hardened (Duplikasi Eksplisit per Boundary)

Masing-masing batas otorisasi mendefinisikan DTO mandiri dengan penolakan eksplisit terhadap field tak dikenal (strict parsing). Tidak ada dependensi langsung antar boundary.

import { z } from "zod";

// Boundary 1: Customer Profile Context
// Hanya mengizinkan mutasi atribut mandiri milik user biasa.
export const CustomerProfileUpdateSchema = z
  .object({
    name: z.string().trim().min(3).max(100),
    email: z.string().email(),
  })
  .strict(); // Menolak request jika ada atribut asing (misal: 'role', 'balance')

export type CustomerProfileUpdateDTO = z.infer<typeof CustomerProfileUpdateSchema>;

// Boundary 2: Admin User Management Context
// Mengizinkan mutasi atribut otoritatif oleh operator dengan izin khusus.
export const AdminUserUpdateSchema = z
  .object({
    name: z.string().trim().min(3).max(100).optional(),
    email: z.string().email().optional(),
    role: z.enum(["CUSTOMER", "ADMIN"]),
    isVerified: z.boolean(),
    balance: z.number().nonnegative(),
  })
  .strict();

export type AdminUserUpdateDTO = z.infer<typeof AdminUserUpdateSchema>;

Dengan pemisahan ini, request customer yang menyertakan role akan langsung digugurkan di lapisan validasi HTTP dengan galat unrecognized keys tanpa menyentuh logika domain maupun basis data.

Trade-offs dan Overhead Pemeliharaan

Menerapkan duplikasi skema memiliki konsekuensi arsitektural yang harus dikelola:

  • Overhead Redundansi: Jika format validasi primitif dasar berubah (misal: batas panjang karakter nama), perubahan perlu direplikasi ke beberapa file skema. Solusi aman: abstraksikan scalar type (seperti UserName = z.string().trim().min(3)), bukan aggregate object DTO.
  • Ukuran Bundle dan Memory Footprint: Pada aplikasi serverless dengan batas memori ketat, duplikasi kelas atau skema objek menambah ukuran artefak build, meski secara praktis dampaknya minimal dibanding risiko pelanggaran data.

Checklist Arsitektur: Duplikasi vs Abstraksi

Gunakan kriteria berikut untuk menentukan kapan kode validasi wajib diduplikasi dan kapan aman diabstraksikan.

Wajib Menggunakan Skema DTO Terduplikasi (Isolasi Boundary):

  • Perbedaan Aktor Akses: Endpoint dikonsumsi oleh aktor dengan tingkat izin berbeda (Tamu vs Pelanggan vs Internal Admin).
  • Mutasi State Sensitif: Payload memengaruhi status verifikasi, relasi kepemilikan data (tenant_id, user_id), atau hak akses (role, permissions).
  • Siklus Hidup Endpoint Independen: Persyaratan kontrak API publik memiliki kebutuhan stabilitas versi (v1, v2) yang tidak boleh terganggu oleh refaktor internal admin.
  • Model Validasi Ketat: Ingress parser mengaktifkan strategi reject unknown fields secara otomatis.

Aman Menerapkan Abstraksi (Shared Definitions):

  • Atribut Primitif Skalar: Validator untuk format data universal yang tidak bergantung peran aktor, seperti format UUID, regex nomor telepon E.164, atau format tanggal ISO 8601.
  • Filter Query Read-Only Standar: Skema parameter paginasi generik (page, limit, sort_order) yang beroperasi pada lapisan pembacaan data tanpa mutasi state.
  • Internal Pure Domain Invariants: Validasi aturan bisnis di dalam domain model (Entities/Value Objects) setelah DTO ingress berhasil dipetakan secara aman.