]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
lua: Add lua sandbox for detection rules
authorJo Johnson <pyrojoe314@gmail.com>
Wed, 20 Dec 2023 20:14:03 +0000 (12:14 -0800)
committerJason Ish <jason.ish@oisf.net>
Mon, 27 May 2024 22:00:17 +0000 (16:00 -0600)
src/Makefile.am
src/detect-lua.c
src/detect-lua.h
src/util-lua-sandbox.c [new file with mode: 0644]
src/util-lua-sandbox.h [new file with mode: 0644]
src/util-lua.c

index 65ebc0216632edd3ed8d8f9083c11a0761ee282d..878138539f46a66a3711c099542465e7fc1b7d82 100755 (executable)
@@ -562,6 +562,7 @@ noinst_HEADERS = \
        util-lua-hassh.h \
        util-lua-http.h \
        util-lua-ja3.h \
+       util-lua-sandbox.h \
        util-lua-smtp.h \
        util-lua-ssh.h \
        util-lua-tls.h \
@@ -1155,6 +1156,7 @@ libsuricata_c_a_SOURCES = \
        util-lua-hassh.c \
        util-lua-http.c \
        util-lua-ja3.c \
+       util-lua-sandbox.c \
        util-lua-smtp.c \
        util-lua-ssh.c \
        util-lua-tls.c \
index a88f7e79a86ac0eba8c20bd38f55337fc76c1413..023aee82baeebded4cc08cdcdb44dd4174b3648a 100644 (file)
@@ -83,6 +83,7 @@ void DetectLuaRegister(void)
 #else /* HAVE_LUA */
 
 #include "util-lua.h"
+#include "util-lua-sandbox.h"
 
 static int DetectLuaMatch (DetectEngineThreadCtx *,
         Packet *, const Signature *, const SigMatchCtx *);
@@ -97,6 +98,9 @@ static void DetectLuaRegisterTests(void);
 static void DetectLuaFree(DetectEngineCtx *, void *);
 static int g_smtp_generic_list_id = 0;
 
+// TODO: move to config
+static const uint64_t g_lua_alloc_limit = 500000, g_lua_instruction_limit = 500000;
+
 /**
  * \brief Registration function for keyword: lua
  */
@@ -479,7 +483,7 @@ static void *DetectLuaThreadInit(void *data)
     t->alproto = lua->alproto;
     t->flags = lua->flags;
 
-    t->luastate = LuaGetState();
+    t->luastate = sb_newstate(g_lua_alloc_limit, g_lua_instruction_limit);
     if (t->luastate == NULL) {
         SCLogError("luastate pool depleted");
         goto error;
@@ -525,7 +529,7 @@ static void *DetectLuaThreadInit(void *data)
 
 error:
     if (t->luastate != NULL)
-        LuaReturnState(t->luastate);
+        sb_close(t->luastate);
     SCFree(t);
     return NULL;
 }
@@ -535,7 +539,7 @@ static void DetectLuaThreadFree(void *ctx)
     if (ctx != NULL) {
         DetectLuaThreadData *t = (DetectLuaThreadData *)ctx;
         if (t->luastate != NULL)
-            LuaReturnState(t->luastate);
+            sb_close(t->luastate);
         SCFree(t);
     }
 }
