Control de acceso

MySQL usa un sistema de dos niveles para controlar el acceso a los procedimientos almacenados: quién puede ejecutarlos y con qué permisos se ejecutan. Esto permite crear una capa de seguridad donde los usuarios acceden a datos a través de procedimientos sin tener acceso directo a las tablas. Puedes cambiar el modo de seguridad de un procedimiento existente con ALTER PROCEDURE sin necesidad de eliminarlo.

Este sistema de control de acceso es una de las razones más poderosas para usar procedimientos almacenados en entornos empresariales. En lugar de dar a los usuarios de la aplicación permisos SELECT, INSERT y UPDATE sobre todas las tablas, puedes restringirlos a ejecutar solo un conjunto específico de procedimientos. Cada procedimiento implementa validaciones, reglas de negocio y controles que garantizan la integridad de los datos, algo que un UPDATE directo no puede garantizar.

SQL SECURITY: DEFINER vs INVOKER

La propiedad SQL SECURITY determina con qué permisos se ejecutan las sentencias SQL dentro del procedimiento. Es la pieza central del modelo de seguridad:

ModoEjecuta con permisos deUso típico
DEFINEREl usuario que creó el procedimientoDar acceso controlado a datos
INVOKEREl usuario que llama al procedimientoRespetar permisos individuales del usuario

Esta distinción es más importante de lo que parece a primera vista. Con DEFINER (el modo por defecto), un usuario puede ejecutar sentencias que normalmente no tendría permiso de ejecutar, porque el servidor usa los permisos del creador del procedimiento, no los del llamador. Es como si el creador le hubiera dado una "llave controlada" para acceder a ciertos datos.

DEFINER: permisos del creador

Con SQL SECURITY DEFINER, el procedimiento se ejecuta como si lo estuviera ejecutando el usuario que lo creó (normalmente root o un usuario administrador). Esto permite dar acceso a datos sensibles de forma controlada:

DELIMITER //
 
CREATE PROCEDURE sp_datos_sensibles()
SQL SECURITY DEFINER
BEGIN
    SELECT id, nombre, email, telefono
    FROM clientes
    ORDER BY nombre;
END //
 
DELIMITER ;

Con SQL SECURITY DEFINER, un usuario sin permiso SELECT en la tabla clientes puede ejecutar este procedimiento si tiene permiso EXECUTE, porque se ejecuta con los permisos del creador (que sí tiene SELECT). El usuario accede a los datos, pero solo de la forma que el procedimiento permite: no puede añadir un WHERE arbitrario, no puede hacer SELECT * para ver columnas adicionales, y no puede modificar los datos.

Este patrón es la base de lo que en seguridad informática se conoce como principio de mínimo privilegio: en lugar de dar al usuario acceso amplio a las tablas, le das acceso estrecho a través de procedimientos que solo exponen lo necesario.

INVOKER: permisos del que llama

Con SQL SECURITY INVOKER, el procedimiento se ejecuta con los permisos del usuario que lo llama. Si ese usuario no tiene SELECT en la tabla clientes, el procedimiento fallará:

DELIMITER //
 
CREATE PROCEDURE sp_mis_datos()
SQL SECURITY INVOKER
BEGIN
    SELECT id, nombre, email
    FROM clientes;
END //
 
DELIMITER ;

Con SQL SECURITY INVOKER, el procedimiento falla si el usuario que lo llama no tiene permiso SELECT en la tabla clientes. No hereda los permisos de nadie: cada usuario que lo ejecuta necesita tener sus propios permisos sobre las tablas involucradas.

INVOKER es útil para procedimientos utilitarios que simplifican operaciones comunes pero no deben elevar privilegios. Por ejemplo, un procedimiento que genera un reporte formateado: la lógica de formateo la aporta el procedimiento, pero los permisos de acceso a los datos los tiene (o no) el usuario que lo llama.

GRANT EXECUTE

El permiso EXECUTE controla quién puede llamar al procedimiento. Es independiente del modo SQL SECURITY: necesitas EXECUTE para poder hacer CALL, independientemente de si el procedimiento luego usa DEFINER o INVOKER:

-- Dar permiso para ejecutar un procedimiento específico
GRANT EXECUTE ON PROCEDURE tienda_mysql.sp_datos_sensibles TO 'usuario_app'@'localhost';
 
-- Dar permiso para ejecutar todos los procedimientos de la base de datos
GRANT EXECUTE ON tienda_mysql.* TO 'usuario_app'@'localhost';
 
-- Revocar permiso
REVOKE EXECUTE ON PROCEDURE tienda_mysql.sp_datos_sensibles FROM 'usuario_app'@'localhost';

La combinación de GRANT EXECUTE con SQL SECURITY DEFINER es muy poderosa: le dices a MySQL "este usuario puede ejecutar este procedimiento, y cuando lo haga, ejecuta las sentencias internas como si fuera el usuario administrador". El resultado es que el usuario de la aplicación puede hacer exactamente lo que el procedimiento permite, ni más ni menos.

Ejemplo práctico: acceso controlado

Supongamos que queremos que un usuario de la aplicación pueda consultar y modificar pedidos sin acceso directo a las tablas. Este es un escenario realista: la aplicación web tiene un usuario MySQL propio (app_user) que no debe poder hacer SELECT * FROM clientes o DELETE FROM pedidos directamente:

-- Procedimiento para crear pedido (DEFINER)
DELIMITER //
 
