T2.3: Implements Security Features

Knowledge Review - InterSystems ObjectScript Specialist

1. Garantiza el uso apropiado de variables y globals para evitar fugas de seguridad

Puntos Clave

  • Globals privados de proceso (^||name): Visibles solo para el proceso actual; usar para datos sensibles temporales
  • Variables locales: Existen solo en la memoria del proceso actual; nunca se persisten ni se registran en el journal
  • Evitar exponer globals directamente: Nunca pasar nombres de globals al código del lado del cliente ni exponerlos en URLs
  • IRISTEMP para datos transitorios: Usar globals ^IRIS.Temp.* para datos temporales que no deberían registrarse en el journal ni respaldarse
  • Seguridad de namespace: Los globals sensibles deben residir en bases de datos con acceso restringido

Notas Detalladas

Globals privados de proceso para datos sensibles

Los globals privados de proceso (PPGs) son visibles solo para el proceso que los crea. Son ideales para almacenar datos intermedios sensibles como tokens de autenticación, valores descifrados o datos temporales de pacientes durante el procesamiento:

 // Almacenar datos sensibles en un global privado de proceso
 SET ^||Session("authToken") = token
 SET ^||Session("decryptedSSN") = ssn

 // Procesar los datos
 DO ..ProcessPatientData()

 // Limpiar -- aunque los PPGs se limpian automáticamente cuando el proceso termina
 KILL ^||Session

Los PPGs NO se registran en el journal, NO se respaldan y NO son accesibles desde otros procesos. Esto los hace significativamente más seguros que los globals regulares para datos sensibles temporales.

Patrones peligrosos a evitar

 // PELIGROSO: Almacenar credenciales en un global regular
 SET ^Config("dbPassword") = "secret123"    // Persistido, registrado en journal, respaldado, visible para todos los procesos

 // PELIGROSO: Exponer la estructura de globals al cliente
 SET %response.ContentType = "application/json"
 WRITE "{""global"": ""^Patient.Data""}"    // Revela la estructura interna de almacenamiento

 // PELIGROSO: Usar nombre de global desde entrada del usuario
 SET globalName = %request.Get("global")
 SET value = @globalName@(id)               // Indirección con entrada del usuario = riesgo de seguridad

 // SEGURO: Usar una capa de abstracción
 SET value = ##class(MyApp.DataService).GetPatient(id)

Proteger datos de globals con seguridad de base de datos

Configurar seguridad a nivel de base de datos para restringir qué roles pueden leer/escribir globals específicos:

  • Mapear globals sensibles a bases de datos dedicadas
  • Aplicar permisos basados en recursos a esas bases de datos
  • Usar roles de aplicación para controlar el acceso a nivel de namespace
 // Verificar si el usuario actual tiene acceso antes de proceder
 IF '$SYSTEM.Security.Check("%DB_SENSITIVE", "READ") {
     SET sc = $$$ERROR($$$AccessDenied)
     QUIT sc
 }
 SET data = ^SensitiveData(patientId)

Variables locales vs globals para seguridad

Las variables locales existen solo en la memoria del proceso y nunca son visibles para otros procesos, nunca se persisten en disco y nunca se registran en el journal. Úselas para:

  • Contraseñas y credenciales durante la autenticación
  • Datos descifrados durante el procesamiento
  • Resultados de cálculos temporales con contenido sensible

Referencias de Documentación

2. Verifica roles para control de permisos

Puntos Clave

  • $ROLES: Variable especial que contiene una lista delimitada por comas de los roles del proceso actual
  • $SYSTEM.Security.Check(): Verifica programáticamente si el usuario actual posee un privilegio específico sobre un recurso
  • Palabra clave Requires: Anotación a nivel de método que restringe la ejecución a usuarios con recursos/privilegios específicos
  • %Admin_Manage, %DB_*, %Development: Recursos comunes del sistema utilizados en verificaciones de permisos
  • Roles de aplicación: Roles personalizados que agrupan privilegios para control de acceso específico de la aplicación

Notas Detalladas

Verificar roles con $ROLES

