← Volver al listado de tecnologías

Capítulo 19: Ambientes y Sandboxing

Por: Artiko
luaenvironmentssandboxingsecurity_ENV

Capítulo 19: Ambientes y Sandboxing

Introducción

La capacidad de ejecutar código dinámico de forma segura es crucial en muchas aplicaciones: plugins de usuarios, scripts de configuración, reglas de negocio editables, y más. Lua proporciona un sistema de ambientes (_ENV) que permite aislar código no confiable, controlar qué puede y no puede hacer, y proteger el sistema anfitrión de código malicioso o con errores.

Entendiendo _ENV: El Ambiente de Ejecución

En Lua 5.2+, todas las variables globales son en realidad accesos a una tabla llamada _ENV. Cada función tiene su propio _ENV que puede ser modificado.

Ejemplo REPL: _ENV Básico

-- _ENV es la tabla de ambientes globales
print(_ENV)  --> table: 0x...

-- Las "globales" son accesos a _ENV
x = 10
print(_ENV.x)  --> 10

-- Son equivalentes:
_ENV.y = 20
print(y)  --> 20

-- Listar todas las globales
print("=== Variables Globales ===")
for k, v in pairs(_ENV) do
    if type(v) ~= "function" and not k:match("^_") then
        print(k, v)
    end
end

Ejemplo REPL: Cambiar el Ambiente de una Función

-- Crear ambiente personalizado
local customEnv = {
    print = print,  -- Permitir print
    msg = "Hola desde ambiente personalizado"
}

-- Función con ambiente estándar
local function func1()
    print(msg)
end

-- Cambiar ambiente de la función
debug.setupvalue(func1, 1, customEnv)

-- ERROR: func1()  --> Error: msg es nil (ambiente original)

-- Mejor forma: definir función con ambiente explícito
local function createFuncWithEnv(env)
    local f = load([[
        print(msg)
    ]], "chunk", "t", env)
    return f
end

local func2 = createFuncWithEnv(customEnv)
func2()  --> Hola desde ambiente personalizado

Sandbox Básico: Aislar Código No Confiable

Un sandbox es un ambiente controlado que limita lo que el código puede hacer.

Ejemplo REPL: Sandbox Simple

-- Crear sandbox básico con funciones seguras
local function createBasicSandbox()
    local sandbox = {
        -- Funciones matemáticas seguras
        math = {
            abs = math.abs,
            floor = math.floor,
            ceil = math.ceil,
            max = math.max,
            min = math.min,
            sqrt = math.sqrt,
            pi = math.pi
        },

        -- Funciones de string seguras
        string = {
            len = string.len,
            upper = string.upper,
            lower = string.lower,
            sub = string.sub,
            format = string.format,
            rep = string.rep
        },

        -- Funciones de tabla seguras
        table = {
            insert = table.insert,
            concat = table.concat,
            sort = table.sort
        },

        -- Funciones básicas seguras
        print = print,
        type = type,
        tonumber = tonumber,
        tostring = tostring,
        pairs = pairs,
        ipairs = ipairs,
        next = next,

        -- Permitir construcción de tablas
        _VERSION = _VERSION
    }

    return sandbox
end

-- Función para ejecutar código en sandbox
local function runInSandbox(code)
    local sandbox = createBasicSandbox()

    -- Compilar código con ambiente sandbox
    local func, err = load(code, "sandbox", "t", sandbox)

    if not func then
        return nil, "Error de compilación: " .. err
    end

    -- Ejecutar con pcall para capturar errores
    local success, result = pcall(func)

    if not success then
        return nil, "Error de ejecución: " .. result
    end

    return result
end

-- Código seguro: funciona
local result1, err1 = runInSandbox([[
    local sum = 0
    for i = 1, 10 do
        sum = sum + i
    end
    return sum
]])

print(result1)  --> 55

-- Código seguro: operaciones de string
local result2, err2 = runInSandbox([[
    local name = "lua"
    return string.upper(name) .. " " .. string.rep("!", 3)
]])

print(result2)  --> LUA !!!

-- Código peligroso: bloqueado (no tiene acceso a io)
local result3, err3 = runInSandbox([[
    io.open("/etc/passwd", "r")
]])

print(err3)  --> Error de ejecución: attempt to index a nil value (global 'io')

