Capítulo 19: Ambientes y Sandboxing
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:
- Debug library: Si está disponible, puede romper cualquier sandbox
- Ataques de timing: Medir tiempo de ejecución puede filtrar información
- Agotamiento de recursos: Difícil de prevenir completamente
- 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:
- Nunca confíes completamente en sandbox de Lua para código malicioso
- Siempre usa sandboxing a nivel OS para código no confiable
- Combina múltiples capas de protección
- Audita el código antes de ejecutarlo si es posible
- Monitorea el uso de recursos en producción
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:
- Lista configurable de funciones permitidas
- Bloquear acceso a funciones no listadas
- Permitir crear nuevas variables locales
- Registrar intentos de acceso bloqueados
-- 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:
- Módulos se cargan solo cuando se usan
- Cada módulo tiene nivel de permisos
- Verificar permisos antes de retornar módulo
- Cachear módulos cargados
-- 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:
- Limitar operaciones por segundo (rate limit)
- Diferentes límites para diferentes operaciones
- Ventana deslizante (sliding window)
- Resetear contadores automáticamente
-- 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:
- _ENV: El sistema de ambientes de Lua y cómo funciona
- Sandbox básico: Aislar código limitando funciones disponibles
- Control de recursos: Límites de tiempo y memoria
- Permisos por niveles: Jerarquías de acceso
- Proxies de ambiente: Monitoreo y control con metatables
- 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.