$ROLES contiene todos los roles asignados al usuario actual, incluyendo roles asignados y sus sub-roles en cascada:

 // Mostrar los roles del usuario actual
 WRITE "$ROLES = ", $ROLES, !
 // Ejemplo de salida: %All,%Developer,MyApp.Admin,MyApp.User

 // Verificar un rol específico
 IF $LISTFIND($LISTFROMSTRING($ROLES), "MyApp.Admin") {
     WRITE "User is an administrator", !
 }

 // Alternativa: verificación simple de cadena (menos precisa debido a coincidencia de subcadena)
 IF $ROLES [ "MyApp.Admin" {
     WRITE "User has admin role", !
 }

Uso de $SYSTEM.Security.Check()

$SYSTEM.Security.Check() verifica si el usuario actual tiene un permiso específico sobre un recurso nombrado. Este es el enfoque recomendado para verificaciones de autorización programáticas:

 // Verificar si el usuario tiene permiso WRITE sobre un recurso
 IF $SYSTEM.Security.Check("MyApp.PatientData", "WRITE") {
     // El usuario está autorizado para modificar datos de pacientes
     SET ^Patient(id, "Name") = newName
 }
 ELSE {
     SET sc = $$$ERROR($$$AccessDenied)
     QUIT sc
 }

 // Verificar permiso READ
 IF '$SYSTEM.Security.Check("MyApp.Reports", "READ") {
     WRITE "Access denied: insufficient privileges", !
     QUIT
 }

El argumento de permiso puede ser: "READ", "WRITE" o "USE".

Seguridad a nivel de método con Requires

La palabra clave Requires en métodos de clase restringe la ejecución a usuarios que posean el privilegio de recurso especificado:

/// Eliminar un registro de paciente (solo administradores)
ClassMethod DeletePatient(id As %Integer) As %Status [ Requires = "MyApp.Admin:WRITE" ]
{
    SET sc = ##class(MyApp.Patient).%DeleteId(id)
    RETURN sc
}

/// Ver log de auditoría (requiere USE sobre el recurso de auditoría)
ClassMethod ViewAuditLog() As %Status [ Requires = "%Admin_Manage:USE" ]
{
    // Solo usuarios con %Admin_Manage:USE pueden ejecutar este método
    // Se genera un error <PROTECT> si el usuario carece del privilegio requerido
    DO ..DisplayLog()
    RETURN $$$OK
}

Si un usuario sin el privilegio requerido llama a un método con Requires, se genera un error .

Patrón práctico de autorización

ClassMethod ProcessOrder(orderId As %Integer) As %Status
{
    // Verificar que el usuario tiene el rol apropiado
    IF '$SYSTEM.Security.Check("MyApp.Orders", "WRITE") {
        $$$ThrowStatus($$$ERROR($$$AccessDenied))
    }

    // Verificación adicional de rol a nivel de negocio
    IF $ROLES '[ "MyApp.OrderProcessor" {
        RETURN $$$ERROR($$$GeneralError, "Role MyApp.OrderProcessor required")
    }

    // Proceder con el procesamiento del pedido
    TSTART
    TRY {
        // ... lógica de procesamiento ...
        TCOMMIT
    }
    CATCH ex {
        TROLLBACK
        RETURN ex.AsStatus()
    }
    RETURN $$$OK
}

Referencias de Documentación

3. Previene ataques de inyección SQL

Puntos Clave

  • Consultas parametrizadas: Usar marcadores de posición ? con %SQL.Statement para prevenir inyección
  • Variables host en SQL embebido: Usar sintaxis :variable; los valores se vinculan de forma segura, no se concatenan
  • Nunca concatenar entrada del usuario: La concatenación de cadenas en consultas SQL es el vector principal de inyección
  • %SQL.Statement: La interfaz recomendada de SQL dinámico con parametrización integrada
  • Validación de entrada: Defensa en profundidad -- validar y sanitizar la entrada incluso cuando se usan parámetros

Notas Detalladas

El problema de la inyección SQL