@@ -581,10 +585,10 @@ static int DetectLuaSetupPrime(DetectEngineCtx *de_ctx, DetectLuaData *ld, const
 {
     int status;
 
-    lua_State *luastate = luaL_newstate();
+    lua_State *luastate = sb_newstate(g_lua_alloc_limit, g_lua_instruction_limit);
     if (luastate == NULL)
         return -1;
-    luaL_openlibs(luastate);
+    luaL_openlibs(luastate); // TODO: get sandbox config and load appropriate libs
 
     /* hackish, needed to allow unittests to pass buffers as scripts instead of files */
 #ifdef UNITTESTS
@@ -863,10 +867,10 @@ static int DetectLuaSetupPrime(DetectEngineCtx *de_ctx, DetectLuaData *ld, const
 
     /* pop the table */
     lua_pop(luastate, 1);
-    lua_close(luastate);
+    sb_close(luastate);
     return 0;
 error:
-    lua_close(luastate);
+    sb_close(luastate);
     return -1;
 }
 
index 4f3834087f17ed89a14d5ee94cff984c253ba371..104c631fe35580e8ea8c6677a68bb6357530773a 100644 (file)
@@ -27,6 +27,7 @@
 #ifdef HAVE_LUA
 
 #include "util-lua.h"
+#include "util-lua-sandbox.h"
 
 typedef struct DetectLuaThreadData {
     lua_State *luastate;
diff --git a/src/util-lua-sandbox.c b/src/util-lua-sandbox.c
new file mode 100644 (file)
index 0000000..0fcb1dd
--- /dev/null
@@ -0,0 +1,299 @@
+/* Copyright (C) 2014-2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Jo Johnson <pyrojoe314@gmail.com>
+ */
+
+#include "suricata-common.h"
+
+#ifdef HAVE_LUA
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lua.h"
+
+#include "lauxlib.h"
+#include "lualib.h"
+
+#include "util-lua-sandbox.h"
+
+typedef struct sb_block_function {
+    const char *module;
+    const char *name;
+} sb_block_function;
+
+static void sb_hook(lua_State *L, lua_Debug *ar);
+LUAMOD_API int luaopen_sandbox(lua_State *L);
+
+static void *sb_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
+{
+    (void)ud;
+    (void)osize; /* not used */
+    sb_lua_state *ctx = (sb_lua_state *)ud;
+    if (nsize == 0) {
+        if (ptr != NULL) {
+            // ASSERT: alloc_bytes > osize
+            ctx->alloc_bytes -= osize;
+        }
+        SCFree(ptr);
+        return NULL;
+    } else {
+        // We can be a bit sloppy on the alloc limit since it's not supposed to be hit.
+        //  ASSERT: ctx->alloc_bytes + nsize > ctx->alloc_bytes
+        if (ctx->alloc_bytes + nsize > ctx->alloc_limit) {
+            // TODO: Trace in a better way
+            return NULL;
+        }
+        void *nptr = SCRealloc(ptr, nsize);
+
+        ctx->alloc_bytes += nsize;
+        return nptr;
+    }
+}
+
+/*
+** These are the set of libs allowed in the restricted lua sandbox
+*/
+static const luaL_Reg sb_restrictedlibs[] = { { LUA_GNAME, luaopen_base },
+    //  {LUA_LOADLIBNAME, luaopen_package},
+    //  {LUA_COLIBNAME, luaopen_coroutine},
+    { LUA_TABLIBNAME, luaopen_table },
+    //{LUA_IOLIBNAME, luaopen_io},
+    //  {LUA_OSLIBNAME, luaopen_os},
+    { LUA_STRLIBNAME, luaopen_string }, { LUA_MATHLIBNAME, luaopen_math },
+    { LUA_UTF8LIBNAME, luaopen_utf8 },
+    { LUA_DBLIBNAME, luaopen_sandbox }, // TODO: remove this from restricted
+    { NULL, NULL } };
+
+// TODO: should we block raw* functions?
+// TODO: Will we ever need to block a subset of functions more than one level deep?
+static const sb_block_function sb_restrictedfuncs[] = { { LUA_GNAME, "collectgarbage" },
+    { LUA_GNAME, "dofile" }, { LUA_GNAME, "getmetatable" }, { LUA_GNAME, "loadfile" },
+    { LUA_GNAME, "load" }, { LUA_GNAME, "pcall" }, { LUA_GNAME, "setmetatable" },
+    { LUA_GNAME, "xpcall" },
+
+    { LUA_STRLIBNAME, "rep" }, // TODO:  probably don't need to block this for normal restricted
+                               // since we have memory limit
+    { NULL, NULL } };
+
+static void sb_loadlibs(lua_State *L, const luaL_Reg *libs)
+{
+    const luaL_Reg *lib;
+    /* "require" functions from 'loadedlibs' and set results to global table */
+    for (lib = libs; lib->func; lib++) {
+        luaL_requiref(L, lib->name, lib->func, 1);
+        lua_pop(L, 1); /* remove lib */
+    }
+}
+
+static void sb_block_functions(lua_State *L, const sb_block_function *funcs)
+{
+    const sb_block_function *func;
+
+    // set target functions to nil
+    lua_pushglobaltable(L);
+    for (func = funcs; func->module; func++) {
+        lua_pushstring(L, func->module);
+        lua_gettable(L, -2); // load module to stack
+        lua_pushstring(L, func->name);
+        lua_pushnil(L);
+        lua_settable(L, -3);
+        lua_pop(L, 1); // remove module from the stack
+    }
+    lua_pop(L, 1); // remove global table
+}
+
+LUALIB_API void sb_loadrestricted(lua_State *L)
+{
+    sb_loadlibs(L, sb_restrictedlibs);
+    sb_block_functions(L, sb_restrictedfuncs);
+}
+
+lua_State *sb_newstate(uint64_t alloclimit, uint64_t instructionlimit)
+{
+    sb_lua_state *sb = SCMalloc(sizeof(sb_lua_state));
+    if (sb == NULL) {
+        // Out of memory.  Error code?
+        return NULL;
+    }
+
+    sb->alloc_limit = alloclimit;
+    sb->alloc_bytes = 0;
+    sb->hook_instruction_count = 100;
+    sb->instruction_limit = instructionlimit;
+
+    sb->L = lua_newstate(sb_alloc, sb); /* create state */
+    if (sb->L == NULL) {
+        // TODO: log or error code?
+        free(sb);
+        return NULL;
+    }
+
+    lua_pushstring(sb->L, SANDBOX_CTX);
+    lua_pushlightuserdata(sb->L, sb);
+    lua_settable(sb->L, LUA_REGISTRYINDEX);
+
+    lua_sethook(sb->L, sb_hook, LUA_MASKCOUNT, sb->hook_instruction_count);
+    return sb->L;
+}
+
+static sb_lua_state *sb_get_ctx(lua_State *L)
+{
+    lua_pushstring(L, SANDBOX_CTX);
+    lua_gettable(L, LUA_REGISTRYINDEX);
+    sb_lua_state *ctx = lua_touserdata(L, -1);
+    // TODO:  log if null?
+    lua_pop(L, 1);
+    return ctx;
+}
+
+void sb_close(lua_State *L)
+{
+    sb_lua_state *sb = sb_get_ctx(L);
+    lua_close(sb->L);
+    SCFree(sb);
+}
+
+static void sb_hook(lua_State *L, lua_Debug *ar)
+{
+    (void)ar;
+    sb_lua_state *sb = sb_get_ctx(L);
+
+    sb->instruction_count += sb->hook_instruction_count;
+
+    if (sb->instruction_limit > 0 && sb->instruction_count > sb->instruction_limit) {
+        // TODO: do we care enough for a full traceback here?
+        luaL_error(L, "Instruction limit exceeded");
+    }
+}
+
+void sb_resetinstructioncounter(lua_State *L)
+{
+    sb_lua_state *sb = sb_get_ctx(L);
+    if (sb != NULL) {
+        sb->instruction_count = 0;
+        lua_sethook(L, sb_hook, LUA_MASKCOUNT, sb->hook_instruction_count);
+    }
+}
+
+void sb_setinstructionlimit(lua_State *L, uint64_t instruction_limit)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        ctx->instruction_limit = instruction_limit;
+    }
+}
+
+static uint64_t sb_getinstructioncount(lua_State *L)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        return ctx->instruction_count;
+    }
+    return 0;
+}
+
+static int sb_Ltotalalloc(lua_State *L)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        lua_pushinteger(L, ctx->alloc_bytes);
+    } else {
+        lua_pushinteger(L, 0);
+    }
+    return 1;
+}
+
+static int sb_Lgetalloclimit(lua_State *L)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        lua_pushinteger(L, ctx->alloc_limit);
+    } else {
+        lua_pushinteger(L, 0);
+    }
+    return 1;
+}
+
+static int sb_Lsetalloclimit(lua_State *L)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        ctx->alloc_limit = luaL_checkinteger(L, 1);
+    }
+    return 0;
+}
+
+/*
+static int sb_Lsetlevel(lua_State *L)
+{
+    lua_Integer level = luaL_checkinteger(L, 1);
+    lua_pushnil(L);
+    lua_setglobal(L, "debug");
+    return 0;
+}
+*/
+
+static int sb_Lgetinstructioncount(lua_State *L)
+{
+    lua_pushinteger(L, sb_getinstructioncount(L));
+    return 1;
+}
+
+static int sb_Lgetinstructionlimit(lua_State *L)
+{
+    sb_lua_state *ctx = sb_get_ctx(L);
+    if (ctx != NULL) {
+        lua_pushinteger(L, ctx->instruction_limit);
+    } else {
+        lua_pushinteger(L, 0);
+    }
+    return 1;
+}
+
+static int sb_Lsetinstructionlimit(lua_State *L)
+{
+    sb_setinstructionlimit(L, luaL_checkinteger(L, 1));
+    return 0;
+}
+
+static int sb_Lresetinstructioncount(lua_State *L)
+{
+    sb_resetinstructioncounter(L);
+    return 0;
+}
+
+static const luaL_Reg sblib[] = { { "totalalloc", sb_Ltotalalloc },
+    { "getalloclimit", sb_Lgetalloclimit }, { "setalloclimit", sb_Lsetalloclimit },
+    // { "setlevel", sb_Lsetlevel },
+    { "instructioncount", sb_Lgetinstructioncount },
+    { "getinstructionlimit", sb_Lgetinstructionlimit },
+    { "setinstructionlimit", sb_Lsetinstructionlimit },
+    { "resetinstructioncount", sb_Lresetinstructioncount }, { NULL, NULL } };
+
+LUAMOD_API int luaopen_sandbox(lua_State *L)
+{
+    luaL_newlib(L, sblib);
+    return 1;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/util-lua-sandbox.h b/src/util-lua-sandbox.h
new file mode 100644 (file)
index 0000000..42ad7fa
--- /dev/null
@@ -0,0 +1,98 @@
+/* Copyright (C) 2014-2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Jo Johnson <pyrojoe314@gmail.com>
+ */
+
+#ifndef __UTIL_LUA_SANDBOX_H__
+#define __UTIL_LUA_SANDBOX_H__
+
+#ifndef HAVE_LUA
+/* If we don't have Lua, create a typedef for sb_lua_State so the
+ * exported Lua functions don't fail the build. */
+typedef void sb_lua_state;
+#else
+#include <stdlib.h>
+#include "lua.h"
+
+#if !defined(SANDBOX_ALLOC_CTX)
+#define SANDBOX_CTX "SANDBOX_CTX"
+#endif
+
+/*
+ *  Lua sandbox usage:  The only needed changes to use the sandboxed lua state are
+ *      to replace calls to lua_newstate and lua_close with sb_newstate and sb_close
+ *      Additionally, sb_loadrestricted can be used to load a restricted set of packages
+ *      that prevent side effecting outside of the lua runtime
+ */
+
+/*
+ *  Struct to store a lua_state and the additional metadata required to sandbox it
+ */
+typedef struct sb_lua_state {
+    lua_State *L;
+
+    // Allocation limits
+    uint64_t alloc_bytes;
+    uint64_t alloc_limit;
+
+    // Execution Limits
+    uint64_t instruction_count;
+    uint64_t instruction_limit;
+    uint64_t hook_instruction_count;
+} sb_lua_state;
+
+enum sb_level { NONE, EXTERNAL_RESTRICTED, PERFORMANCE_RESTRICTED };
+
+/*
+ *  Replaces luaL_newstate.  Sets an upper bound for allocations and bytecode
+ *      instructions for the lua runtime on this state.
+ *
+ *  alloclimit - maximium number of bytes lua can allocate before receiving out of memory.
+ *      A value of zero will not limit allocations
+ *  instructionlimit - maximum number of lua bytecode instructions before an error is thrown
+ *      A value of zero will not limit the number of instructions
+ */
+lua_State *sb_newstate(uint64_t alloclimit, uint64_t instructionlimit);
+
+/*
+ *  Replaces lua_close.  Handles freeing the sb_lua_state
+ */
+void sb_close(lua_State *sb);
+
+/*
+ *  Resets the instruction counter for the sandbox to 0
+ */
+void sb_resetinstructioncounter(lua_State *sb);
+
+/*
+ *  Sets the maximum number of lua instructions before erroring out
+ */
+void sb_setinstructionlimit(lua_State *L, uint64_t instruction_limit);
+
+/*
+ *  Replaces luaL_openlibs.  Only opens allowed paackages for the sandbox and
+ *  masks out dangerous functions from the base.
+ */
+LUALIB_API void sb_loadrestricted(lua_State *L);
+
+#endif /* HAVE_LUA */
+
+#endif /*  __UTIL_LUA_SANDBOX_H__ */
\ No newline at end of file
index d903f44e39df0990e04645c32f34bda34d55d430..900e66401de476af0e1837925b5caa5ec7cfe0e3 100644 (file)
@@ -54,6 +54,7 @@
 #include <lauxlib.h>
 
 #include "util-lua.h"
+#include "util-lua-sandbox.h"
 
 lua_State *LuaGetState(void)
 {