-- Código peligroso: bloqueado (no tiene acceso a os)
local result4, err4 = runInSandbox([[
    os.execute("rm -rf /")
]])

print(err4)  --> Error de ejecución: attempt to index a nil value (global 'os')

Sandbox Avanzado: Control de Recursos

Un sandbox robusto debe controlar no solo qué funciones están disponibles, sino también cuántos recursos puede consumir el código.

Ejemplo REPL: Sandbox con Límite de Tiempo

-- Sandbox con timeout para prevenir loops infinitos
local function createTimedSandbox(maxTime)
    local startTime

    local sandbox = createBasicSandbox()

    -- Hook para verificar tiempo de ejecución
    local function checkTime()
        if os.clock() - startTime > maxTime then
            error("Timeout: ejecución excedió " .. maxTime .. " segundos")
        end
    end

    return sandbox, checkTime
end

local function runWithTimeout(code, maxTime)
    local sandbox, checkTime = createTimedSandbox(maxTime)

    local func, err = load(code, "sandbox", "t", sandbox)
    if not func then
        return nil, "Error de compilación: " .. err
    end

    -- Establecer hook de debug para verificar tiempo
    local startTime = os.clock()
    debug.sethook(checkTime, "", 10000)  -- Verificar cada 10000 instrucciones

    local success, result = pcall(func)

    -- Limpiar hook
    debug.sethook()

    if not success then
        return nil, "Error: " .. result
    end

    return result
end

-- Código que termina rápido: funciona
local result1 = runWithTimeout([[
    local sum = 0
    for i = 1, 1000 do
        sum = sum + i
    end
    return sum
]], 1)

print(result1)  --> 500500

-- Loop infinito: bloqueado por timeout
local result2, err2 = runWithTimeout([[
    while true do
        -- loop infinito
    end
]], 0.5)

print(err2)  --> Error: Timeout: ejecución excedió 0.5 segundos

Ejemplo REPL: Sandbox con Límite de Memoria

-- Sandbox que limita el uso de memoria
local function createMemoryLimitedSandbox(maxMemoryKB)
    local sandbox = createBasicSandbox()
    local initialMemory = collectgarbage("count")

    -- Función para verificar memoria
    local function checkMemory()
        local currentMemory = collectgarbage("count")
        local usedMemory = currentMemory - initialMemory

        if usedMemory > maxMemoryKB then
            error(string.format(
                "Límite de memoria excedido: %.2f KB usado, máximo %.2f KB",
                usedMemory, maxMemoryKB
            ))
        end
    end

    -- Envolver funciones que crean tablas
    local originalInsert = table.insert
    sandbox.table.insert = function(...)
        checkMemory()
        return originalInsert(...)
    end

    return sandbox, checkMemory
end

local function runWithMemoryLimit(code, maxMemoryKB)
    local sandbox, checkMemory = createMemoryLimitedSandbox(maxMemoryKB)

    local func, err = load(code, "sandbox", "t", sandbox)
    if not func then
        return nil, "Error de compilación: " .. err
    end

    -- Hook para verificar memoria periódicamente
    debug.sethook(checkMemory, "", 1000)

    local success, result = pcall(func)

    debug.sethook()

    if not success then
        return nil, "Error: " .. result
    end

    return result
end

-- Código con uso moderado de memoria: funciona
local result1 = runWithMemoryLimit([[
    local t = {}
    for i = 1, 100 do
        table.insert(t, i)
    end
    return #t
]], 100)

print(result1)  --> 100

-- Código que intenta usar mucha memoria: bloqueado
local result2, err2 = runWithMemoryLimit([[
    local t = {}
    for i = 1, 1000000 do
        table.insert(t, string.rep("x", 1000))
    end
    return #t
]], 100)

print(err2)  --> Error: Límite de memoria excedido...

Ambientes Anidados: Herencia de Permisos

Podemos crear jerarquías de ambientes donde algunos tienen más permisos que otros.

Ejemplo REPL: Sistema de Permisos por Niveles

-- Sistema de ambientes con niveles de permisos
local PermissionLevels = {
    RESTRICTED = 1,  -- Solo lectura y matemáticas básicas
    STANDARD = 2,    -- + manipulación de strings y tablas
    ELEVATED = 3,    -- + operaciones de fecha/hora
    ADMIN = 4        -- Acceso completo (peligroso)
}

