Cómo prevenir ataques de Broken Access Control en Solidity

Autor del artículo

Los ataques de control de acceso defectuoso ocurren cuando un contrato Solidity permite que un llamador no confiable acceda a una capacidad que debería estar restringida, como mover activos, cambiar una implementación, asignar roles o configurar una dirección de confianza. El atacante no necesita quebrar la criptografía: explota una comprobación de autorización faltante, incorrecta o demasiado amplia para escalar efectivamente sus privilegios.

El impacto puede afectar a todo el protocolo, ya que las funciones privilegiadas suelen controlar fondos agrupados o configuraciones críticas para la gobernanza. Poly Network perdió $611.0M el 10-08-2021 en Ethereum, BSC y Polygon en un incidente clasificado por DefiLlama como Control de Acceso Inapropiado. La autorización, por tanto, no es solo una preocupación de la interfaz de usuario o un detalle administrativo; es parte del límite de seguridad central del contrato.

¿Qué es una vulnerabilidad de control de acceso defectuoso en Solidity?

Una vulnerabilidad de control de acceso defectuoso existe cuando un contrato no verifica de manera confiable que msg.sender está autorizado para una acción privilegiada, o cuando su modelo de autorización concede más autoridad de la que requiere esa acción. En Solidity, la falla suele aparecer en funciones que pueden ser llamadas externamente y que modifican almacenamientos sensibles, transfieren activos, actualizan código o cambian contrapartes de confianza.

Un contrato puede ser vulnerable incluso cuando tiene una variable owner. La pregunta clave es si cada ruta de ejecución privilegiada comprueba correctamente esa autoridad, incluyendo funciones auxiliares, inicializadores, funciones de gestión de roles y ganchos de actualización.

Un ataque típico de control de acceso defectuoso sigue esta secuencia:

  1. El atacante identifica una función callable externamente con consecuencias privilegiadas.
  2. La función no verifica el llamador, verifica la dirección equivocada, o se basa en un estado controlado por el atacante.
  3. El atacante llama directamente a la función, eludiendo restricciones frontend.
  4. La llamada otorga un rol, cambia una dirección de confianza, o invoca una operación de movimiento de activos.
  5. El atacante usa la capacidad recién adquirida para extraer fondos o alterar el comportamiento del protocolo.
  6. El contrato acepta la transacción porque la EVM solo aplica verificaciones en la cadena.

Los botones en la interfaz, la documentación y los procedimientos operativos previstos no restringen las llamadas directas al contrato. Cualquier cuenta puede enviar calldata a una función pública o externa. Por ello, el código Solidity debe imponer la política exactamente donde ocurre la transición sensible de estado.

¿Qué patrones en Solidity causan ataques de control de acceso y escalada de privilegios?

Las vulnerabilidades por control de acceso inapropiado surgen comúnmente cuando los desarrolladores exponen configuradores, ayudantes administrativos o caminos de inicialización sin considerar que toda función pública constituye una API vulnerable a ataques. El patrón peligroso no se limita a la ausencia total del modificador onlyOwner, sino que también incluye autorizaciones mutable por llamadores arbitrarios, heredadas incorrectamente, aplicadas de forma inconsistente, o demasiado amplias para la operación.

Los patrones de fallo habituales incluyen:

  • Configuradores públicos que permiten a cualquiera reemplazar un tesoro, oráculo, implementación, operador o beneficiario.
  • Funciones de gestión de roles sin verificación de administrador.
  • Inicializadores que pueden llamarse después de la implementación o por una cuenta no prevista.
  • Funciones que verifican tx.origin en lugar de msg.sender.
  • Funciones internas privilegiadas accedidas mediante un envoltorio externo sin protección.
  • Un solo rol de administrador que puede realizar todas las acciones operativas, aumentando el radio de impacto en caso de compromiso.
  • Autorización basada en una dirección suministrada por el usuario en lugar de un rol inmutable o controlado.
  • Funciones de actualización que verifican el llamador, pero no la nueva implementación o camino de actualización.

Este patrón defectuoso es especialmente sutil:

function setOperator(address newOperator) external {
    operator = newOperator;
}

La función de pago puede requerir correctamente msg.sender == operator, pero la frontera de control de acceso ya ha fallado porque cualquiera puede nombrarse a sí mismo operador. Los revisores deben rastrear hacia atrás desde cada función sensible para identificar quién puede adquirir el rol requerido.

La metodología de Soken trata la asignación de roles y la configuración de direcciones de confianza como operaciones privilegiadas en sí mismas. Un diseño de control de acceso seguro protege tanto la transferencia final de activos como cada ruta que puede desbloquearla.

¿Cómo se ve un ataque de control de acceso defectuoso en código Solidity?