La inyección SQL ocurre cuando la entrada proporcionada por el usuario se concatena directamente en una cadena SQL, permitiendo a un atacante alterar la lógica de la consulta:

 // VULNERABLE: Concatenación directa de cadenas
 SET sql = "SELECT * FROM Patient WHERE Name = '" _ userInput _ "'"
 // Si userInput = "' OR 1=1 --", la consulta se convierte en:
 // SELECT * FROM Patient WHERE Name = '' OR 1=1 --'
 // ¡Esto devuelve TODOS los pacientes!

Prevención con %SQL.Statement y parámetros ?

El enfoque correcto usa consultas parametrizadas donde la entrada del usuario se vincula como parámetros, nunca se interpola en la cadena SQL:

 SET stmt = ##class(%SQL.Statement).%New()
 SET sc = stmt.%Prepare("SELECT Name, DOB FROM Patient WHERE PatientId = ?")
 IF $$$ISERR(sc) {
     WRITE "Prepare error: ", $SYSTEM.Status.GetErrorText(sc), !
     QUIT
 }

 // Ejecutar con parámetro -- vinculado de forma segura independientemente del contenido
 SET rs = stmt.%Execute(userInput)
 WHILE rs.%Next() {
     WRITE rs.Name, " - ", rs.DOB, !
 }

Múltiples parámetros:

 SET sc = stmt.%Prepare("SELECT * FROM Patient WHERE State = ? AND Age > ?")
 SET rs = stmt.%Execute(stateInput, ageInput)

SQL embebido seguro con variables host

El SQL embebido utiliza automáticamente vinculación segura a través de variables host (prefijadas con dos puntos):

 // SEGURO: Las variables host se vinculan, no se concatenan
 SET patientName = userInput
 &sql(SELECT DOB, Phone INTO :dob, :phone
      FROM Patient
      WHERE Name = :patientName)
 IF SQLCODE = 0 {
     WRITE "DOB: ", dob, !
 }

Las variables con prefijo de dos puntos (:patientName, :dob, :phone) son tratadas como parámetros vinculados por el motor SQL, haciendo imposible la inyección a través de estos valores.

Nombres dinámicos de tabla/columna

Cuando los nombres de tabla o columna deben ser dinámicos (lo cual los parámetros no pueden manejar), use un enfoque de lista blanca:

 // Validar contra una lista blanca conocida
 SET allowedTables = $LISTBUILD("Patient", "Appointment", "Provider")
 IF '$LISTFIND(allowedTables, tableName) {
     WRITE "Invalid table name", !
     QUIT
 }
 // Ahora es seguro usar en la construcción de la consulta
 SET sql = "SELECT * FROM " _ tableName _ " WHERE Id = ?"
 SET sc = stmt.%Prepare(sql)
 SET rs = stmt.%Execute(idValue)  // idValue aún parametrizado

Defensa en profundidad: validación de entrada

Incluso con consultas parametrizadas, valide la entrada como una capa adicional:

 // Validar formato esperado
 IF patientId '?1.N {
     // No es un ID numérico válido
     WRITE "Invalid patient ID format", !
     QUIT
 }
 SET rs = stmt.%Execute(patientId)

Referencias de Documentación

4. Implementa verificaciones de permisos SQL embebidos

Puntos Clave

  • El SQL embebido NO verifica privilegios SQL: El SQL embebido omite la verificación de privilegios SQL — las aplicaciones deben verificar los privilegios explícitamente
  • %CHECKPRIV: Comando SQL para verificar si el usuario actual tiene un privilegio SQL específico (usar antes de operaciones de SQL embebido)
  • GRANT/REVOKE: Comandos SQL para asignar o revocar privilegios sobre tablas, vistas y procedimientos
  • Privilegios SQL: %ALTER, SELECT, INSERT, UPDATE, DELETE, EXECUTE y REFERENCES sobre objetos específicos
  • SQLCODE -99: Código de error devuelto cuando una operación de SQL Dinámico falla por privilegios insuficientes
  • Herencia de privilegios: A los roles se les pueden otorgar privilegios SQL, y los usuarios heredan privilegios de sus roles

