Fungsi processPayment Anda memiliki akses ke database, gateway pembayaran, dan log audit karena ia kebetulan berada dalam proses yang sama. Jika penyerang menemukan bug injeksi dalam satu HTTP handler, mereka mewarisi semuanya. Fungsi itu tidak pernah meminta kekuatan ini. Ia hanya menganggap memilikinya karena tempat di mana ia dideploy.

Ini adalah otoritas ambient, dan ini adalah model default di hampir setiap sistem yang kita bangun. Keamanan berbasis kemampuan mengajukan pertanyaan yang berbeda: bagaimana jika sebuah fungsi hanya bisa melakukan apa yang secara eksplisit diberikan kepadanya?

Masalah otoritas ambient

Sebagian besar aplikasi menggunakan access control list yang diperiksa di batas sistem. Sebuah permintaan mengenai API gateway, JWT divalidasi, peran diperiksa, lalu permintaan memasuki trust zone di mana setiap fungsi memiliki akses implisit ke segalanya. Koneksi database adalah singleton global. Klien S3 diimpor dari module bersama. Kode apa pun yang berjalan di dalam proses dapat memanggil salah satunya.

Ini berfungsi sampai tidak lagi.

Bug deserialisasi dalam fungsi utilitas, dependency yang telah dikompromikan, atau serangan confused deputy mengubah satu pelanggaran kecil menjadi akses penuh ke sistem. Blast radius tidak dibatasi oleh apa yang dibutuhkan operasi spesifik. Ia dibatasi oleh apa yang dimiliki seluruh proses.

Keamanan berbasis kemampuan membalik model tersebut. Otoritas bukanlah properti dari siapa Anda. Ia adalah properti dari apa yang Anda miliki.

Apa itu keamanan berbasis kemampuan?

Keamanan berbasis kemampuan adalah model di mana hak akses direpresentasikan oleh token yang tidak dapat dipalsukan, yang disebut kemampuan (capabilities), yang harus secara eksplisit diteruskan ke fungsi yang membutuhkannya. Sebuah kemampuan adalah baik referensi ke sumber daya maupun izin untuk menggunakannya. Anda tidak dapat meminta akses berdasarkan nama. Anda hanya dapat menggunakan kemampuan yang sudah Anda miliki.

Ini bukan role-based access control dengan langkah tambahan. Dalam RBAC, pengguna memiliki peran, dan sistem memeriksa peran itu pada saat akses. Dalam keamanan berbasis kemampuan, tidak ada pemeriksaan sentral. Jika Anda memegang kemampuan tersebut, Anda dapat menggunakannya. Jika tidak, Anda tidak bisa. Kemampuan itu sendiri adalah bukti otorisasi.

Model ini berasal dari riset sistem operasi pada tahun 1970-an, tetapi semakin relevan seiring kita membangun sistem yang lebih terkompartemen. Module WebAssembly, microservices, API browser, dan plugin yang di-sandbox semuanya menggunakan pola seperti kemampuan meskipun mereka tidak menggunakan namanya.

Bagaimana kemampuan bekerja dalam praktik

Berikut penampakannya dalam kode. Dalam model otoritas ambient, fungsi apa pun dapat memanggil database:

// Ambient authority: any code in this module can use db
import { db } from './db';

async function getUser(id: string) {
  return db.query('SELECT * FROM users WHERE id = ?', [id]);
}

async function processOrder(orderId: string) {
  const order = await db.query('SELECT * FROM orders WHERE id = ?', [orderId]);
  // What if a bug here lets an attacker query arbitrary tables?
  return order;
}

Kedua fungsi berbagi koneksi db global dengan akses penuh. Dalam model kemampuan, Anda meneruskan kemampuan yang terbatas:

// A capability is just a constrained handle
interface UserReadCap {
  getUser(id: string): Promise<User>;
}

interface OrderReadCap {
  getOrder(id: string): Promise<Order>;
}

async function getUser(id: string, db: UserReadCap) {
  return db.getUser(id);
}

async function processOrder(orderId: string, orders: OrderReadCap) {
  return orders.getOrder(orderId);
}

getUser tidak dapat menyentuh orders. processOrder tidak dapat menyentuh users. Compiler menegakkan ini. Implementasi processOrder yang telah dikompromikan hanya dapat mengeksfiltrasi orders, bukan menghapus tabel atau membaca password hash. Kemampuan adalah batasan pada perilaku.

Dalam bahasa dengan sistem tipe yang lebih kuat, Anda dapat mendorong ini lebih jauh. Dalam Rust, sebuah kemampuan dapat memiliki file descriptor. Sistem tipe memastikan ia tidak dapat diduplikasi tanpa izin, dan borrow checker memastikan ia tidak bertahan lebih lama dari validitasnya:

use std::fs::File;
use std::io::{self, Write};

fn write_log(file: &mut File, msg: &str) -> io::Result<()> {
    // This function can ONLY write to the file it was handed.
    // It cannot open new files. It cannot read the filesystem.
    writeln!(file, "{}", msg)
}

&mut File adalah kemampuannya. Fungsi tersebut tidak dapat memunculkan yang lain.

Mengapa keamanan berbasis kemampuan bukanlah default

Keamanan berbasis kemampuan memiliki biaya nyata. Yang paling jelas adalah ergonomi. Setiap function signature bertambah. Anda harus menyeret kemampuan melalui call chain, yang terasa seperti dependency injection yang dibawa ke ekstrem logisnya. Dalam basis kode yang besar, ini bisa menjadi membosankan.