local function createEnvironment(level)
    local env = {}

    -- Nivel RESTRICTED: lo mínimo
    if level >= PermissionLevels.RESTRICTED then
        env.print = print
        env.type = type
        env.tonumber = tonumber
        env.tostring = tostring
        env.math = {
            abs = math.abs,
            floor = math.floor,
            ceil = math.ceil,
            max = math.max,
            min = math.min
        }
    end

    -- Nivel STANDARD: + strings y tablas
    if level >= PermissionLevels.STANDARD then
        env.string = {
            len = string.len,
            upper = string.upper,
            lower = string.lower,
            sub = string.sub,
            format = string.format
        }
        env.table = {
            insert = table.insert,
            concat = table.concat,
            sort = table.sort
        }
        env.pairs = pairs
        env.ipairs = ipairs
    end

    -- Nivel ELEVATED: + fecha/hora
    if level >= PermissionLevels.ELEVATED then
        env.os = {
            time = os.time,
            date = os.date,
            clock = os.clock,
            difftime = os.difftime
        }
    end

    -- Nivel ADMIN: acceso completo (usar con precaución)
    if level >= PermissionLevels.ADMIN then
        env.io = io
        env.os = os
        env.require = require
        env.dofile = dofile
        env.loadfile = loadfile
    end

    return env
end

-- Función para ejecutar código con nivel de permisos
local function runWithPermissions(code, level)
    local env = createEnvironment(level)

    local func, err = load(code, "code", "t", env)
    if not func then
        return nil, "Error: " .. err
    end

    return pcall(func)
end

-- Código RESTRICTED: solo matemáticas
local ok1, result1 = runWithPermissions([[
    return math.abs(-42)
]], PermissionLevels.RESTRICTED)

print(result1)  --> 42

-- Código STANDARD: puede usar strings
local ok2, result2 = runWithPermissions([[
    return string.upper("hello")
]], PermissionLevels.STANDARD)

print(result2)  --> HELLO

-- Código ELEVATED: puede usar fecha/hora
local ok3, result3 = runWithPermissions([[
    return os.date("%Y-%m-%d")
]], PermissionLevels.ELEVATED)

print(result3)  --> 2025-01-20 (o fecha actual)

-- RESTRICTED intenta usar strings: falla
local ok4, err4 = runWithPermissions([[
    return string.upper("hello")
]], PermissionLevels.RESTRICTED)

print(err4)  --> attempt to index a nil value (global 'string')

-- STANDARD intenta usar os: falla
local ok5, err5 = runWithPermissions([[
    return os.date()
]], PermissionLevels.STANDARD)

print(err5)  --> attempt to index a nil value (global 'os')

Deep Dive: Proxies de Ambiente con Metatables

Podemos usar metatables para crear ambientes que monitorean y controlan cada acceso.

Ejemplo REPL: Ambiente con Logging de Accesos

-- Crear ambiente que registra todos los accesos
local function createLoggingEnvironment(baseEnv)
    local accessLog = {}

    local proxy = {}

    local mt = {
        __index = function(t, key)
            table.insert(accessLog, {
                operation = "READ",
                key = key,
                timestamp = os.clock()
            })

            return baseEnv[key]
        end,

        __newindex = function(t, key, value)
            table.insert(accessLog, {
                operation = "WRITE",
                key = key,
                value = value,
                timestamp = os.clock()
            })

            baseEnv[key] = value
        end
    }

    setmetatable(proxy, mt)

    -- Función para obtener el log
    function proxy.getAccessLog()
        return accessLog
    end

    return proxy
end

-- Usar ambiente con logging
local baseEnv = createBasicSandbox()
local loggedEnv = createLoggingEnvironment(baseEnv)

-- Ejecutar código
local code = [[
    local x = math.abs(-10)
    local y = string.upper("test")
    print(x, y)
]]

local func = load(code, "code", "t", loggedEnv)
func()
--> 10  TEST

-- Ver log de accesos
print("\n=== Access Log ===")
for _, entry in ipairs(loggedEnv.getAccessLog()) do
    print(string.format(
        "[%.6f] %s: %s",
        entry.timestamp,
        entry.operation,
        entry.key
    ))
end
--> [0.000123] READ: math
--> [0.000234] READ: string
--> [0.000345] READ: print

Ejemplo REPL: Ambiente con Cuotas de Recursos

