Tu función processPayment tiene acceso a la base de datos, la pasarela de pagos y el registry de auditoría porque resulta que vive en el mismo proceso. Si un atacante encuentra un bug de inyección en un HTTP handler, hereda todo ello. La función nunca pidió esos poderes. Simplemente los asumió en virtud de dónde fue desplegada.

Esto es autoridad ambiental, y es el modelo por defecto en casi todos los sistemas que construimos. La seguridad basada en capacidades plantea una pregunta diferente: ¿qué pasaría si una función solo pudiera hacer lo que se le entregó explícitamente?

El problema de la autoridad ambiental

La mayoría de las aplicaciones usan listas de control de acceso verificadas en el límite del sistema. Una petición llega a una API gateway, se valida el JWT, se comprueban los roles, y entonces la petición entra en una zona de confianza donde cada función tiene acceso implícito a todo. La conexión a la base de datos es un singleton global. El cliente de S3 se importa desde un module compartido. Cualquier código que se ejecute dentro del proceso puede invocar cualquiera de ellos.

Esto funciona hasta que deja de funcionar.

Un bug de deserialización en una función de utilidad, una dependencia comprometida o un confused deputy attack convierten una pequeña brecha en acceso total al sistema. El radio de explosión no está limitado por lo que la operación específica necesitaba. Está limitado por lo que tenía todo el proceso.

La seguridad basada en capacidades invierte el modelo. La autoridad no es una propiedad de quién eres. Es una propiedad de lo que posees.

¿Qué es la seguridad basada en capacidades?

La seguridad basada en capacidades es un modelo donde los derechos de acceso se representan mediante tokens infalsificables, llamados capacidades, que deben pasarse explícitamente a las funciones que las necesitan. Una capacidad es tanto una referencia a un recurso como el permiso para usarlo. No puedes solicitar acceso por nombre. Solo puedes usar una capacidad que ya poseas.

Esto no es control de acceso basado en roles con pasos extra. En RBAC, un usuario tiene un rol, y el sistema comprueba ese rol en el momento del acceso. En la seguridad basada en capacidades, no hay una verificación central. Si posees la capacidad, puedes usarla. Si no, no puedes. La propia capacidad es la prueba de autorización.

Este modelo se remonta a la investigación en sistemas operativos de los años 70, pero es cada vez más relevante a medida que construimos sistemas más compartimentados. Los modules de WebAssembly, los microservicios, las APIs del navegador y los plugins en sandbox utilizan todos patrones similares a las capacidades, incluso si no usan el nombre.

Cómo funcionan las capacidades en la práctica

Así es como se ve en código. En el modelo de autoridad ambiental, cualquier función puede llamar a la base de datos:

// 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;
}

Ambas funciones comparten una conexión global a db con acceso total. En el modelo de capacidades, pasas una capacidad delimitada:

// 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 no puede tocar órdenes. processOrder no puede tocar usuarios. El compiler lo impone. Una implementación comprometida de processOrder solo puede exfiltrar órdenes, no eliminar tablas ni leer hashes de contraseñas. La capacidad es el límite del comportamiento.

En lenguajes con sistemas de tipos más fuertes, puedes llevar esto más allá. En Rust, una capacidad puede poseer un descriptor de archivo. El sistema de tipos garantiza que no se puede duplicar sin permiso, y el borrow checker garantiza que no sobreviva a su validez:

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)
}

El &mut File es la capacidad. La función no puede conjurar otra.

Por qué la seguridad basada en capacidades no es la opción por defecto

La seguridad basada en capacidades tiene costes reales. El más obvio es la ergonomía. Cada firma de función crece. Tienes que pasar capacidades a través de las cadenas de llamadas, lo que se siente como inyección de dependencias llevada a su extremo lógico. En una base de código grande, esto puede volverse tedioso.