Error handling juga menjadi lebih kompleks. Dalam model otoritas ambient, kegagalan terhubung ke database adalah masalah global yang ditangani saat inisialisasi. Dalam model kemampuan, setiap fungsi yang menerima kemampuan harus mempertimbangkan apa yang terjadi jika kemampuan tersebut dicabut atau tidak valid di tengah operasi.

Ada juga biaya debugging. Ketika akses ditolak dalam sistem ACL, Anda memeriksa kebijakannya. Ketika sebuah kemampuan hilang, Anda menelusuri kembali call chain untuk menemukan siapa yang seharusnya meneruskannya. Ini adalah seperangkat keterampilan yang berbeda, dan sebagian besar pengembang tidak terbiasa dengannya.

Pertukaran-pertukaran ini menjelaskan mengapa keamanan berbasis kemampuan sebagian besar terbatas pada sistem operasi, browser, dan lingkungan keamanan tinggi. Ini tidak gratis.

Di mana keamanan berbasis kemampuan muncul saat ini

Anda sudah menggunakan keamanan berbasis kemampuan, meskipun Anda tidak mengetahui namanya. Dalam browser, fetch adalah fungsi global, tetapi ia dibatasi oleh same-origin policy dan CORS. Sebuah Service Worker menerima kemampuan acara spesifik. Sebuah module WebAssembly harus secara eksplisit diberikan akses ke memori dan host functions.

Dalam infrastruktur cloud, AWS IAM policy conditions dan scoped token bersifat seperti kemampuan. Sebuah presigned S3 URL adalah sebuah kemampuan: token yang tidak dapat dipalsukan yang memberikan operasi spesifik pada sumber daya spesifik untuk waktu tertentu. Kubernetes service accounts dengan binding RBAC yang berbatas sempit juga bergerak ke arah ini.

Trennya adalah menuju kompartemen yang lebih kecil dengan otoritas ambient yang lebih sedikit. Container menghilangkannya dari OS. WebAssembly menghilangkannya dari proses browser. Langkah berikutnya adalah menghilangkannya dari fungsi kita sendiri.

Cara mulai menggunakan kemampuan dalam kode Anda

Anda tidak perlu menulis ulang seluruh aplikasi Anda. Mulailah dari batas-batas di mana blast radius paling berpengaruh.

Isolasi lapisan akses data Anda ke dalam objek kemampuan. Alih-alih mengekspor pool database global, ekspor fungsi yang mengembalikan scoped handle:

// capabilities.ts
export interface UserStore {
  getById(id: string): Promise<User | null>;
  updateEmail(id: string, email: string): Promise<void>;
}

export interface AuditLog {
  record(event: AuditEvent): Promise<void>;
}

// Hand out capabilities at the application boundary
function createUserStore(db: Pool): UserStore {
  return {
    async getById(id) {
      const row = await db.query('SELECT * FROM users WHERE id = ?', [id]);
      return row ? mapUser(row) : null;
    },
    async updateEmail(id, email) {
      await db.query('UPDATE users SET email = ? WHERE id = ?', [email, id]);
    }
  };
}

Kemudian teruskan hanya kemampuan yang dibutuhkan sebuah handler:

async function handleProfileUpdate(
  req: Request,
  users: UserStore,
  audit: AuditLog
) {
  const user = await users.getById(req.userId);
  await users.updateEmail(req.userId, req.body.email);
  await audit.record({ type: 'email_changed', userId: req.userId });
}

handleProfileUpdate tidak dapat mengirim email. Ia tidak dapat menghapus akun. Ia hanya dapat melakukan apa yang diberikan kepadanya. Jika handler ini memiliki bug deserialisasi, penyerang tidak dapat berpindah ke sistem pembayaran karena kemampuan tersebut tidak pernah diteruskan.

Pertanyaan yang sering diajukan tentang keamanan berbasis kemampuan

Bukankah ini hanya dependency injection?

Ini terlihat serupa, tetapi maksudnya berbeda. Dependency injection adalah tentang testability dan modularitas. Kemampuan adalah tentang keamanan dan least privilege. Sebuah DI container mungkin masih menyuntikkan koneksi database global. Sebuah kemampuan dibatasi pada persis apa yang seharusnya diizinkan untuk dilakukan oleh pemanggil.

Apakah ini menggantikan authentication?

Tidak. Authentication menjawab “siapa Anda?” Kemampuan menjawab “apa yang dapat Anda lakukan?” Anda masih perlu memverifikasi identitas di batas. Setelah itu, kemampuan membatasi apa yang dapat disentuh oleh kode yang telah diautentikasi.

Bahasa apa yang mendukung ini dengan baik?

Bahasa apa pun dengan sistem tipe dapat mengekspresikan kemampuan sebagai interface atau trait. Rust dan TypeScript keduanya berfungsi dengan baik. Dalam bahasa yang diketik secara dinamis seperti Python atau JavaScript tanpa interface yang ketat, Anda kehilangan compile-time enforcement, tetapi pola ini masih meningkatkan kejelasan kode dan membatasi penyalahgunaan yang tidak disengaja.

Otoritas ambient adalah kebiasaan, bukan hukum. Keamanan berbasis kemampuan lebih sulit diadopsi daripada dipahami, tetapi arah industri sudah jelas. Batasan yang lebih kecil. Kekuatan implisit yang lebih sedikit. Fungsi-fungsi yang hanya dapat melakukan apa yang secara eksplisit diberikan kepada mereka.

Mulailah dengan satu handler. Berikan kepadanya satu kemampuan alih-alih koneksi database. Lihat apa yang rusak. Sebagian besar waktu, yang rusak adalah asumsi tersembunyi yang tidak Anda ketahui dibuat oleh kode Anda.