CREATE PROCEDURE sp_crear_pedido(
    IN p_cliente_id INT,
    IN p_producto_id INT,
    IN p_cantidad INT,
    OUT p_pedido_id INT
)
SQL SECURITY DEFINER
MODIFIES SQL DATA
COMMENT 'Crea un nuevo pedido con validación'
BEGIN
    DECLARE v_precio DECIMAL(10,2);
    DECLARE v_stock INT;
 
    -- Validar stock
    SELECT precio, stock INTO v_precio, v_stock
    FROM productos WHERE id = p_producto_id AND activo = TRUE;
 
    IF v_stock IS NULL THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Producto no encontrado o inactivo';
    END IF;
 
    IF v_stock < p_cantidad THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Stock insuficiente';
    END IF;
 
    -- Crear pedido
    INSERT INTO pedidos (cliente_id, fecha, total, estado)
    VALUES (p_cliente_id, CURDATE(), v_precio * p_cantidad, 'pendiente');
 
    SET p_pedido_id = LAST_INSERT_ID();
 
    INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
    VALUES (p_pedido_id, p_producto_id, p_cantidad, v_precio);
 
    UPDATE productos SET stock = stock - p_cantidad WHERE id = p_producto_id;
END //
 
DELIMITER ;

El procedimiento valida el stock antes de crear el pedido y usa SIGNAL para generar errores descriptivos si algo falla. Toda esta lógica se ejecuta como una unidad: no hay forma de que el usuario de la aplicación cree un pedido sin validación de stock, porque no tiene INSERT directo en la tabla pedidos.

-- Procedimiento para consultar pedidos del cliente
DELIMITER //
 
CREATE PROCEDURE sp_mis_pedidos(IN p_cliente_id INT)
SQL SECURITY DEFINER
READS SQL DATA
BEGIN
    SELECT p.id, p.fecha, p.total, p.estado,
           GROUP_CONCAT(pr.nombre SEPARATOR ', ') AS productos
    FROM pedidos p
    JOIN detalle_pedidos dp ON p.id = dp.pedido_id
    JOIN productos pr ON dp.producto_id = pr.id
    WHERE p.cliente_id = p_cliente_id
    GROUP BY p.id, p.fecha, p.total, p.estado
    ORDER BY p.fecha DESC;
END //
 
DELIMITER ;
-- Dar permisos al usuario de la aplicación
-- GRANT EXECUTE ON PROCEDURE tienda_mysql.sp_crear_pedido TO 'app_user'@'localhost';
-- GRANT EXECUTE ON PROCEDURE tienda_mysql.sp_mis_pedidos TO 'app_user'@'localhost';

El usuario app_user puede crear y consultar pedidos sin tener SELECT, INSERT o UPDATE en las tablas directamente. Esto significa que incluso si la aplicación tiene una vulnerabilidad de SQL injection, el atacante solo puede llamar a los procedimientos disponibles, no ejecutar sentencias arbitrarias sobre las tablas.

Ver el DEFINER

Cada procedimiento tiene un DEFINER asociado que se establece al crearlo. Puedes consultarlo en information_schema:

SELECT ROUTINE_NAME, DEFINER, SECURITY_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'tienda_mysql'
AND ROUTINE_TYPE = 'PROCEDURE';
ROUTINE_NAMEDEFINERSECURITY_TYPE
sp_datos_sensiblesroot@localhostDEFINER
sp_mis_datosroot@localhostINVOKER
sp_crear_pedidoroot@localhostDEFINER
sp_mis_pedidosroot@localhostDEFINER

Si el DEFINER es un usuario que ya no existe (por ejemplo, se eliminó la cuenta), los procedimientos con SQL SECURITY DEFINER fallarán al ejecutarse. Esto es importante al migrar bases de datos entre servidores: los procedimientos conservan su DEFINER original, que puede no existir en el servidor de destino.

Cambiar el modo de seguridad

Para cambiar el modo de seguridad de un procedimiento existente sin eliminarlo (y sin perder los GRANT EXECUTE asociados), usa ALTER PROCEDURE:

ALTER PROCEDURE sp_mis_datos
SQL SECURITY DEFINER;

Ver permisos de un usuario

Para verificar qué permisos tiene un usuario específico:

SHOW GRANTS FOR 'usuario_app'@'localhost';

Esto mostrará todos los privilegios, incluidos los EXECUTE sobre procedimientos específicos.

Recomendaciones

La elección entre DEFINER e INVOKER depende del caso de uso. Estas son las directrices generales:

EscenarioRecomendaciónRazón
API de datos para aplicacionesDEFINER + GRANT EXECUTEControl total sobre qué puede hacer la aplicación
Procedimientos administrativosINVOKERRespeta los permisos de cada administrador
Reportes para usuarios limitadosDEFINERPermite acceso a datos que el usuario no puede consultar directamente
Procedimientos utilitariosINVOKERNo deben elevar privilegios

En la mayoría de los sistemas de producción, los procedimientos que interactúan con datos de negocio usan DEFINER (para controlar el acceso), mientras que los procedimientos de utilidad o administración usan INVOKER (para respetar el nivel de acceso de cada usuario).

La combinación de DEFINER con una política estricta de GRANT EXECUTE crea un sistema donde la base de datos actúa como una API: los consumidores (aplicaciones, usuarios) solo pueden realizar las operaciones predefinidas, con las validaciones incorporadas, sin posibilidad de saltárselas.

Limpieza

DROP PROCEDURE IF EXISTS sp_datos_sensibles;
DROP PROCEDURE IF EXISTS sp_mis_datos;
DROP PROCEDURE IF EXISTS sp_crear_pedido;
DROP PROCEDURE IF EXISTS sp_mis_pedidos;

Con esto completamos la sección de procedimientos almacenados. Hemos cubierto desde la creación básica hasta el control de flujo, cursores y seguridad. En la siguiente sección exploraremos el manejo de errores dentro de procedimientos.

Escrito por Eduardo Lázaro