-- Ambiente que limita cuántas veces se pueden llamar funciones
local function createQuotaEnvironment(baseEnv, quotas)
    local usage = {}

    -- Inicializar contadores
    for key in pairs(quotas) do
        usage[key] = 0
    end

    local proxy = {}

    local mt = {
        __index = function(t, key)
            local value = baseEnv[key]

            -- Si es una función con cuota, envolver
            if type(value) == "function" and quotas[key] then
                return function(...)
                    usage[key] = usage[key] + 1

                    if usage[key] > quotas[key] then
                        error(string.format(
                            "Cuota excedida para '%s': %d/%d llamadas",
                            key, usage[key] - 1, quotas[key]
                        ))
                    end

                    return value(...)
                end
            end

            return value
        end,

        __newindex = function(t, key, value)
            baseEnv[key] = value
        end
    }

    setmetatable(proxy, mt)

    -- Función para ver uso de recursos
    function proxy.getResourceUsage()
        return usage
    end

    return proxy
end

-- Crear ambiente con cuotas
local baseEnv = {
    print = print,
    calculate = function(x) return x * 2 end,
    processData = function(data) return #data end
}

local quotaEnv = createQuotaEnvironment(baseEnv, {
    print = 3,          -- Máximo 3 prints
    calculate = 5,      -- Máximo 5 cálculos
    processData = 2     -- Máximo 2 procesamientos
})

-- Código que respeta las cuotas
local code1 = [[
    print("Primera llamada")
    print("Segunda llamada")
    print("Tercera llamada")
]]

local func1 = load(code1, "code", "t", quotaEnv)
func1()
--> Primera llamada
--> Segunda llamada
--> Tercera llamada

-- Código que excede cuota
local code2 = [[
    for i = 1, 10 do
        calculate(i)
    end
]]

local func2 = load(code2, "code", "t", quotaEnv)
local ok, err = pcall(func2)
print(err)
--> Cuota excedida para 'calculate': 5/5 llamadas

-- Ver uso de recursos
print("\n=== Uso de Recursos ===")
for func, count in pairs(quotaEnv.getResourceUsage()) do
    print(string.format("%s: %d llamadas", func, count))
end

Soapbox: Seguridad del Sandbox no es Absoluta

Advertencia Importante: Ningún sandbox es 100% seguro.

Limitaciones conocidas:

  1. Debug library: Si está disponible, puede romper cualquier sandbox
  2. Ataques de timing: Medir tiempo de ejecución puede filtrar información
  3. Agotamiento de recursos: Difícil de prevenir completamente
  4. Bugs de Lua: Vulnerabilidades en el intérprete mismo

Mejores prácticas:

-- MAL: Confiar solo en sandbox de Lua
local function unsafeSandbox(code)
    -- Solo remover funciones peligrosas no es suficiente
    local env = {print = print, math = math}
    return load(code, "code", "t", env)
end

-- MEJOR: Múltiples capas de protección
local function betterSandbox(code)
    -- 1. Validar sintaxis
    local func, err = load(code, "code", "t", {})
    if not func then return nil, err end

    -- 2. Ambiente restringido
    local env = createBasicSandbox()
    func = load(code, "code", "t", env)

    -- 3. Límites de recursos
    debug.sethook(checkTimeout, "", 1000)

    -- 4. Ejecutar con pcall
    local ok, result = pcall(func)

    -- 5. Limpiar
    debug.sethook()

    return ok, result
end

-- MEJOR AÚN: Proceso separado
-- Ejecutar código no confiable en proceso aislado con OS sandboxing
-- (Docker, chroot, seccomp, etc.)

Recomendaciones:

Caso Práctico: Sistema de Plugins Seguro

Implementaremos un sistema completo de plugins con aislamiento, validación y control de recursos.

-- Sistema de plugins con sandboxing robusto
local PluginSystem = {}

