A defesa, negar por padrão
Decida o acesso no servidor, negue por padrão e cheque por recurso e por ação; nunca confie no cliente.
Todas as falhas da trilha caem no mesmo conserto, resumido em quatro regras.
Negar por padrão. O estado inicial é "não pode". Só uma regra explícita libera. Use lista de permissão (de papéis, de métodos, de campos), nunca de bloqueio: o que você esquecer de listar fica trancado, não aberto.
Decidir no servidor. A checagem mora onde o cliente não alcança. Nada de confiar no navegador (botão escondido), num cabeçalho (Referer, X-User-Role) ou num campo que veio no corpo. A identidade e o papel vêm da sessão verificada no servidor.
Por recurso e por ação. Não basta "é admin?". Pergunte "este usuário pode FAZER ESTA AÇÃO sobre ESTE recurso?": pode aprovar ESTA fatura? é dono DESTE relatório? Junte papel + ação + dono.
Num lugar só. Uma camada central de autorização, chamada por toda rota, normalizando o pedido antes de checar, em vez de uma checagem espalhada e diferente em cada endpoint.
const mayApprove = POLICY[role]?.['invoices:approve'] === true; // allow-list, nega por padrão
if (!mayApprove) return deny();
if (!ownsInvoice(session.userId, id)) return deny(); // por recurso
Em Python, o mesmo: role = session["role"] (não um cabeçalho), require_permission("invoices:approve") e uma checagem de dono por recurso.