Sua função processPayment tem acesso ao banco de dados, ao gateway de pagamento e ao log de auditoria simplesmente porque vive no mesmo processo. Se um atacante encontra um bug de injeção em um HTTP handler, ele herda tudo isso. A função nunca pediu esses poderes. Ela simplesmente os assumiu em virtude de onde foi implantada.

Isso é autoridade ambiente (ambient authority), e é o modelo padrão em quase todos os sistemas que construímos. A segurança baseada em capabilities faz uma pergunta diferente: e se uma função só pudesse fazer o que lhe foi explicitamente entregue?

O problema da autoridade ambiente

A maioria das aplicações usa listas de controle de acesso verificadas na fronteira do sistema. Uma requisição atinge um API gateway, o JWT é validado, as roles são verificadas, e então a requisição entra em uma zona de confiança onde cada função tem acesso implícito a tudo. A conexão com o banco de dados é um singleton global. O cliente S3 é importado de um module compartilhado. Qualquer código que roda dentro do processo pode invocar qualquer um deles.

Isso funciona até que não funcione mais.

Um bug de desserialização em uma função utilitária, uma dependência comprometida ou um ataque de confused deputy transformam uma pequena brecha em acesso total ao sistema. O raio de destruição não é limitado pelo que a operação específica precisava. É limitado por tudo o que o processo inteiro tinha.

A segurança baseada em capabilities inverte o modelo. Autoridade não é uma propriedade de quem você é. É uma propriedade do que você detém.

O que é segurança baseada em capabilities?

Segurança baseada em capabilities é um modelo onde direitos de acesso são representados por tokens infalsificáveis, chamados capabilities, que devem ser explicitamente passados para funções que os necessitam. Uma capability é tanto uma referência a um recurso quanto a permissão para usá-lo. Você não pode solicitar acesso pelo nome. Só pode usar uma capability que já possui.

Isso não é controle de acesso baseado em roles (RBAC) com passos extras. Em RBAC, um usuário tem uma role, e o sistema verifica essa role no momento do acesso. Em segurança baseada em capabilities, não existe verificação central. Se você detém a capability, pode usá-la. Se não detém, não pode. A própria capability é a prova de autorização.

Esse modelo remonta a pesquisas em sistemas operacionais nos anos 1970, mas é cada vez mais relevante à medida que construímos sistemas mais compartimentalizados. modules WebAssembly, microsserviços, APIs de navegador e plugins sandboxados todos usam padrões similares a capabilities, mesmo que não usem o nome.

Como capabilities funcionam na prática

Aqui está como isso se parece em código. No modelo de autoridade ambiente, qualquer função pode chamar o banco de dados:

// 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 as funções compartilham uma conexão global db com acesso total. No modelo de capability, você passa uma capability limitada:

// 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 não pode tocar em pedidos (orders). processOrder não pode tocar em usuários. O compiler impõe isso. Uma implementação comprometida de processOrder só pode exfiltrar pedidos, não apagar tabelas ou ler hashes de senha. A capability é o limite do comportamento.

Em linguagens com sistemas de tipos mais fortes, você pode ir além. Em Rust, uma capability pode possuir um file descriptor. O sistema de tipos garante que ele não pode ser duplicado sem permissão, e o borrow checker garante que ele não sobrevive à sua validade:

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

O &mut File é a capability. A função não pode conjurar outra.

Por que a segurança baseada em capabilities não é o padrão

A segurança baseada em capabilities tem custos reais. O mais óbvio é a ergonomia. A assinatura de cada função cresce. Você tem que passar capabilities por toda a cadeia de chamadas, o que parece injeção de dependências levada ao extremo lógico. Em uma base de código grande, isso pode se tornar tedioso.

O tratamento de erros também fica mais complexo. No modelo de autoridade ambiente, uma falha de conexão com o banco de dados é uma preocupação global tratada na inicialização. No modelo de capability, cada função que recebe uma capability deve considerar o que acontece se essa capability for revogada ou inválida no meio da operação.

Há também um custo de debugging. Quando o acesso é negado em um sistema ACL, você verifica a política. Quando uma capability está faltando, você rastreia a cadeia de chamadas para encontrar quem deveria tê-la passado. Esse é um conjunto de habilidades diferente, e a maioria dos desenvolvedores não está acostumada.

Essas compensações explicam por que a segurança baseada em capabilities tem sido predominantemente confinada a sistemas operacionais, navegadores e ambientes de alta segurança. Não é gratuito.

Onde a segurança baseada em capabilities aparece hoje

Você já usou segurança baseada em capabilities, mesmo sem saber o nome. No navegador, fetch é uma função global, mas é limitada pela política de mesma origem (same-origin policy) e pelo CORS. Um Service Worker recebe capabilities de events específicos. Um module WebAssembly deve ser explicitamente concedido acesso à memória e a funções host.

Em infraestrutura de nuvem, condições de políticas do AWS IAM e tokens com escopo são similares a capabilities. Uma URL pré-assinada do S3 é uma capability: um token infalsificável que concede uma operação específica em um recurso específico por um tempo específico. Service accounts do Kubernetes com RBAC bindings estreitamente limitados também seguem nessa direção.

A tendência é para compartimentos menores com menos autoridade ambiente. Containers a removeram do SO. WebAssembly a removeu do processo do navegador. O próximo passo é removê-la das nossas próprias funções.

Como começar a usar capabilities no seu código

Você não precisa reescrever sua aplicação inteira. Comece nas fronteiras onde o raio de destruição importa mais.

Isolie sua camada de acesso a dados em objetos de capability. Em vez de exportar um pool global de banco de dados, exporte funções que retornam handles limitados:

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

Então passe apenas as capabilities que um handler precisa:

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 não pode enviar e-mails. Não pode deletar contas. Só pode fazer o que lhe foi entregue. Se esse handler tiver um bug de desserialização, o atacante não pode pivotar para o sistema de pagamentos porque a capability nunca foi passada.

Perguntas frequentes sobre segurança baseada em capabilities

Isso não é apenas injeção de dependências?

Parece similar, mas a intenção é diferente. Injeção de dependências é sobre testabilidade e modularidade. Capabilities são sobre segurança e menor privilégio. Um container de DI ainda pode injetar uma conexão global de banco de dados. Uma capability é limitada exatamente ao que o chamador deveria ser permitido fazer.

Isso substitui autenticação?

Não. Autenticação responde “quem é você?” Capabilities respondem “o que você pode fazer?” Você ainda precisa verificar identidade na fronteira. Depois disso, capabilities limitam o que o código autenticado pode tocar.

Quais linguagens suportam isso bem?

Qualquer linguagem com um sistema de tipos pode expressar capabilities como interfaces ou traits. Rust e TypeScript funcionam bem. Em linguagens dinamicamente tipadas como Python ou JavaScript sem interfaces estritas, você perde a imposição em tempo de compilação, mas o padrão ainda melhora a clareza do código e limita o uso acidental indevido.

Autoridade ambiente é um hábito, não uma lei. Segurança baseada em capabilities é mais difícil de adotar do que de entender, mas a direção da indústria é clara. Fronteiras menores. Menos poder implícito. Funções que só podem fazer o que lhes foi explicitamente entregue.

Comece com um handler. Passe a ele uma capability em vez de uma conexão de banco de dados. Veja o que quebra. Na maioria das vezes, o que quebra é uma suposição oculta que você não sabia que seu código estava fazendo.