La gestión de errores también se vuelve más compleja. En el modelo de autoridad ambiental, un fallo de conexión a la base de datos es una preocupación global manejada en la inicialización. En el modelo de capacidades, cada función que recibe una capacidad debe considerar qué ocurre si esa capacidad se revoca o es inválida a mitad de la operación.

También hay un coste de depuración. Cuando se deniega el acceso en un sistema ACL, compruebas la política. Cuando falta una capacidad, rastreas hacia atrás a través de la cadena de llamadas para encontrar quién debía pasarla. Este es un conjunto de habilidades diferente, y la mayoría de los desarrolladores no están acostumbrados a ello.

Estos compromisos explican por qué la seguridad basada en capacidades se ha limitado principalmente a sistemas operativos, navegadores y entornos de alta seguridad. No es gratuito.

Dónde aparece la seguridad basada en capacidades hoy

Ya has usado seguridad basada en capacidades, incluso si no conocías el nombre. En el navegador, fetch es una función global, pero está restringida por la same-origin policy y CORS. Un Service Worker recibe capacidades de events específicas. Un module de WebAssembly debe recibir acceso explícito a la memoria y a las funciones del host.

En infraestructura cloud, las condiciones de políticas de AWS IAM y los tokens con alcance son similares a capacidades. Una URL presignada de S3 es una capacidad: un token infalsificable que concede una operación específica sobre un recurso específico durante un tiempo específico. Las cuentas de servicio de Kubernetes con bindings de RBAC estrechamente delimitados también se mueven en esta dirección.

La tendencia es hacia compartimentos más pequeños con menos autoridad ambiental. Los containers la eliminaron del sistema operativo. WebAssembly la eliminó del proceso del navegador. El siguiente paso es eliminarla de nuestras propias funciones.

Cómo empezar a usar capacidades en tu código

No necesitas reescribir toda tu aplicación. Empieza en los límites donde el radio de explosión importa más.

Aísla tu capa de acceso a datos en objetos de capacidad. En lugar de exportar un pool global de base de datos, exporta funciones que devuelvan handles delimitados:

// 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]);
    }
  };
}

Luego pasa solo las capacidades que a handler needs:

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 no puede enviar correos electrónicos. No puede eliminar cuentas. Solo puede hacer lo que se le entregó. Si this handler has un bug de deserialización, el atacante no puede pivotar hacia el sistema de pagos porque la capacidad nunca se pasó.

Preguntas frecuentes sobre la seguridad basada en capacidades

¿No es esto solo inyección de dependencias?

Se parece, pero la intención es diferente. La inyección de dependencias trata sobre testabilidad y modularidad. Las capacidades tratan sobre seguridad y mínimo privilegio. Un container DI podría seguir inyectando una conexión global a la base de datos. Una capacidad está delimitada exactamente a lo que el llamador debería poder hacer.

¿Esto reemplaza la autenticación?

No. La autenticación responde “¿quién eres?”. Las capacidades responden “¿qué puedes hacer?”. Todavía necesitas verificar la identidad en el límite. Después de eso, las capacidades restringen lo que el código autenticado puede tocar.

¿Qué lenguajes soportan esto bien?

Cualquier lenguaje con un sistema de tipos puede expresar capacidades como interfaces o traits. Tanto Rust como TypeScript funcionan bien. En lenguajes de tipado dinámico como Python o JavaScript sin interfaces estrictas, pierdes la aplicación en tiempo de compilación, pero el patrón sigue mejorando la claridad del código y limitando el uso accidental indebido.

La autoridad ambiental es un hábito, no una ley. La seguridad basada en capacidades es más difícil de adoptar que de entender, pero la dirección de la industria es clara. Límites más pequeños. Menos poder implícito. Funciones que solo pueden hacer lo que se les entregó explícitamente.

Empieza con a handler. Pásale una capacidad en lugar de una conexión a la base de datos. Observa qué se rompe. La mayoría de las veces, lo que se rompe es una suposición oculta que no sabías que tu código estaba haciendo.