function PluginSystem.new()
    local system = {
        plugins = {},
        apiVersion = "1.0"
    }

    -- API disponible para plugins
    local function createPluginAPI(pluginName)
        return {
            -- Utilidades básicas
            print = function(...)
                print(string.format("[Plugin:%s]", pluginName), ...)
            end,

            -- Matemáticas seguras
            math = {
                abs = math.abs,
                floor = math.floor,
                ceil = math.ceil,
                max = math.max,
                min = math.min,
                sqrt = math.sqrt,
                sin = math.sin,
                cos = math.cos,
                pi = math.pi
            },

            -- Strings seguros
            string = {
                len = string.len,
                upper = string.upper,
                lower = string.lower,
                sub = string.sub,
                format = string.format,
                match = string.match,
                gsub = string.gsub
            },

            -- Tablas seguras
            table = {
                insert = table.insert,
                remove = table.remove,
                concat = table.concat,
                sort = table.sort
            },

            -- Funciones básicas
            type = type,
            tonumber = tonumber,
            tostring = tostring,
            pairs = pairs,
            ipairs = ipairs,
            next = next,
            assert = assert,
            error = error,

            -- API del sistema
            getPluginName = function()
                return pluginName
            end,

            getAPIVersion = function()
                return system.apiVersion
            end,

            -- Storage aislado por plugin
            storage = {},

            -- Logging
            log = function(level, message)
                print(string.format(
                    "[%s][%s] %s",
                    os.date("%H:%M:%S"),
                    level,
                    message
                ))
            end
        }
    end

    -- Cargar plugin desde código
    function system.loadPlugin(name, code, config)
        config = config or {}
        local maxTime = config.maxTime or 1.0
        local maxMemoryKB = config.maxMemory or 1024

        -- Validar nombre
        if system.plugins[name] then
            return false, "Plugin ya existe: " .. name
        end

        -- Crear ambiente para el plugin
        local api = createPluginAPI(name)

        -- Compilar código del plugin
        local pluginFunc, compileErr = load(code, name, "t", api)

        if not pluginFunc then
            return false, "Error de compilación: " .. compileErr
        end

        -- Ejecutar con límites
        local startTime = os.clock()
        local startMemory = collectgarbage("count")

        local function checkLimits()
            -- Verificar tiempo
            if os.clock() - startTime > maxTime then
                error("Plugin excedió tiempo máximo de carga")
            end

            -- Verificar memoria
            local usedMemory = collectgarbage("count") - startMemory
            if usedMemory > maxMemoryKB then
                error(string.format(
                    "Plugin excedió memoria máxima: %.2f KB",
                    usedMemory
                ))
            end
        end

        debug.sethook(checkLimits, "", 1000)

        local ok, plugin = pcall(pluginFunc)

        debug.sethook()

        if not ok then
            return false, "Error de ejecución: " .. plugin
        end

        -- Validar interfaz del plugin
        if type(plugin) ~= "table" then
            return false, "Plugin debe retornar una tabla"
        end

        if not plugin.init or type(plugin.init) ~= "function" then
            return false, "Plugin debe tener función init"
        end

        -- Guardar plugin
        system.plugins[name] = {
            instance = plugin,
            api = api,
            config = config,
            loadTime = os.clock() - startTime
        }

        return true, "Plugin cargado exitosamente"
    end

    -- Ejecutar método de plugin
    function system.callPlugin(name, method, ...)
        local plugin = system.plugins[name]

        if not plugin then
            return false, "Plugin no encontrado: " .. name
        end

        local func = plugin.instance[method]

        if not func or type(func) ~= "function" then
            return false, string.format(
                "Método '%s' no existe en plugin '%s'",
                method, name
            )
        end

        -- Ejecutar con protección
        local ok, result = pcall(func, plugin.instance, ...)

        if not ok then
            return false, "Error en plugin: " .. result
        end

        return true, result
    end

    -- Listar plugins cargados
    function system.listPlugins()
        local list = {}
        for name, plugin in pairs(system.plugins) do
            table.insert(list, {
                name = name,
                loadTime = plugin.loadTime,
                hasInit = plugin.instance.init ~= nil,
                hasUpdate = plugin.instance.update ~= nil
            })
        end
        return list
    end

    -- Descargar plugin
    function system.unloadPlugin(name)
        if not system.plugins[name] then
            return false, "Plugin no encontrado"
        end

        -- Llamar cleanup si existe
        local ok, err = system.callPlugin(name, "cleanup")

        system.plugins[name] = nil
        collectgarbage("collect")

        return true
    end

    return system
end

-- === EJEMPLOS DE USO ===

-- Crear sistema
local pluginSys = PluginSystem.new()