Notas Detalladas

SQL embebido y verificación de privilegios

Importante: El SQL embebido NO realiza verificación de privilegios SQL. La documentación de InterSystems establece: *"Las sentencias de SQL embebido no realizan verificación de privilegios; se asume que las aplicaciones que usan SQL embebido verificarán los privilegios antes de usar sentencias de SQL embebido."*

La verificación de privilegios SQL se aplica a:

  • SQL Dinámico (%SQL.Statement)
  • Conexiones ODBC
  • Conexiones JDBC
  • SQL Shell

El SQL embebido puede acceder a todas las tablas, vistas y columnas y realizar cualquier operación independientemente de las asignaciones de privilegios. Use %CHECKPRIV explícitamente si necesita verificar privilegios en código de SQL embebido.

Verificar privilegios SQL con %CHECKPRIV

Use %CHECKPRIV en SQL embebido para verificar los privilegios de un usuario antes de intentar una operación:

 // Verificar si el usuario actual tiene privilegio SELECT sobre la tabla Patient
 &sql(%CHECKPRIV SELECT ON MyApp.Patient)
 IF SQLCODE = 0 {
     WRITE "User has SELECT privilege on Patient", !
 }
 ELSE {
     WRITE "Access denied to Patient table", !
     QUIT
 }

 // Verificar privilegio INSERT
 &sql(%CHECKPRIV INSERT ON MyApp.Patient)
 IF SQLCODE '= 0 {
     WRITE "Cannot insert: ", %msg, !
     QUIT
 }

Manejo de fallos de privilegios (SQLCODE -99)

Cuando una operación de SQL Dinámico falla por privilegios faltantes, SQLCODE se establece en -99 (esto NO aplica al SQL Embebido, que omite las verificaciones de privilegios):

 &sql(INSERT INTO MyApp.AuditLog (Action, Username, Timestamp)
      VALUES ('LOGIN', :username, CURRENT_TIMESTAMP))
 IF SQLCODE = -99 {
     // Error de privilegio -- manejar correctamente
     WRITE "Insufficient SQL privileges for audit logging", !
     // Registrar a través de un mecanismo alternativo
     DO ##class(MyApp.Logger).Log("WARN", "SQL privilege denied for " _ $USERNAME)
 }
 ELSEIF SQLCODE < 0 {
     WRITE "SQL error: ", SQLCODE, " - ", %msg, !
 }

GRANT y REVOKE en SQL embebido

Gestionar privilegios SQL programáticamente usando GRANT y REVOKE:

 // Otorgar SELECT sobre una tabla a un rol
 &sql(GRANT SELECT ON MyApp.Patient TO MyAppReadOnly)
 IF SQLCODE '= 0 {
     WRITE "Grant failed: ", %msg, !
 }

 // Otorgar múltiples privilegios
 &sql(GRANT SELECT, INSERT, UPDATE ON MyApp.Patient TO MyAppDataEntry)

 // Otorgar EXECUTE sobre un procedimiento almacenado
 &sql(GRANT EXECUTE ON MyApp.CalculateRisk TO MyAppClinician)

 // Revocar un privilegio
 &sql(REVOKE DELETE ON MyApp.Patient FROM MyAppDataEntry)

 // Otorgar con GRANT OPTION (permite al beneficiario otorgar el mismo privilegio a otros)
 &sql(GRANT SELECT ON MyApp.Patient TO MyAppAdmin WITH GRANT OPTION)

Combinar verificaciones de roles con verificaciones de privilegios SQL

Para defensa en profundidad, combine verificaciones de roles de ObjectScript con verificación de privilegios SQL:

ClassMethod UpdatePatientRecord(patientId As %Integer, data As %String) As %Status
{
    // Capa 1: Verificación de rol de ObjectScript
    IF '$SYSTEM.Security.Check("MyApp.PatientData", "WRITE") {
        RETURN $$$ERROR($$$AccessDenied)
    }

    // Capa 2: Verificación de privilegio SQL
    &sql(%CHECKPRIV UPDATE ON MyApp.Patient)
    IF SQLCODE '= 0 {
        RETURN $$$ERROR($$$GeneralError, "SQL UPDATE privilege required")
    }

    // Capa 3: Ejecutar la actualización con manejo de errores apropiado
    &sql(UPDATE MyApp.Patient SET Data = :data WHERE PatientId = :patientId)
    IF SQLCODE < 0 {
        RETURN $$$ERROR($$$GeneralError, "Update failed: " _ %msg)
    }
    IF SQLCODE = 100 {
        RETURN $$$ERROR($$$GeneralError, "Patient not found")
    }

    RETURN $$$OK
}

Privilegios a nivel de columna

Los privilegios SQL pueden otorgarse a nivel de columna para un control de grano fino:

 // Otorgar SELECT solo sobre columnas no sensibles
 &sql(GRANT SELECT (Name, DOB, Phone) ON MyApp.Patient TO MyAppReceptionist)

 // Denegar acceso a columnas sensibles
 &sql(REVOKE SELECT (SSN, DiagnosisCode) ON MyApp.Patient FROM MyAppReceptionist)

Ver privilegios actuales

Consultar los metadatos de seguridad SQL para ver qué privilegios existen:

 &sql(SELECT Grantee, Privilege, TableName
      INTO :grantee, :priv, :tbl
      FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES
      WHERE TableName = 'MyApp.Patient')
 WHILE SQLCODE = 0 {
     WRITE grantee, " has ", priv, " on ", tbl, !
     &sql(FETCH INTO :grantee, :priv, :tbl)
 }

Referencias de Documentación

Resumen de Preparación para el Examen

Conceptos críticos a dominar:

  1. PPGs para datos sensibles: Los globals privados de proceso (^||name) están aislados por proceso, no se registran en el journal, no se respaldan -- ideales para datos sensibles temporales
  2. $SYSTEM.Security.Check(): El método programático principal para verificar permisos a nivel de recurso; acepta nombre de recurso y tipo de permiso (READ/WRITE/USE)
  3. $ROLES: Contiene una lista delimitada por comas de los roles del usuario actual; usar $LISTFIND para verificación precisa en lugar del operador contiene ([)
  4. Consultas parametrizadas: Siempre usar ? con %SQL.Statement o :variable con SQL embebido; nunca concatenar entrada del usuario en cadenas SQL
  5. %CHECKPRIV: Comando de SQL embebido para verificar privilegios SQL antes de ejecutar operaciones
  6. SQLCODE -99: El código de error específico para violaciones de privilegios en operaciones SQL

Escenarios comunes de examen:

  • Identificar código vulnerable que concatena entrada del usuario en consultas SQL
  • Elegir entre variables locales, PPGs y globals regulares para almacenar datos sensibles
  • Escribir llamadas a $SYSTEM.Security.Check() con el nombre de recurso y tipo de permiso correctos
  • Corregir patrones de código inseguros (indirección de globals con entrada del usuario, nombres de globals expuestos)
  • Implementar sentencias GRANT/REVOKE para acceso SQL basado en roles
  • Determinar la ubicación correcta para verificaciones de autorización (nivel de método con Requires vs programático)
  • Reconocer SQLCODE -99 e implementar manejo de errores apropiado

Recomendaciones de práctica:

  • Crear una clase de prueba con métodos usando la palabra clave Requires y probar con diferentes roles de usuario
  • Escribir versiones vulnerables y seguras de consultas SQL dinámicas para entender la diferencia
  • Usar $ROLES y $SYSTEM.Security.Check() en la Terminal con diferentes cuentas de usuario
  • Configurar privilegios SQL con GRANT y probar acceso con %CHECKPRIV
  • Crear globals privados de proceso y verificar que son invisibles desde otros procesos usando una segunda sesión de Terminal
  • Intentar operaciones SQL sin privilegios adecuados para observar el comportamiento de SQLCODE -99
  • Practicar GRANT a nivel de columna para restringir acceso a campos sensibles (SSN, códigos de diagnóstico)

Report an Issue