Este vault vulnerable verifica al llamador antes de un pago, pero su función setPayoutOperator es pública. Un atacante puede asignarse a sí mismo como operador y luego invocar pay, haciendo que la verificación de autorización posterior carezca de sentido.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerablePayoutVault {
    address public payoutOperator;

    constructor(address initialOperator) {
        payoutOperator = initialOperator;
    }

    receive() external payable {}

    function setPayoutOperator(address newOperator) external {
        // VULNERABLE: cualquier llamador puede concederse autoridad de pago.
        payoutOperator = newOperator;
    }

    function pay(address payable recipient, uint256 amount) external {
        require(msg.sender == payoutOperator, "not operator");
        require(address(this).balance >= amount, "saldo insuficiente");

        (bool sent, ) = recipient.call{value: amount}("");
        require(sent, "pago fallido");
    }
}

La solución separa la administración de las operaciones. Solo el dueño puede otorgar o revocar el rol de pago de alcance restringido, mientras que un operador de pagos no puede administrar roles ni cambiar la propiedad.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FixedPayoutVault {
    address public owner;
    mapping(address => bool) public payoutOperators;

    modifier onlyOwner() {
        require(msg.sender == owner, "no es propietario");
        _;
    }

    modifier onlyPayoutOperator() {
        require(payoutOperators[msg.sender], "no es operador de pago");
        _;
    }

    constructor(address initialOwner, address initialOperator) {
        require(initialOwner != address(0), "propietario cero");
        require(initialOperator != address(0), "operador cero");
        owner = initialOwner;
        payoutOperators[initialOperator] = true;
    }

    receive() external payable {}

    function setPayoutOperator(address operator, bool allowed) external onlyOwner {
        require(operator != address(0), "operador cero");
        // FIX: solo el propietario puede modificar la membresía del rol privilegiado.
        payoutOperators[operator] = allowed;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "propietario cero");
        owner = newOwner;
    }

    function pay(address payable recipient, uint256 amount)
        external
        onlyPayoutOperator
    {
        require(address(this).balance >= amount, "saldo insuficiente");
        (bool sent, ) = recipient.call{value: amount}("");
        require(sent, "pago fallido");
    }
}

Si existen cambios de rol, permisos de actualización, o retiradas del tesoro en tu sistema, una auditoría y evaluación de seguridad centrada en control de acceso puede seguir si cada ruta hacia esas capacidades está correctamente restringida.

¿Cómo pueden los desarrolladores de Solidity prevenir vulnerabilidades por control de acceso inapropiado?

Prevenir ataques de control de acceso defectuoso requiere un modelo de permisos explícito, no la adición dispersa de require tras completar la funcionalidad. Define qué actor puede realizar cada operación sensible, por qué esa autoridad existe y qué rol menor puede realizar tareas rutinarias de forma segura.

Técnica Lo que previene Costo en gas o complejidad Limitación
Modificador onlyOwner Administración no autorizada de funciones solo para el propietario Una lectura de almacenamiento por llamada protegida Un propietario comprometido mantiene toda la autoridad de propietario
Mapeos de roles o AccessControl de OpenZeppelin Uso no autorizado de roles operativos separados Lecturas de almacenamiento y lógica de gestión de roles Mal diseño de roles puede crear privilegios excesivos
Roles con mínimo privilegio Un guardián u operador usando poderes administrativos no relacionados Más funciones y definiciones de roles para mantener Requiere límites operativos claros
Timelocks para cambios críticos Actualizaciones maliciosas inmediatas o cambios en parámetros Ejecución diferida y contratos adicionales No previene acción maliciosa si los usuarios no responden en el plazo
Administración mediante multisig Una sola clave cambia unilateralmente el estado privilegiado Coordinación de firmantes y sobrecarga operativa Claves compartidas o políticas débiles siguen siendo riesgosas
Guardias de inicialización única Inicialización no autorizada o repetida Una bandera de almacenamiento y lógica de inicialización Los procedimientos de despliegue aún deben establecer la autoridad deseada

Utiliza primitivas de propiedad y roles revisadas y confiables cuando sea apropiado, pero no trates una librería como un modelo de seguridad completo. Por ejemplo, Ownable de OpenZeppelin establece la mecánica de propiedad, mientras que el equipo del protocolo aún debe decidir si la propiedad controla pausas, actualizaciones, recaudación de tarifas, reemplazo de oráculos y recuperación de emergencias.

Evita tx.origin para autorización. Puede ser influido por contratos intermediarios y no identifica al llamador inmediato. Usa msg.sender, y luego verifica si coincide con un propietario definido, rol, multisig, o regla de autorización específica del contrato.

¿Qué incidentes reales ilustran el impacto de ataques por control de acceso?

DefiLlama registra 130 incidentes en smart contracts clasificados como Control de Acceso Inapropiado desde 2020 hasta 2026, excluyendo brechas en exchanges y wallets, con pérdidas combinadas de $909.7M. Los siguientes incidentes verificados muestran que las fallas en control de acceso afectan puentes, protocolos DeFi, bots y aplicaciones en múltiples cadenas.

Protocolo Fecha Monto Cadena
Poly Network 2021-08-10 $611.0M Ethereum, BSC, Polygon
Mirror 2021-10-08 $90.0M Terra
Wintermute 2022-06-05 $17.6M Optimism
ValueDefi 2021-05-05 $10.0M BSC
Cork V1 2025-05-28 $12.0M Ethereum
SUPERFORTUNE AI 2026-05-27 $15.2M Ethereum
The Sandbox 2026-08-22 $675K Base, BSC, Ethereum
Radix 2026-08-31 $1.2M Radix