-- Plugin 1: Calculadora
local calculatorCode = [[
    local plugin = {}

    function plugin:init()
        self:log("INFO", "Calculadora inicializada")
        storage.operations = 0
    end

    function plugin:add(a, b)
        storage.operations = storage.operations + 1
        return a + b
    end

    function plugin:multiply(a, b)
        storage.operations = storage.operations + 1
        return a * b
    end

    function plugin:getStats()
        return {
            operations = storage.operations
        }
    end

    function plugin:cleanup()
        self:log("INFO", "Limpiando calculadora")
    end

    return plugin
]]

local ok, err = pluginSys.loadPlugin("calculator", calculatorCode)
print("Cargar calculator:", ok, err)
--> Cargar calculator: true  Plugin cargado exitosamente

-- Inicializar plugin
pluginSys.callPlugin("calculator", "init")
--> [Plugin:calculator] [INFO] Calculadora inicializada

-- Usar plugin
local ok1, result1 = pluginSys.callPlugin("calculator", "add", 5, 3)
print("5 + 3 =", result1)  --> 8

local ok2, result2 = pluginSys.callPlugin("calculator", "multiply", 4, 7)
print("4 * 7 =", result2)  --> 28

local ok3, stats = pluginSys.callPlugin("calculator", "getStats")
print("Operaciones:", stats.operations)  --> 2

-- Plugin 2: Formateador de texto
local formatterCode = [[
    local plugin = {}

    function plugin:init()
        self:log("INFO", "Formateador inicializado")
    end

    function plugin:uppercase(text)
        return string.upper(text)
    end

    function plugin:reverse(text)
        return text:reverse()
    end

    function plugin:titleCase(text)
        return text:gsub("(%a)([%w_']*)", function(first, rest)
            return string.upper(first) .. string.lower(rest)
        end)
    end

    return plugin
]]

pluginSys.loadPlugin("formatter", formatterCode)
pluginSys.callPlugin("formatter", "init")

local ok4, result4 = pluginSys.callPlugin("formatter", "titleCase", "hello world")
print(result4)  --> Hello World

-- Listar todos los plugins
print("\n=== Plugins Cargados ===")
for _, info in ipairs(pluginSys.listPlugins()) do
    print(string.format(
        "- %s (tiempo de carga: %.4fs)",
        info.name, info.loadTime
    ))
end

-- Plugin malicioso: bloqueado
local maliciousCode = [[
    local plugin = {}

    function plugin:init()
        -- Intentar acceder a funciones peligrosas
        io.open("/etc/passwd", "r")  -- Bloqueado: io no existe
    end

    return plugin
]]

local ok5, err5 = pluginSys.loadPlugin("malicious", maliciousCode)
print("\nPlugin malicioso:", ok5, err5)
--> Plugin malicioso: false  Error de ejecución: attempt to index a nil value (global 'io')

-- Descargar plugin
pluginSys.unloadPlugin("calculator")
--> [Plugin:calculator] [INFO] Limpiando calculadora

Ejercicios

Ejercicio 1: Ambiente con Whitelist de Funciones

Crea un ambiente que solo permite llamar funciones específicas de una whitelist, bloqueando todo lo demás.

Requisitos:

-- Tu implementación aquí
local function createWhitelistEnvironment(whitelist)
    -- Implementar ambiente con whitelist
end

-- Ejemplo de uso:
local env = createWhitelistEnvironment({
    "print", "math.abs", "string.upper"
})

local code = [[
    print(string.upper("hello"))  -- OK
    print(math.abs(-10))          -- OK
    -- os.time()                   -- Bloqueado
]]

Solución:

local function createWhitelistEnvironment(whitelist)
    local allowed = {}
    local blockedAccesses = {}

    -- Procesar whitelist
    for _, path in ipairs(whitelist) do
        allowed[path] = true
    end

    -- Crear ambiente base
    local env = {}

    -- Helper para obtener valor desde path
    local function getFromGlobal(path)
        local parts = {}
        for part in path:gmatch("[^.]+") do
            table.insert(parts, part)
        end

        local value = _G
        for _, part in ipairs(parts) do
            value = value[part]
            if value == nil then break end
        end

        return value
    end

    -- Metatable para controlar accesos
    local mt = {
        __index = function(t, key)
            -- Verificar si está en whitelist
            if allowed[key] then
                return getFromGlobal(key)
            end

            -- Verificar patrones como "math.abs"
            for allowedPath in pairs(allowed) do
                if allowedPath:match("^" .. key .. "%.") then
                    -- Crear subtabla permitida
                    local subtable = {}
                    local prefix = key .. "."

                    setmetatable(subtable, {
                        __index = function(st, subkey)
                            local fullPath = prefix .. subkey
                            if allowed[fullPath] then
                                return getFromGlobal(fullPath)
                            end

                            table.insert(blockedAccesses, fullPath)
                            error("Acceso bloqueado: " .. fullPath)
                        end
                    })

                    return subtable
                end
            end

            -- No permitido
            table.insert(blockedAccesses, key)
            error("Acceso bloqueado: " .. key)
        end,

        __newindex = function(t, key, value)
            -- Permitir crear variables locales
            rawset(t, key, value)
        end
    }

    setmetatable(env, mt)

    -- Función para obtener accesos bloqueados
    function env.getBlockedAccesses()
        return blockedAccesses
    end

    return env
end

-- Test
local env = createWhitelistEnvironment({
    "print", "math.abs", "math.floor", "string.upper", "string.lower"
})

local code = [[
    print(string.upper("hello"))
    print(math.abs(-42))

    local x = 10
    print(x)
]]

local func = load(code, "test", "t", env)
func()
--> HELLO
--> 42
--> 10

-- Intentar acceso bloqueado
local badCode = [[
    os.time()
]]

local badFunc = load(badCode, "bad", "t", env)
local ok, err = pcall(badFunc)
print(err)  --> Acceso bloqueado: os

Ejercicio 2: Sistema de Módulos con Lazy Loading y Permisos

Implementa un sistema donde los módulos se cargan bajo demanda y cada módulo tiene permisos diferentes.

Requisitos:

-- Tu implementación aquí
local function createModuleSystem()
    -- Implementar sistema de módulos
end

-- Ejemplo de uso:
local modules = createModuleSystem()

modules.register("utils", {
    permissions = {"read"},
    factory = function() return {greet = function() return "Hi" end} end
})

local utils = modules.load("utils", {"read"})
print(utils.greet())

Solución:

local function createModuleSystem()
    local registry = {}
    local loaded = {}

    local system = {}

    -- Registrar módulo
    function system.register(name, spec)
        if registry[name] then
            error("Módulo ya registrado: " .. name)
        end

        registry[name] = {
            permissions = spec.permissions or {},
            factory = spec.factory
        }
    end

    -- Verificar permisos
    local function hasPermissions(required, granted)
        for _, req in ipairs(required) do
            local found = false
            for _, grant in ipairs(granted) do
                if grant == req or grant == "admin" then
                    found = true
                    break
                end
            end

            if not found then
                return false, "Falta permiso: " .. req
            end
        end

        return true
    end

    -- Cargar módulo
    function system.load(name, userPermissions)
        -- Verificar que existe
        if not registry[name] then
            error("Módulo no encontrado: " .. name)
        end

        -- Verificar permisos
        local spec = registry[name]
        local ok, err = hasPermissions(spec.permissions, userPermissions)

        if not ok then
            error(string.format(
                "Permisos insuficientes para '%s': %s",
                name, err
            ))
        end

        -- Si ya está cargado, retornar
        if loaded[name] then
            return loaded[name]
        end

        -- Cargar módulo
        print("Cargando módulo:", name)
        local module = spec.factory()
        loaded[name] = module

        return module
    end

    -- Descargar módulo
    function system.unload(name)
        loaded[name] = nil
        collectgarbage("collect")
    end

    -- Listar módulos
    function system.list()
        local list = {}
        for name, spec in pairs(registry) do
            table.insert(list, {
                name = name,
                permissions = spec.permissions,
                loaded = loaded[name] ~= nil
            })
        end
        return list
    end

    return system
end

-- Test
local modules = createModuleSystem()

-- Registrar módulos con diferentes permisos
modules.register("public_utils", {
    permissions = {},
    factory = function()
        return {
            greet = function() return "Hello!" end
        }
    end
})

modules.register("user_data", {
    permissions = {"read"},
    factory = function()
        return {
            getName = function() return "Alice" end
        }
    end
})

modules.register("admin_tools", {
    permissions = {"admin"},
    factory = function()
        return {
            deleteAll = function() return "Deleted!" end
        }
    end
})

