Kebocoran referensi objek mutable antar-modul merupakan celah struktural yang sering memicu privilege escalation dan korupsi state tanpa terdeteksi layer otorisasi. Ketika modul backend mengembalikan referensi internal langsung atau mengandalkan ambient authority (seperti service locator global atau token lingkungan yang bisa diakses siapa saja), boundary integritas modul tersebut runtuh.
Prinsip OOP yang dirumuskan Alan Kay menekankan bahwa objek semestinya mempertahankan retensi state lokal secara absolut dan hanya berinteraksi via pengiriman pesan (pure message passing). Model Object Capability (OCap) memformalkan prinsip ini: suatu komponen hanya boleh mengeksekusi mutasi jika memegang referensi kapabilitas eksplisit yang tidak dapat dipalsukan (unforgeable), bukan karena identitas global pemanggil.
Bahaya Ambient Authority dan Kebocoran Referensi
Pada arsitektur konvensional, komponen sering kali memiliki dependensi implisit terhadap resource global:
// Anti-pattern: Ambient Authority & Leaked Reference
class WalletService {
private state = { balance: 1000, frozen: false };
// Mengembalikan referensi mutable internal
getState() {
return this.state;
}
}
// Kode eksternal dapat memanipulasi state tanpa validasi modul:
const wallet = new WalletService();
const leakedState = wallet.getState();
leakedState.balance = 999999; // Bypass invariant bisnis
Dalam model OCap, pemegang referensi tidak boleh memiliki wewenang otomatis atas seluruh kapabilitas sistem. Otoritas didelegasikan secara eksplisit dalam bentuk token yang terikat pada sub-aksi tertentu, dan seluruh input pesan diperlakukan sebagai batas ketidakpercayaan (untrusted boundary).
Arsitektur Dispatcher Boundary Berbasis OCap
Untuk mengamankan boundary modul, ada tiga mekanisme wajib:
- Deep Freezing / Defensive Cloning: Mencegah pemanggil memodifikasi payload pesan setelah dikirim (payload tampering / TOCTOU).
- Unforgeable Capability Token: Menggunakan instance unik berbasis closure atau
WeakSetinternal yang tidak dapat direkonstruksi oleh pihak luar. - Caretaker Pattern (Revokasi): Membungkus kapabilitas dengan proxy kontrol yang dapat diputus aksesnya seketika tanpa perlu merestart proses.
Implementasi Kode: TypeScript OCap Boundary
Berikut adalah implementasi modul akun finansial dengan dispatcher ketat, token kapabilitas tidak terpalsukan, pencegahan mutasi langsung, mekanisme revokasi, dan validasi runtime:
// ledger-boundary.ts
// 1. Tipe Pesan dan Token
export type Command =
| { type: 'DEPOSIT'; amount: number }
| { type: 'WITHDRAW'; amount: number };
export interface CapabilityToken {
readonly __brand: unique symbol;
}
export interface Caretaker {
token: CapabilityToken;
revoke: () => void;
}
// 2. Modul Boundary Terisolasi
export function createSecureLedger(initialBalance: number) {
// State internal: Tidak dapat diakses langsung dari scope luar
let balance = initialBalance;
const validTokens = new WeakSet<CapabilityToken>();
// Validasi skema pesan runtime murni tanpa dependensi eksternal
function validateCommand(msg: unknown): asserts msg is Command {
if (typeof msg !== 'object' || msg === null) {
throw new Error('INVALID_PAYLOAD: Payload harus berupa object');
}
const candidate = msg as Record<string, unknown>;
if (candidate.type !== 'DEPOSIT' && candidate.type !== 'WITHDRAW') {
throw new Error(`INVALID_COMMAND_TYPE: ${String(candidate.type)}`);
}
if (typeof candidate.amount !== 'number' || Number.isNaN(candidate.amount) || candidate.amount <= 0) {
throw new Error('INVALID_AMOUNT: Nominal harus angka positif');
}
}
// Factory unforgeable capability token menggunakan closure identity
function issueCapability(): Caretaker {
let isRevoked = false;
const token: CapabilityToken = Object.freeze(Object.create(null));
validTokens.add(token);
return {
token,
revoke: () => {
isRevoked = true;
validTokens.delete(token);
}
};
}
// Dispatcher tunggal: Komunikasi murni via message passing
function dispatch(token: CapabilityToken, rawMessage: unknown): { balance: number } {
// Proteksi OCap: Cek validitas kapabilitas
if (!validTokens.has(token)) {
throw new Error('UNAUTHORIZED_CAPABILITY: Token tidak valid atau telah direvokasi');
}
// Proteksi Payload Tampering: Deep freeze copy untuk mencegah mutasi asinkron
const message = Object.freeze(structuredClone(rawMessage));
validateCommand(message);
// Mutasi state lokal terisolasi
switch (message.type) {
case 'DEPOSIT':
balance += message.amount;
break;
case 'WITHDRAW':
if (balance < message.amount) {
throw new Error('INSUFFICIENT_FUNDS');
}
balance -= message.amount;
break;
}
// Kembalikan immutable snapshot, bukan state pointer internal
// ponytail: snapshot shallow-freeze cukup untuk primitive record. Pakai deepClone jika state kompleks.
return Object.freeze({ balance });
}
return {
issueCapability,
dispatch
};
}
Uji Verifikasi Integritas State (Runnable Assert)
Kode uji mandiri di bawah memvalidasi empat vektor pelanggaran: pemalsuan token, manipulasi payload via referensi bersama, revokasi akses runtime, dan bypass invariant.
// test-runner.ts
import assert from 'node:assert/strict';
import { createSecureLedger, CapabilityToken } from './ledger-boundary.js';
function runSecurityAssertions() {
const ledger = createSecureLedger(500);
const caretaker = ledger.issueCapability();
// 1. Eksekusi Sah via Capability Token
const res1 = ledger.dispatch(caretaker.token, { type: 'DEPOSIT', amount: 200 });
assert.equal(res1.balance, 700, 'Deposit sah harus berhasil mengubah state');
// 2. Uji Mutasi State Ilegal via Snapshot Output
const snapshot = res1 as { balance: number };
assert.throws(() => {
// @ts-expect-error Pengujian runtime freeze
snapshot.balance = 99999;
}, /Cannot assign to read only property/, 'Output snapshot tidak boleh mutable');
// 3. Uji Payload Tampering via Referensi Luar
const mutableCommand = { type: 'WITHDRAW' as const, amount: 100 };
// Jalankan dispatch sambil mencoba memutasi objek secara paralel
ledger.dispatch(caretaker.token, mutableCommand);
mutableCommand.amount = 50000; // Tidak boleh berdampak pada balance yang sudah terisolasi
assert.equal(ledger.dispatch(caretaker.token, { type: 'DEPOSIT', amount: 0.01 }).balance, 600.01);
// 4. Uji Penolakan Token Palsu (Forged / Ambient Authority Token)
const forgedToken = Object.freeze(Object.create(null)) as CapabilityToken;
assert.throws(() => {
ledger.dispatch(forgedToken, { type: 'DEPOSIT', amount: 100 });
}, /UNAUTHORIZED_CAPABILITY/, 'Token palsu harus ditolak');
// 5. Uji Mekanisme Revokasi Kapabilitas
caretaker.revoke();
assert.throws(() => {
ledger.dispatch(caretaker.token, { type: 'DEPOSIT', amount: 100 });
}, /UNAUTHORIZED_CAPABILITY/, 'Token yang direvokasi harus ditolak');
console.log('Semua pengujian OCap boundary berhasil: Integritas modul terverifikasi.');
}
runSecurityAssertions();
Trade-Offs dan Batasan Praktis
- Overhead Kloning: Penggunaan
structuredClonepada boundary menjamin isolasi payload bebas efek samping, tetapi memicu alokasi memori tambahan jika payload berukuran megabytes. Terapkan validasi ukuran (payload size budgeting) sebelum kloning. - Manajemen Siklus Hidup Token: Penggunaan
WeakSetmemastikan token tidak bocor di memori jika modul pemanggil terhapus oleh garbage collection. Namun, status revokasi eksplisit tetap memerlukan referensi handle via caretaker. - Disiplin Boundary Antar-Modul: Arsitektur OCap menuntut disiplin penuh untuk tidak pernah mengekspor instance runtime mutable ke dependency injection container secara terbuka.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!