Estos incidentes comparten una lección de seguridad: el valor económico de un protocolo puede quedar expuesto cuando los límites de autoridad de un contrato no coinciden con su modelo de confianza previsto. La función específica afectada puede variar, pero la clase de falla es la misma: un actor obtiene o ejerce autoridad que el diseño debió restringir.

¿Cómo detectar vulnerabilidades por control de acceso defectuoso antes del despliegue?

Detectar vulnerabilidades requiere mapear los cambios de estado sensibles a sus condiciones de autorización, y demostrar que ningún camino alternativo puede sortear esas condiciones. Herramientas automatizadas ayudan a identificar patrones sospechosos, pero la revisión manual es esencial para entender la autoridad empresarial, supuestos de confianza y cadenas de escalada de privilegios.

Utiliza esta lista de verificación:

  1. Enumera cada función que transfiere activos, cambia roles, actualiza código, pausa actividades o modifica direcciones de confianza.
  2. Identifica quién debe ser el llamador y qué regla de autorización corresponde en cada función.
  3. Rastrea cada función que pueda conceder, revocar, transferir o inicializar esa autoridad.
  4. Verifica todos los envoltorios externos que lleguen a funciones internas privilegiadas.
  5. Confirma que la inicialización solo ocurra una vez y solo bajo el proceso previsto de despliegue.
  6. Asegúrate de que los roles de emergencia no puedan realizar acciones rutinarias no relacionadas o de gobernanza.
  7. Prueba la revocación de roles, transferencia de propiedad y manejo de direcciones cero.
  8. Revisa proxies, contratos heredados y caminos delegatecall en el mismo límite de autoridad.

Slither puede marcar patrones de autorización ausentes, uso peligroso de tx.origin, y modificadores inconsistentes, pero sus hallazgos necesitan validación manual. La fuzzing con Foundry es útil para generar llamadas desde direcciones arbitrarias y verificar que los llamadores no autorizados no puedan modificar estados privilegiados.

Las pruebas de invariantes son particularmente efectivas. Por ejemplo, una invariancia del vault puede indicar que una dirección sin rol de pago nunca debe reducir el saldo del vault a través de pay. Otra puede definir que solo el propietario puede alterar la membresía de roles. Estas propiedades prueban directamente la regla de seguridad sin depender únicamente de los flujos esperados de usuario.

Insight de seguridad, Metodología Soken: Revisa la autorización como un grafo de capacidades, no solo como una lista de modificadores. Cada resultado privilegiado debe tener un camino deliberado y auditable hacia la autoridad, y cada camino no deseado es una posible ruta de escalada.

Los ataques de control de acceso defectuoso se previenen haciendo la autoridad explícita, minimizando los poderes de cada rol, y protegiendo las funciones que asignan esos poderes con tanto cuidado como las funciones que mueven fondos. Una función de retiro correctamente restringida no será segura si cualquiera puede nombrar al retirador.

Comienza construyendo un mapa de autoridad para tus contratos desplegados y planificados, incluidos proxies y cuentas operativas, y convierte cada regla en un modificador y una prueba de invariancia. Para una investigación de seguridad Solidity más profunda y clases de vulnerabilidades relacionadas, explora el Soken Hub.

Autor del artículo

Preguntas frecuentes

¿Qué es una vulnerabilidad de broken access control en Solidity?

Una vulnerabilidad de broken access control en Solidity ocurre cuando un contrato no confirma de manera confiable que msg.sender está autorizado para una acción privilegiada. Esto permite a un agente no confiable acceder a capacidades de cambio de estado y escalar privilegios.

¿Cómo crean los ataques de broken access control escalada de privilegios?

Estos ataques permiten que un actor no confiable en Solidity tenga acceso a capacidades reservadas para usuarios autorizados, pudiendo realizar acciones como mover fondos, cambiar implementaciones, asignar roles o configurar direcciones confiables.

¿Qué acciones en Solidity deben estar restringidas por control de acceso?

Se deben restringir todas las capacidades de cambio de estado no destinadas a cualquier llamador. Ejemplos clave incluyen mover fondos, cambiar implementaciones, asignar roles y configurar direcciones confiables, asegurando decisiones de autorización confiables en cada caso.

¿Qué ocurrió en el incidente de control de acceso en Poly Network?

Poly Network perdió $611M en 2021-08-10 en Ethereum, BSC y Polygon. Se clasificó como un caso de Improper Access Control, demostrando la importancia de las verificaciones de autorización para proteger fondos y configuraciones críticas del protocolo.

¿Por qué la autorización es una frontera de seguridad clave en Solidity?

Porque funciones privilegiadas controlan fondos y configuraciones críticas. Una verificación de autorización ausente, incorrecta o demasiado permisiva puede permitir accesos no autorizados, convirtiendo una debilidad administrativa en un riesgo de seguridad a nivel de todo el protocolo.

Chat