SQL Server

Cómo actúa db_chainig en SQL Server

En su día publicamos un artículo donde analizábamos en profundidad los riesgos de activar la propiedad db_chainig en nuestras bases de datos: db_chainig: una configuración de seguridad peligrosa. Allí abordamos su impacto en la superficie de ataque de una instancia de SQL Server, especialmente cuando se activa de forma global o sin tener un control estricto de propietarios y permisos.

Hoy retomamos el tema con un enfoque diferente. Vamos a mostrar una demo práctica que responde a una duda concreta planteada por un integrante de la comunidad de Telegram “SQL Server Español”, y que nos permite ver de forma clara cómo se comporta esta propiedad cuando varios objetos en distintas bases de datos se relacionan entre sí. Aprovecharé además para precisar algunos matices técnicos que suelen pasar desapercibidos, sobre todo cuando intentamos aplicar el modelo de encapsulación de acceso sin otorgar permisos directos.

Recordatorio: qué es db_chainig y por qué nos debe importar

Como ya explicamos en el artículo de hace meses, db_chainig es una propiedad que habilita el encadenamiento de propietarios entre bases de datos distintas. Cuando está desactivada (valor por defecto), SQL Server impide que un objeto de una base de datos acceda a otro en una base diferente, aunque ambos compartan el mismo propietario y aunque el encadenamiento funcionase sin problema dentro de una única base de datos.

Este comportamiento se introdujo por motivos de seguridad, y su activación requiere un análisis muy cuidadoso. No obstante, existen escenarios legítimos donde permitir este acceso cruzado puede simplificar la arquitectura, como cuando separamos lógica de negocio y almacenamiento físico en distintas bases de datos bajo el mismo control.

La demo: ejemplo de acceso cruzado encapsulado entre bases de datos

Para mostrar cómo se comporta realmente esta propiedad en un escenario típico, he preparado una demo que tenéis disponible aquí.

El entorno consiste en dos bases de datos una con una vista de una tabla de la otra base de datos (podríamos llevarlo al mundo real como una base de datos donde residen los procedimientos que encapsulan la lógica de acceso) y otra base de datos con una tabla (que sería la base de datos donde se encuentran las tablas con los datos reales)

Creamos un usuario solo con privilegios explícitos sobre la base de datos con la vista e intentamos seleccionar la vista que, recordad, lee una tabla de la otra base de datos.

En un primer lugar obtenemos un error porque el usuario no existe en la otra base de datos pero si lo creamos, sin permisos, claro, nos encontramos con otro error porque el usuario no puede acceder a la tabla. 

Hay que recordar que la vista filtra los datos de la tabla por lo que en ningún caso queremos dar al usuario permisos explícitos sobre la tabla, solo debe consultar la información a través de la vista.

Con db_chainig desactivado, la ejecución falla con un error de permisos, incluso si ambos objetos tienen el mismo propietario (dbo). Al activar la propiedad en ambas bases de datos el procedimiento funciona sin que el usuario tenga permisos directos sobre la tabla. Esto demuestra cómo SQL Server evalúa las cadenas de confianza solo si dicha propiedad está activa.

Consideraciones clave sobre propietarios y contexto de ejecución

Uno de los matices más importantes que se observa en la demo es que el encadenamiento entre bases de datos solo se permite si los objetos involucrados tienen el mismo propietario y si la propiedad db_chainig está activa en ambas bases de datos.

Otro detalle importante es que el contexto de ejecución no ha cambiado mediante EXECUTE AS o similar, lo que invalidaría la cadena de confianza.

Esto significa que no basta con activar la propiedad en una sola base de datos. Además, hay que evitar que el procedimiento se ejecute en un contexto que no sea el del usuario propietario de ambos objetos. Este tipo de situaciones no siempre son evidentes y pueden complicar el diagnóstico cuando un procedimiento aparentemente correcto lanza un error de acceso denegado.

Seguridad frente a conveniencia: ¿cuándo tiene sentido usar db_chainig?

Tal y como ya señalamos en el artículo anterior, activar db_chainig a nivel de instancia es algo que deberíamos evitar en la mayoría de escenarios, sobre todo si trabajamos en un entorno multi-tenant o con bases de datos de aplicaciones distintas.

Sin embargo, si tenemos un entorno controlado, donde todas las bases de datos son desplegadas por nosotros, los objetos comparten propietario y los accesos están bien encapsulados, db_chainig puede ser una herramienta útil para simplificar la gestión de permisos y mantener el modelo de mínima exposición.

En ese contexto, lo importante es no relajarse: hay que documentar cada caso en el que se activa, asegurarse de que no hay EXECUTE AS, revisar periódicamente los propietarios de los objetos y evitar cualquier modificación que pueda romper la cadena de confianza sin darnos cuenta.

Alternativas a db_chaining: firma con certificados

Para aquellos entornos donde la seguridad es prioritaria, pero aún así necesitamos encapsular accesos entre bases de datos, lo más recomendable es firmar los procedimientos almacenados con certificados. Esta técnica, algo más compleja de implementar, permite simular una elevación de permisos perfectamente controlada y auditable, sin necesidad de alterar propiedades globales ni confiar en la coherencia del dbo.

Si todavía no has trabajado con este enfoque, te recomiendo investigar cómo funciona la creación de certificados, usuarios basados en certificado y firma de procedimientos. Puedes consultar el artículo que publiqué sobre el tema la semana pasada.

Conclusión

Con esta demo he querido complementar el análisis previo sobre db_chainig, esta vez desde un enfoque más práctico. El comportamiento por defecto protege nuestras bases de datos, pero hay casos reales donde su activación puede facilitar una arquitectura limpia y modular.