-- Usuario sin permisos
local publicMod = modules.load("public_utils", {})
print(publicMod.greet())  --> Hello!

-- Usuario con permiso de lectura
local userData = modules.load("user_data", {"read"})
print(userData.getName())  --> Alice

-- Intentar cargar módulo sin permisos
local ok, err = pcall(function()
    modules.load("admin_tools", {"read"})
end)
print(err)  --> Permisos insuficientes para 'admin_tools': Falta permiso: admin

-- Usuario admin puede cargar todo
local adminMod = modules.load("admin_tools", {"admin"})
print(adminMod.deleteAll())  --> Deleted!

Ejercicio 3: Sandbox con Rate Limiting

Crea un sandbox que limita cuántas operaciones por segundo puede realizar el código.

Requisitos:

-- Tu implementación aquí
local function createRateLimitedSandbox(limits)
    -- Implementar sandbox con rate limiting
end

-- Ejemplo de uso:
local sandbox = createRateLimitedSandbox({
    print = 5,      -- Máximo 5 prints por segundo
    calculate = 10  -- Máximo 10 cálculos por segundo
})

Solución:

local function createRateLimitedSandbox(limits)
    local callHistory = {}

    -- Inicializar históricos
    for funcName in pairs(limits) do
        callHistory[funcName] = {}
    end

    -- Limpiar histórico antiguo
    local function cleanOldCalls(funcName)
        local now = os.clock()
        local history = callHistory[funcName]
        local newHistory = {}

        for _, timestamp in ipairs(history) do
            if now - timestamp < 1.0 then
                table.insert(newHistory, timestamp)
            end
        end

        callHistory[funcName] = newHistory
    end

    -- Verificar rate limit
    local function checkRateLimit(funcName)
        cleanOldCalls(funcName)

        local history = callHistory[funcName]
        local limit = limits[funcName]

        if #history >= limit then
            error(string.format(
                "Rate limit excedido para '%s': %d/%d llamadas por segundo",
                funcName, #history, limit
            ))
        end

        table.insert(history, os.clock())
    end

    -- Crear sandbox
    local sandbox = {
        print = function(...)
            checkRateLimit("print")
            print(...)
        end,

        calculate = function(x)
            checkRateLimit("calculate")
            return x * 2
        end,

        -- Otras funciones básicas sin límite
        type = type,
        tostring = tostring,
        tonumber = tonumber
    }

    -- Función para obtener estadísticas
    function sandbox.getRateLimitStats()
        local stats = {}
        for funcName, history in pairs(callHistory) do
            cleanOldCalls(funcName)
            stats[funcName] = {
                calls = #history,
                limit = limits[funcName],
                remaining = limits[funcName] - #history
            }
        end
        return stats
    end

    return sandbox
end

-- Test
local sandbox = createRateLimitedSandbox({
    print = 3,
    calculate = 5
})

-- Usar funciones dentro del límite
sandbox.print("Mensaje 1")
sandbox.print("Mensaje 2")
sandbox.print("Mensaje 3")

-- Próxima llamada excede el límite
local ok, err = pcall(function()
    sandbox.print("Mensaje 4")
end)
print("\nError:", err)
--> Error: Rate limit excedido para 'print': 3/3 llamadas por segundo

-- Ver estadísticas
print("\n=== Rate Limit Stats ===")
for func, stats in pairs(sandbox.getRateLimitStats()) do
    print(string.format(
        "%s: %d/%d (restantes: %d)",
        func, stats.calls, stats.limit, stats.remaining
    ))
end

-- Esperar 1 segundo y resetear
os.execute("sleep 1")

sandbox.print("Mensaje después de 1 segundo")  --> Funciona
print("Rate limit reseteado correctamente")

Resumen

En este capítulo exploramos:

  1. _ENV: El sistema de ambientes de Lua y cómo funciona
  2. Sandbox básico: Aislar código limitando funciones disponibles
  3. Control de recursos: Límites de tiempo y memoria
  4. Permisos por niveles: Jerarquías de acceso
  5. Proxies de ambiente: Monitoreo y control con metatables
  6. Caso práctico: Sistema completo de plugins con sandboxing

El sandboxing es esencial para ejecutar código no confiable, pero debe combinarse con protecciones a nivel de sistema operativo para aplicaciones de producción. En el próximo capítulo exploraremos técnicas avanzadas de optimización y profiling.