Eso sí, hay que entender muy bien sus implicaciones, probarlo a fondo y asegurarnos de que el entorno cumple con los requisitos de seguridad y propiedad para que el encadenamiento funcione sin sorpresas. Y si no queremos depender de esta propiedad, las firmas con certificados siguen siendo la alternativa más elegante y segura.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 2 comentarios

Tablas temporales vs. variables de tabla en SQL Server: diferencias que sí importan

Las tablas temporales y las variables de tabla en SQL Server son como los destornilladores y las llaves inglesas de nuestra caja de herramientas: parecidas en concepto, pero con propósitos, comportamientos y peculiaridades bien distintos. Ambos mecanismos nos permiten trabajar con conjuntos de datos intermedios sin necesidad de recurrir a tablas permanentes, pero quien haya intentado usarlos indistintamente en entornos reales sabe que las diferencias importan. Y mucho.

Vamos a destripar ambas opciones con calma, ver cómo se comportan, qué ventajas ofrecen y en qué situaciones conviene (o no) usarlas. Porque sí, aunque parezcan intercambiables, tratarlas como tal suele acabar en consultas lentas, planes de ejecución misteriosos y DBA rezando en voz baja.

Tablas temporales: las inquilinas del tempdb

Las tablas temporales (#TablasTemporales) son lo más parecido a una tabla normal que podemos crear en tiempo de ejecución sin dejar huella permanente. Se crean en la base de datos tempdb, y existen mientras dure la sesión (o el scope) que las creó. Podemos definir índices, claves primarias, restricciones, estadísticas… Vamos, que son tablas de verdad, aunque vivan en alquiler. Su sintaxis es familiar y directa:

O incluso más común aún:

El plan de ejecución que las acompaña suele ser robusto, especialmente si trabajamos con cantidades de datos considerables. SQL Server genera estadísticas automáticas sobre las columnas, lo cual permite un plan más ajustado al volumen real. Esto, que parece un detalle técnico sin importancia, marca la diferencia entre una consulta que vuela y otra que arrastra los pies como yo un lunes por la mañana antes del primer café.

¿Inconvenientes? Algunos. El uso de tempdb implica competencia con otros procesos que también están abusando del mismo recurso. Además, su ciclo de vida depende del contexto: si las creamos en un procedimiento almacenado y lo llamamos varias veces, conviene borrarlas explícitamente con DROP TABLE o usar IF OBJECT_ID(…) IS NOT NULL.

Y por supuesto, cuidado con el scope: una tabla temporal creada dentro de un procedimiento no es accesible desde fuera. Pero eso no debería sorprendernos. Tampoco esperamos que un DECLARE de una variable sobreviva al final del procedimiento.

Variables de tabla: pequeñas, rápidas… y caprichosas

Las variables de tabla (@VariablesDeTabla) se introdujeron como una forma rápida y elegante de manejar pequeños conjuntos de datos sin el overhead de una tabla temporal completa. Son ideales para almacenar unas cuantas filas, iterar lógicamente sobre ellas o devolver resultados simples.

La sintaxis es limpia:

Y su ciclo de vida es exactamente el del bloque donde se declaran. No hay que preocuparse por borrarlas ni por interferencias externas. Hasta aquí todo bien.

Ahora viene el problema: SQL Server no genera estadísticas sobre variables de tabla. Nunca. Ni en 2008 ni en 2022. Esto significa que el optimizador trabaja a ciegas. Literalmente: asume que una variable de tabla tiene una única fila. Da igual si tiene 1 o 10.000. El plan de ejecución será el de una tabla de una fila. Y eso, amigos, rara vez termina bien.

¿Hay excepciones? Desde SQL Server 2019, con OPTION (RECOMPILE), el optimizador puede estimar el número real de filas en algunos casos. Pero es una tirita en una fractura abierta. A veces ayuda, otras no. Y seguir usándolas a ciegas es una receta para la frustración.

¿Entonces son inútiles? No, ni mucho menos. Funcionan de maravilla cuando el número de filas es pequeño (menos de 100 suele ser seguro) y cuando las operaciones son simples. Pero si metemos un JOIN, un GROUP BY o empezamos a empujar lógica compleja… mejor sacar la artillería de verdad: tabla temporal.

Tablas temorales vs Variables de tabla: lo que no te dice la documentación

Hablemos claro. Las diferencias no están solo en la sintaxis o el ámbito. Lo importante es cómo se comportan bajo carga, cómo afectan al plan de ejecución y qué tipo de mantenimiento requieren. Para compararlas vamos a ver uno a uno los aspectos más interesantes.

Estadísticas

Las tablas temporales sí generan estadísticas; las variables de tabla no. Esto significa que las temporales permiten planes de ejecución más óptimos en escenarios con muchos datos. Las variables, no.

Soporte de índices

Ambas opciones permiten claves primarias y restricciones únicas. Desde SQL Server 2014 es posible definir índices secundarios en variables de tabla, pero solo dentro de la declaración y con sintaxis limitada. En tablas temporales podemos crear cualquier tipo de índice, incluidos los columnstore, sin restricciones adicionales.

Transacciones

Las variables de tabla no se ven afectadas por ROLLBACK. Si algo falla, su contenido sigue ahí, lo cual puede ser bueno… o un bug encubierto. Las tablas temporales, en cambio, participan en las transacciones como cualquier otra tabla.

Almacenamiento y persistencia

Ambas opciones viven en tempdb, aunque las variables lo hagan de forma menos visible. Pero a nivel físico, no hay magia: no están «en memoria», como algunos aún creen. Eso sí, las temporales suelen dejar más rastro en el sistema de archivos si no se gestionan bien.

Además, como ya hemos comentado las tablas temporales duran lo que dura la sesión, es decir, mientras no las borremos o cerremos esa sesión seguirán ahí. Las variables tipo tabla, por el contrario duran lo que dura la ejecución del lote (batch).

Si veis en la imagen, la segunda consulta no encuentra nada. Esto pasa porque después del “GO” ya se considera otro lote.

Paralelismo

Las tablas temporales pueden beneficiarse del paralelismo en las consultas; las variables de tabla, salvo casos contados y versiones muy recientes de SQL Server, no.

Lectura y escritura

En escenarios de alto volumen, las operaciones sobre variables de tabla pueden ser considerablemente más lentas que sobre tablas temporales. Aunque el coste del DECLARE parezca nulo, el impacto acumulado en los planes de ejecución mal optimizados se paga caro.

Casos de uso, ¿Cuándo elegir tablas temporales o variables de tabla?

Una variable de tabla nos viene de perlas cuando queremos devolver una pequeña tabla desde una función, cuando estamos en mitad de un script complejo que necesita guardar una docena de valores intermedios, o cuando buscamos claridad sin sacrificar rendimiento (porque sabemos que los datos son pocos y controlados). Por norma general, no deberíamos usarlas para más de 100 registros.

Una tabla temporal brilla en todo lo demás: cargas intermedias, transformaciones complejas, conjuntos de datos que van a vivir varias etapas, o cuando necesitamos analizar y refinar el rendimiento de una consulta. Incluso para esas subconsultas que usamos varias veces en una misma consulta y pueden llegar a ser pesadas.

También conviene recordar que hay un tercer actor en esta historia: las tablas temporales globales (##TablaGlobal) y las tablas de memoria (MEMORY_OPTIMIZED). Pero eso ya es otro capítulo. O varios.

Conclusión

Elegir entre una tabla temporal y una variable de tabla no debería depender del estado de ánimo, sino del uso que le vamos a dar. Si los datos son escasos, la lógica es sencilla y no necesitamos estadísticas ni índices complejos, la variable funciona. Pero si hay que unirse a otras tablas, mover volumen o exprimir rendimiento, la tabla temporal es la opción profesional.

No olvidemos que el optimizador de SQL Server toma decisiones basadas en lo que sabe. Y con una variable de tabla, lo que sabe es poco. Si le damos una tabla temporal bien definida, con índices y estadísticas, puede hacer su trabajo. Si le damos una caja cerrada con un «ya te apañarás», no esperemos milagros.

Así que, la próxima vez que tengamos que elegir entre DECLARE @Tabla y CREATE TABLE #Temp, pensemos dos veces. Porque sí, ambas pueden almacenar datos. Pero sólo una de ellas está preparada para aguantar una jornada completa sin pedir un café doble a mitad de camino. Y no, no es la variable.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, Rendimiento, SQL Server, 1 comentario

Procedimientos almacenados para auditoría en SQL Server; Cuando el «sálvese quien pueda» no es estrategia

Llevamos años insistiendo en la importancia de tener auditoría en los sistemas. Y no, no hablamos de perseguir al becario por haber hecho un DELETE sin WHERE. Hablamos de una estrategia seria, sostenible y bien diseñada para dejar constancia de qué ocurre dentro de nuestras bases de datos. Y aquí es donde los procedimientos almacenados (sí, esos viejos conocidos de mil batallas) pueden jugar un papel muy digno si sabemos usarlos con cabeza.

Porque seamos honestos: muchos sistemas de auditoría empiezan con buenas intenciones y terminan convertidos en un Frankenstein lleno de triggers, jobs ocultos y tablas que nadie se atreve a truncar. Pero no tiene por qué ser así. Vamos a hablar de cómo diseñar procedimientos almacenados que nos permitan registrar operaciones relevantes, mantener la trazabilidad sin comprometer el rendimiento, y, sobre todo, que no nos obliguen a pedir perdón cada vez que ejecutamos un SELECT contra la tabla de auditoría.

¿Por qué procedimientos almacenados y no triggers o Extended Events?

Antes de lanzarnos a picar código como si no hubiera mañana, pongamos un poco de contexto. Sí, podríamos usar triggers para capturar operaciones DML. Sí, Extended Events son una maravilla para ciertas trazas. Pero aquí venimos a hablar de soluciones controladas, personalizables y que se integren de forma limpia con nuestras operaciones diarias.

Los procedimientos almacenados nos permiten centralizar la lógica de inserción, modificación y eliminación, y al mismo tiempo registrar en una tabla de auditoría todo lo que nos interesa: quién, cuándo, qué y desde dónde. Si el acceso a los datos está canalizado a través de SPs, tenemos el terreno perfecto para auditar sin traumas. ¿Que no todo pasa por SPs? Entonces el problema no es la auditoría, es el diseño general de la aplicación. Y eso es otra guerra.

Diseño de la tabla de auditoría: no empecemos la casa por el tejado

Antes de ponernos a escribir procedimientos, necesitamos tener clara nuestra tabla de auditoría. No, no vale con un INSERTED tirado en una tabla con columnas tipo Campo1, Campo2, y una nota que diga “aquí va lo que sea”. Una buena tabla de auditoría tiene estructura, contexto y propósito. Una estructura mínima (y funcional) podría ser algo así:

Y aquí hay que hacer una pausa. Porque sí, el campo Datos contiene JSON, pero no es de tipo JSON. ¿Por qué? Porque SQL Server, hasta la versión 2022, no soporta el tipo JSON nativo en entornos on-premises. En Azure SQL Database ya existe, y todo apunta a que SQL Server 2025 lo incorporará. Pero mientras tanto, usamos NVARCHAR(MAX).

Y sí, uso JSON. No, no me he pasado al NoSQL. Pero si queremos registrar los datos afectados sin necesidad de 50 columnas por cada tabla, el JSON nos da flexibilidad. 

No porque nos guste —aunque a muchos sí—, sino porque es lo que hay. Y además, no nos engañemos, el JSON nos da lo que necesitamos: almacenar estructuras flexibles y trabajar con ellas fácilmente gracias a funciones como JSON_VALUE, OPENJSON o ISJSON(). En resumen: guardamos JSON como texto, pero con cabeza.

Procedimientos almacenados con lógica de auditoría: así sí

Ahora que tenemos clara la tabla, vamos a por la lógica. El patrón es simple: cada procedimiento almacenado que realice operaciones sobre los datos deberá incluir una llamada al procedimiento de auditoría, justo después de la operación. Imaginemos un SP de actualización de empleados. Este es un ejemplo limpio y funcional, luego vosotros lo complicáis lo que queráis.

Y el procedimiento InsertarAuditoria, que sigue siendo igual de simple:

Nada de magia negra, solo lógica clara, controlada y auditable en sí misma.

Cuándo montar auditoría (y cuándo no): porque registrar cada pestañeo no es auditar

No todo merece una entrada en la auditoría. Si registramos cada acceso, cada SELECT o cada ejecución de SP, acabaremos con una tabla de 2 TB que nadie se atreve a consultar y que ralentiza cada INSERT. Hay que auditar cambios de estado relevantes: inserciones, modificaciones, eliminaciones y, dependiendo del negocio, consultas que tengan implicaciones legales o de privacidad. Auditar todo “por si acaso” es tan útil como llevar paraguas en el desierto. Y casi igual de cómodo.

Versionado de datos vs auditoría: primos lejanos

Una confusión habitual es pensar que la auditoría y el versionado son lo mismo. Spoiler: no lo son. Auditar es dejar constancia de qué ocurrió, quién lo hizo y cómo. Versionar es guardar el estado completo anterior de un registro, por si hay que volver atrás o comparar. Podemos combinar ambos enfoques, pero no deberíamos mezclarlos como si fueran equivalentes.

Para el versionado tenemos técnicas distintas como el libro de contabilidad, o incluso estructuras propias si el estándar se nos queda corto. Si lo que queremos es saber quién cambió qué, la auditoría nos basta. Si queremos saber qué valores tenía antes, entonces toca sacar la artillería del versionado.

Auditoría y rendimiento: ese delicado equilibrio

Registrar operaciones añade carga. No importa lo minimalista que sea el SP de auditoría: es una operación más por cada modificación. Si nuestra base de datos gestiona 10.000 cambios por minuto, quizás tengamos que plantear estrategias adicionales, como colas (Service Broker, por ejemplo) o particionado de la tabla de auditoría.

Y no, no vale con poner la tabla en otra base de datos para “que no moleste”. Separar el almacenamiento está bien, pero si la transacción espera a que el log de la otra base escriba… seguimos teniendo cuello de botella.

Diseñar una auditoría eficiente es como diseñar un buen índice: no basta con saber que existe, hay que entender cómo afecta a todo lo demás.

Conclusión

Los procedimientos almacenados siguen siendo una herramienta poderosa para implementar auditorías en SQL Server, especialmente cuando tenemos control sobre cómo accede la aplicación a los datos. Nos permiten registrar cambios con contexto, mantener el rendimiento bajo control y evitar la opacidad de soluciones como triggers masivos o rastreos indiscriminados.

Y sí, usamos JSON. Aunque hoy lo almacenamos como NVARCHAR(MAX), es una decisión consciente, no un apaño. Cuando SQL Server soporte el tipo JSON nativamente, y lo hará porque ya lo hace en Azure, estaremos listos para aprovecharlo sin rehacer nada.

Así que no, no necesitas un monstruo de triggers ni una solución de terceros para saber qué está pasando en tu sistema. A veces, basta con un poco de disciplina, procedimientos bien pensados… y la firme convicción de que auditar no es opcional. Es supervivencia.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios

Firmar procedimientos almacenados con certificados

Una política de permisos mínimos para usuarios (y roles) es fundamental para la seguridad de nuestros datos. Sin embargo, a menudo nos enfrentamos al dilema de cómo hacerlo sin sacrificar funcionalidad ni exponer en exceso nuestros objetos. Como decía, la necesidad de implementar un principio de privilegios mínimos es esencial en entornos seguros y controlados, especialmente en bases de datos con múltiples aplicaciones o equipos interactuando. Una técnica muy potente y poco utilizada en SQL Server es la firma de procedimientos almacenados con certificados. Hoy vamos a explorar cómo funciona y por qué puede marcar una diferencia importante en nuestras estrategias de seguridad.

Delegación segura con certificados

Cuando se habla de conceder permisos a través de procedimientos almacenados, una de las primeras aproximaciones suele ser el uso de EXECUTE AS. Aunque funcional, esta técnica presenta inconvenientes, especialmente cuando entra en juego el “chaining” de ejecución o cuando queremos evitar tener que gestionar contextos de ejecución elevados.

Ahí es donde entran los certificados. Firmar procedimientos almacenados nos permite encapsular permisos de forma precisa y segura sin cambiar el contexto de ejecución, sin depender de impersonaciones y sin debilitar el modelo de seguridad de la base de datos.

¿Cómo funciona la firma con certificados?

El procedimiento se basa en tres pasos fundamentales: creación del certificado, firma del procedimiento y asignación del permiso requerido a un login asociado al certificado. Cada uno de estos pasos es determinante para conseguir una delegación limpia y controlada.

Partimos de una situación donde un procedimiento necesita acceder a un objeto (por ejemplo, una tabla o vista), pero el usuario que lo ejecuta no tiene permiso directo sobre ese objeto. Esto normalmente no sería un problema salvo que el procedimiento y el objeto sean de un propietario distinto o que la consulta desde el procedimiento se ejecute en otro contexto de seguridad diferente, por ejemplo por usar sp_executesql. En estos casos, si firmamos el procedimiento con un certificado y asociamos el certificado a un login o usuario que sí tenga ese permiso, el procedimiento se ejecutará con esos privilegios, pero sin necesidad de cambiar el contexto de ejecución del usuario.

Caso práctico detallado

Supongamos que queremos que ciertos usuarios puedan ejecutar un procedimiento llamado usp_ObtenerVentasPrivadas, que accede a una tabla sensible como VentasPrivadas, pero sin darles acceso directo a dicha tabla. El procedimiento sería como el siguiente:

Tal como está, cualquier usuario que tenga permiso de ejecución sobre usp_ObtenerVentasPrivadas podrá acceder a VentasPrivadas, pero sin tener acceso directo a ella ni posibilidad de usarla fuera de este procedimiento. Esto es porque el contexto de ejecución sigue siendo el del usuario original al no tener código dinámico y al pertenecer la tabla y el procedimiento al mismo esquema (con el mismo owner del esquema).

Cuando la cosa se complica

Pero, ¿qué pasa si cambiamos el contexto de ejecución o tenemos distinto owner? Aquí es donde entran en juego los certificados. 

Yo para la demo que os pongo lo he simplificado y ejecuto una consulta simple pero, pongamos que tenemos un procedimiento que genera SQL dinámico y lo ejecuta con sp_executesql. Este sería el ejemplo que podeis reproducir vosotros mismos.

Como veis, si probamos a ejecutar el procedimiento almacenado con el usuario limitado va a dar error porque no tiene permisos sobre la tabla. Sin embargo, cuando creamos un certificado, lo asociamos a un usuario que tiene permisos sobre la tabla y firmamos el procedimiento con ese usuario ya podemos ejecutar sin error el procedimiento con el usuario limitado.

Ventajas clave de usar certificados

Una de las principales ventajas frente a EXECUTE AS es que el certificado no interfiere con el contexto de ejecución. Esto significa que si dentro del procedimiento hay llamadas a otros objetos que usan permisos del usuario original, todo seguirá funcionando correctamente. Además, los certificados son inmunes a problemas comunes como la pérdida de contexto entre bases de datos, lo que resulta útil en entornos distribuidos.

Otra ventaja es la auditabilidad. Como los permisos no se conceden directamente a los usuarios finales, sino que se encapsulan dentro de procedimientos firmados, es más sencillo identificar los puntos de entrada permitidos y realizar auditorías.

También se evita el problema clásico de los permisos residuales. Si un usuario necesita ejecutar varios procedimientos que requieren distintos permisos, no es necesario concederle un permiso amplio ni crear roles intermedios complejos. Firmamos cada procedimiento con los permisos justos que requiere, y el acceso queda perfectamente delimitado.

Consideraciones de los certificados a tener en cuenta

La firma de procedimientos con certificados no está exenta de ciertas limitaciones. Para empezar, no podemos firmar procedimientos encriptados ni CLR. Además, si un procedimiento se modifica, se pierde la firma y hay que volver a aplicarla.

En cuanto a la gestión de certificados, conviene centralizar su creación y almacenamiento de forma segura. El uso de contraseñas fuertes y una política clara de mantenimiento y renovación de certificados es fundamental para evitar riesgos.

Por último, es importante evitar el uso excesivo de esta técnica, especialmente si se convierte en la única forma de delegación. En bases de datos muy grandes o con cientos de procedimientos, puede ser más conveniente crear roles bien definidos y controlar el acceso de forma más clásica. La clave está en encontrar el equilibrio adecuado.

Comportamiento de DENY

Otra consideración muy importante que debemos tener en cuenta es que el uso de DENY no puede ser sobrepasado por una firma de certificado. Si el usuario que ejecuta el procedimiento tiene un DENY explícito sobre el objeto que se consulta dentro del procedimiento, nada podrá permitirle el acceso, por mucho que el certificado tenga ese GRANT.

Esto tiene dos consecuencias prácticas. La primera es que si queremos controlar el acceso mediante firmas, no debemos usar DENY. Sencillamente basta con no conceder permisos. Por otro lado, si existe un DENY, servirá como veto absoluto, incluso para accesos indirectos a través de procedimientos firmados.

Conclusión

Firmar procedimientos almacenados con certificados es una herramienta extremadamente útil y robusta para aplicar el principio de privilegios mínimos de forma segura y mantenible. Nos permite conceder permisos de forma encapsulada, sin necesidad de impersonaciones ni contextos elevados, y facilita una delegación precisa del acceso.

Es una técnica que deberíamos tener siempre en nuestro arsenal cuando diseñamos la arquitectura de seguridad de una base de datos SQL Server. En combinación con otras prácticas como el uso de roles, vistas seguras y auditoría de permisos, puede contribuir a sistemas mucho más sólidos, trazables y mantenibles.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios

Contextos de seguridad en SQL dinámico: permisos, procedimientos y sp_executesql

En mi artículo anterior os hablé de cómo construir SQL dinámico de forma segura usando sp_executesql, y cómo evitar riesgos como el SQL Injection. Sin embargo, hay un aspecto igual de crítico que no se suele tener en cuenta: el contexto de seguridad desde el que se ejecuta el código dinámico y su impacto en los permisos.

Muchos desarrolladores se sorprenden cuando un procedimiento almacenado que funciona con SQL «normal» deja de funcionar al pasar a SQL dinámico, a pesar de tener los mismos permisos. El motivo está en cómo SQL Server maneja los permisos de ejecución implícitos y qué ocurre cuando usamos sp_executesql. En este artículo voy a tratar de explicarlo paso a paso.

Permisos implícitos al ejecutar un procedimiento almacenado

Cuando concedemos a un usuario permiso para ejecutar un procedimiento almacenado, por ejemplo:

Ese usuario puede ejecutar el procedimiento sin necesidad de tener permisos directos sobre las tablas internas que use dicho procedimiento. Es decir, aunque no tenga SELECT sobre Sales.SalesOrderHeader, si el procedimiento ejecuta esta consulta:

… el usuario podrá obtener los datos. Esto ocurre porque el contexto de ejecución del procedimiento es el del propietario, y si el procedimiento y las tablas tienen el mismo dueño, SQL Server permite ese acceso mediante el mecanismo de ownership chaining (encadenamiento de propiedad). Este comportamiento es clave para encapsular la lógica de negocio sin exponer directamente las tablas subyacentes.

¿Qué ocurre con los permisos cuando usamos SQL dinámico?

Aquí viene la trampa. Si dentro del procedimiento usamos SQL dinámico con sp_executesql como:

… entonces se rompe la cadena de propiedad, y SQL Server evalúa los permisos como si el usuario estuviera ejecutando directamente la consulta, no como si formara parte del procedimiento. En otras palabras: aunque el usuario tenga permiso de ejecución sobre el procedimiento, necesitará permisos explícitos de SELECT sobre la tabla referenciada en el SQL dinámico.

Esto puede llevar a errores difíciles de diagnosticar si no se comprende cómo funciona el contexto de seguridad.

Demostración del problema de permisos

Imaginemos este escenario:

  • Usuario app_user
  • Procedimiento dbo.usp_Informe
  • Tabla Sales.SalesOrderHeader
  • Escenario app_user tiene EXECUTE sobre usp_Informe, pero no SELECT sobre la tabla
  • Código del procedimiento:

El primer SELECT se ejecuta correctamente gracias al ownership chaining. El segundo da error:

Cuándo se rompe y cuándo no

Para que se mantenga el ownership chaining, se deben cumplir dos condiciones:

  • Los objetos deben pertenecer al mismo propietario (normalmente dbo)
  • La consulta no debe usar SQL dinámico
  • En cuanto usamos EXEC, sp_executesql o EXECUTE AS con un contexto diferente, se interrumpe esa cadena y SQL Server valida los permisos del usuario directamente.

¿Cómo se soluciona el problema de permisos?

Existen varias estrategias según el contexto, pero las más comunes son estas:

1. Firmar el procedimiento con un certificado

Es la solución más profesional. Se firma el procedimiento con un certificado que tenga los permisos necesarios sobre las tablas, y el usuario ejecuta el procedimiento sin tener permisos directos. Requiere más trabajo, pero es la solución más segura y escalable, especialmente en entornos regulados o críticos.

2. Conceder permisos explícitos sobre las tablas

Es la opción más directa pero rompe el aislamiento que buscamos al encapsular la lógica dentro de procedimientos. Puede ser válido en entornos internos o controlados.

Pero en ese caso ya no protegemos las tablas detrás de la lógica del procedimiento.

3. Evitar usar SQL dinámico innecesariamente

Si el acceso a los datos no requiere construir partes dinámicas (columnas, nombres de tabla, filtros condicionales avanzados), es mejor mantener el SQL como texto plano. Así se conserva el contexto de permisos del procedimiento.

En ocasiones, usamos SQL dinámico cuando lo que realmente necesitamos es ejecutar una u otra consulta en función de un parámetro de entrada del procedimiento. En estos casos es mejor crear una lógica con IF que códigos dinámicos. 

Por ejemplo, este procedimiento con código SQL dinámico:

… lo podríamos sustituir por este:

Conclusión

El uso de SQL dinámico no es solo una cuestión de sintaxis o seguridad frente a inyecciones. También tiene implicaciones directas en el modelo de permisos y seguridad de SQL Server. Es importante entender que al usar sp_executesql, el procedimiento pierde la protección que le daba la cadena de propiedad, y el motor evalúa los permisos como si se tratase de una ejecución independiente. Este comportamiento puede ser confuso si no se conoce, pero una vez lo interiorizamos, se convierte en una herramienta poderosa para diseñar arquitecturas seguras y mantenibles. Si estamos diseñando procedimientos que deben proteger las tablas subyacentes, debemos considerar firmar con certificados, controlar cuidadosamente los permisos, o bien evitar SQL dinámico cuando sea posible.  

En el artículo anterior explicamos cómo generar SQL dinámico de forma segura desde el punto de vista sintáctico y de rendimiento. Pero ahora, sabemos que también hay que hacerlo de forma segura desde el punto de vista del contexto de ejecución.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios

Código dinámico con seguridad en SQL Server

En muchas ocasiones nos enfrentamos a escenarios donde necesitamos construir sentencias SQL de forma dinámica. Ya sea para crear filtros condicionales, construir cláusulas ORDER BY en tiempo de ejecución o ejecutar consultas sobre distintos objetos, la generación de SQL dinámico parece una solución sencilla y flexible. Pero esta potencia viene acompañada de riesgos, especialmente desde el punto de vista de la seguridad.

A lo largo de los años, he visto cómo el uso descuidado del SQL dinámico ha sido uno de los vectores de ataques de inyección SQL (SQLi) más comunes, y aún sigue siéndolo. Por eso, en este artículo vamos a repasar cómo generar SQL dinámico en SQL Server de forma segura, analizando técnicas recomendadas y errores frecuentes, con ejemplos claros y aplicables.

¿Cuándo usamos SQL dinámico?

Los escenarios más frecuentes en los que aparece la necesidad de SQL dinámico suelen estar relacionados con filtros condicionales, búsquedas avanzadas, generación de informes personalizables, lógica multi-tenant o incluso mantenimiento automatizado.

Un ejemplo muy habitual es una búsqueda con varios filtros opcionales. Supongamos una aplicación que consulta una tabla de personas donde el usuario puede buscar por nombre, ciudad y país. Si tratamos de resolver esto con un procedimiento estándar, el número de combinaciones posibles puede crecer exponencialmente. El SQL dinámico permite construir la sentencia ajustada a los filtros que el usuario haya proporcionado.

El problema del SQL Injection

El gran riesgo del SQL dinámico mal implementado es el conocido SQL Injection, una técnica con la que un atacante puede alterar la consulta ejecutada para acceder o modificar datos sin autorización. Esto ocurre cuando concatenamos directamente valores dentro de la cadena SQL. Veamos un ejemplo inseguro:

Si @city proviene de un parámetro externo (una app, una web), el usuario podría inyectar algo como: “Madrid’; DROP TABLE Person.Person;” y provocar un desastre.

Este patrón, por desgracia, sigue viéndose demasiado a menudo en aplicaciones heredadas o mal diseñadas.

Uso seguro de SQL dinámico con sp_executesql

La solución más eficaz ante este problema es usar sp_executesql, que permite construir consultas dinámicas pero separando el código de los datos mediante parámetros tipados. Esto bloquea cualquier intento de inyección porque el valor del parámetro no se interpreta como código. Reescribamos el ejemplo anterior de forma segura:

Aquí, aunque el usuario intentase inyectar código en @city, no lo conseguiría. SQL Server lo tratará como un valor, no como una parte de la instrucción.

Además, esta técnica también permite la reutilización de planes de ejecución, lo que supone una ventaja adicional en términos de rendimiento.

Código dinámico con filtros condicionales

Un paso más allá es cuando necesitamos construir dinámicamente múltiples filtros. En estos casos, lo ideal es ir concatenando las condiciones SQL pero parametrizando todos los valores. Veamos un ejemplo más completo:

De este modo, solo se añaden los filtros que tengan valor, pero todos los valores siguen protegidos mediante parámetros.

Identificadores dinámicos: el caso más delicado

sp_executesql no permite parametrizar nombres de columnas o tablas. Esto es especialmente importante si necesitamos cambiar el objeto sobre el que se ejecuta la consulta. En estos casos debemos concatenar el identificador, pero asegurándonos de que el valor es válido. La función QUOTENAME es clave para evitar inyecciones sobre identificadores ya que introduce el nombre del objeto entre corchetes [].

Aquí estamos asumiendo que el nombre de la tabla ha sido validado previamente. Aún así, QUOTENAME evita que un valor como SalesOrderHeader; DROP TABLE x;– pueda hacer daño. 

En entornos multi-tenant esto es especialmente útil si cada cliente tiene su propia tabla (modelo database-per-tenant) y accedemos a ellas dinámicamente.

Consejos adicionales para SQL dinámico seguro

Cuando usamos SQL dinámico en entornos críticos o expuestos a usuarios externos, es fundamental aplicar otras prácticas complementarias como encapsular en procedimientos almacenados ya que así reducimos la exposición del motor y permitimos auditar más fácilmente. Otra buena práctica es registrar las consultas generadas, esto es especialmente útil para soporte, auditoría y detección de patrones de abuso. Otro paso obligatorio, para mi es evitar privilegios excesivos; el usuario que ejecuta el código dinámico no debe tener más permisos de los necesarios.

Por último, si queremos ir un paso más allá, podemos aplicar SET FMTONLY OFF y otras opciones de seguridad sobre todo si trabajamos con herramientas de terceros. De esta manera podremos asegurarnos de que el motor de base de datos ejecuta todo el bloque tal cual, sin modificar el flujo por culpa del modo de metadatos.

Conclusión

El SQL dinámico en SQL Server puede ser tan útil como peligroso. En nuestras manos está la diferencia entre construir una solución flexible y robusta o abrir una puerta trasera a posibles ataques.

La clave está en nunca concatenar valores directamente y utilizar sp_executesql con parámetros siempre que sea posible. Cuando se trabaja con nombres de objetos, debemos validar y proteger con QUOTENAME. Y si el contexto lo permite, encapsular toda la lógica dentro de procedimientos controlados.

Estas técnicas yo las aplico habitualmente en proyectos reales, y son parte esencial de una arquitectura segura, especialmente en entornos donde la escalabilidad o la “multi-tenencia” requieren cierta flexibilidad a nivel de metadatos. 

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 1 comentario

Orden de los registros en SQL Server cuando no usamos ORDER BY

Cuando trabajamos con SQL Server, ya seamos DBAs, analistas o desarrolladores, a menudo nos topamos con una situación que puede parecer trivial, pero que esconde una trampa para quienes no conocen el funcionamiento interno del motor de SQL Server: ¿cuál es el orden de los resultados cuando no se especifica explícitamente un ORDER BY? El otro día me lo preguntó un compañero y quería también compartirlo con vosotros. La respuesta, aunque sencilla, tiene implicaciones importantes tanto para la lógica de negocio como para el rendimiento y la consistencia de nuestras aplicaciones.

El mito del orden “natural” en SQL Server

Todos hemos oído alguna vez frases como “esta tabla siempre me devuelve los datos en orden de inserción” o “los resultados salen ordenados por la clave primaria aunque no lo indique”. Y aunque en muchas ocasiones estas afirmaciones parecen cumplirse, lo cierto es que confiar en un orden implícito es una práctica peligrosa que puede romperse en cualquier momento. SQL Server, por diseño, no garantiza ningún tipo de orden si no se especifica expresamente una cláusula ORDER BY.

El optimizador y la aleatoriedad controlada

Esto pasa porque el motor de SQL Server tiene como objetivo principal devolver el resultado correcto de la consulta en el menor tiempo posible. Esto implica que, cuando no hay una indicación explícita de orden, el optimizador tiene plena libertad para usar el plan de ejecución que considere más eficiente, incluso si eso implica devolver los datos en un orden distinto cada vez que se ejecuta la misma consulta.

La forma en que SQL Server accede a los datos —ya sea mediante un table scan, index scan, index seek, lookup o incluso los hash match— influye directamente en el orden en el que los registros son devueltos. Y como estos planes pueden variar en función de las estadísticas, la carga del sistema o incluso la edición de SQL Server, el orden final de los resultados es impredecible.

El rol de los índices: ¿orden oculto?

Es cierto que muchas veces el orden “parece” coincidir con el de un índice, en especial cuando se usa un index scan. Por ejemplo, si tenemos una tabla con una clave primaria basada en un índice clustered, es habitual que un SELECT * FROM Tabla sin ORDER BY devuelva los datos según ese índice clustered. Pero esto no es una garantía, ni una promesa del motor.

Un cambio en el plan de ejecución, una actualización estadística, o una simple alteración en el número de registros puede hacer que SQL Server decida usar otro índice, o incluso hacer un table scan, y romper ese orden «natural».

Casos prácticos: cuándo cambia el orden y por qué

Supongamos una tabla de pedidos con una clave primaria sobre OrderID. Si hacemos:

En la mayoría de las ejecuciones obtendremos los datos ordenados por OrderID. Pero si añadimos un WHERE, un JOIN, un TOP, un GROUP BY, o incluso una clausula INCLUDE en un índice, el plan de ejecución puede variar, y con ello el orden.

En una prueba con AdventureWorks, podréis observar cómo una simple adición de una condición WHERE puede provocar un cambio de plan de ejecución de un Clustered Index Scan a un NonClustered Index Seek, seguido de un Key Lookup, y con ello se alterará el orden de los resultados.

El problema de confiar en el azar

Imaginemos que una aplicación espera que los resultados vengan ordenados por fecha. La visualización puede estar funcionando correctamente durante meses, pero tras una reorganización de índices o una actualización del motor, el plan de ejecución cambia. De pronto, los datos aparecen en orden aparentemente aleatorio. El fallo no está en SQL Server, sino en haber confiado en algo que nunca fue una garantía.

A nivel de lógica de negocio, esta suposición puede tener consecuencias nefastas, especialmente en procesos de paginación, importaciones, exportaciones de datos o cálculos acumulativos.

El caso especial de las funciones TOP sin ORDER BY

Un caso particularmente peligroso es el uso de TOP(n) sin un ORDER BY. Por ejemplo:

¿El primer registro? ¿Según qué criterio? Puede que hoy obtengamos un empleado llamado «Tolomeo», mañana «Herminia» y pasado «Anacleto». El motor devolverá el primero que encuentre según el plan actual, que puede cambiar sin previo aviso. Este es uno de los errores más comunes en desarrollo, y conviene tenerlo siempre presente.

Orden en columnstore, tablas temporales y paralelismo

Cuando trabajamos con índices columnstore, la aleatoriedad del orden aún se amplifica más. Este tipo de almacenamiento columnar está optimizado para escaneos masivos, y el orden de los registros no es algo relevante desde el punto de vista del motor. Además, el uso de paralelismo, buffers intermedios y reordenamientos internos hacen que cada ejecución pueda devolver los datos en un orden distinto. 

En consultas complejas con operaciones UNION, CTE, tablas temporales o incluso operadores spool intermedios, la combinación de resultados puede alterar el orden. Una operación Merge Join, por ejemplo, puede forzar un orden intermedio, pero si se sustituye por un Hash Match debido a un cambio en las estadísticas, ese orden desaparece.

¿Y en Azure SQL?

En el caso de bases de datos en Azure, ya sea en modo SQL Database o en SQL Managed Instance, el comportamiento es exactamente el mismo. La arquitectura del servicio no cambia esta característica. De hecho, dado que el motor puede escalar dinámicamente o balancear cargas, es aún más crítico no confiar en ningún tipo de orden implícito.

Conclusión

Como práctica profesional, debemos asumir que ninguna consulta en SQL Server garantiza un orden si no usamos expresamente la cláusula ORDER BY. Esta regla es especialmente importante cuando diseñamos procedimientos almacenados, informes o integraciones que dependen del orden de los datos.

Aunque el comportamiento “parezca” consistente, no debemos basarnos en lo que ocurre hoy, sino en lo que el motor puede decidir hacer mañana. En nuestras pruebas y validaciones, es conveniente forzar el uso de ORDER BY siempre que sea necesario incluso cuando el conjunto de datos es pequeño, para asegurar que nuestras aplicaciones sean consistentes, mantenibles y previsibles. 

Pero cuidado, como suelo comentar en mis formaciones, el ORDER BY es una operación especialmente costosa, no debemos abusar de ella cuando no sea necesario tener ordenados los datos. Sin embargo, esto no quiere decir que sea solo un adorno estético para los datos, es una parte fundamental de la lógica de las consultas. Sin él, estamos a merced del optimizador, y eso nunca es buena idea.

¿Te gustaría que prepare un vídeo para youtube con pruebas para demostrar visualmente estos comportamientos? Pídemelo y lo montamos.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios