LCOV - code coverage report
Current view: top level - src - library.c (source / functions) Hit Total Coverage
Test: mediawiki/php/luasandbox test coverage report Lines: 142 192 74.0 %
Date: 2023-12-12 07:35:14 Functions: 11 14 78.6 %

          Line data    Source code
       1             : /**
       2             :  * This file holds the library of functions which are written in C and exposed
       3             :  * to Lua code, and the code which manages registration of both the custom
       4             :  * library and the parts of the standard Lua library which we allow.
       5             :  */
       6             : 
       7             : #ifdef HAVE_CONFIG_H
       8             : #include "config.h"
       9             : #endif
      10             : 
      11             : #include <lua.h>
      12             : #include <lauxlib.h>
      13             : #include <lualib.h>
      14             : #include <math.h>
      15             : 
      16             : #include "php.h"
      17             : #include "php_luasandbox.h"
      18             : 
      19             : #ifdef LUASANDBOX_NO_CLOCK
      20             : #include <time.h>
      21             : #endif
      22             : 
      23             : static void luasandbox_lib_filter_table(lua_State * L, char ** member_names);
      24             : static HashTable * luasandbox_lib_get_allowed_globals();
      25             : 
      26             : static int luasandbox_base_tostring(lua_State * L);
      27             : static int luasandbox_math_random(lua_State * L);
      28             : static int luasandbox_math_randomseed(lua_State * L);
      29             : static int luasandbox_base_pcall(lua_State * L);
      30             : static int luasandbox_base_xpcall(lua_State *L);
      31             : static int luasandbox_os_clock(lua_State * L);
      32             : 
      33             : #if LUA_VERSION_NUM < 502
      34             : static int luasandbox_base_pairs(lua_State *L);
      35             : static int luasandbox_base_ipairs(lua_State *L);
      36             : #endif
      37             : 
      38             : /**
      39             :  * Allowed global variables. Omissions are:
      40             :  *   * pcall, xpcall: We have our own versions which don't allow interception of
      41             :  *     timeout etc. errors.
      42             :  *   * loadfile: insecure.
      43             :  *   * load, loadstring: Probably creates a protected environment so has
      44             :  *     the same problem as pcall. Also omitting these makes analysis of the
      45             :  *     code for runtime etc. feasible.
      46             :  *   * print: Not compatible with a sandbox environment
      47             :  *   * tostring: Provides addresses of tables and functions, which provides an
      48             :  *     easy ASLR workaround or heap address discovery mechanism for a memory
      49             :  *     corruption exploit. We have our own version.
      50             :  *   * Any new or undocumented functions like newproxy.
      51             :  *   * package: cpath, loadlib etc. are insecure.
      52             :  *   * coroutine: Not useful for our application so unreviewed at present.
      53             :  *   * io, file, os: insecure
      54             :  *   * debug: Provides various ways to break the sandbox, such as setupvalue()
      55             :  *     and getregistry().
      56             :  */
      57             : char * luasandbox_allowed_globals[] = {
      58             :     // base
      59             :     "assert",
      60             :     "error",
      61             :     "getfenv",
      62             :     "getmetatable",
      63             :     "ipairs",
      64             :     "next",
      65             :     "pairs",
      66             :     "rawequal",
      67             :     "rawget",
      68             :     "rawset",
      69             :     "select",
      70             :     "setfenv",
      71             :     "setmetatable",
      72             :     "tonumber",
      73             :     "type",
      74             :     "unpack",
      75             :     "_G",
      76             :     "_VERSION",
      77             :     // libs
      78             :     "string",
      79             :     "table",
      80             :     "math",
      81             :     "os",
      82             :     "debug",
      83             :     NULL
      84             : };
      85             : 
      86             : char * luasandbox_allowed_os_members[] = {
      87             :     "date",
      88             :     "difftime",
      89             :     "time",
      90             :     NULL
      91             : };
      92             : 
      93             : char * luasandbox_allowed_debug_members[] = {
      94             :     "traceback",
      95             :     NULL
      96             : };
      97             : 
      98             : 
      99             : 
     100             : ZEND_EXTERN_MODULE_GLOBALS(luasandbox);
     101             : 
     102             : /** {{{  luasandbox_lib_register
     103             :  */
     104         126 : void luasandbox_lib_register(lua_State * L)
     105             : {
     106             :     // Load the standard libraries that we need
     107         126 :     lua_pushcfunction(L, luaopen_base);
     108         126 :     lua_call(L, 0, 0);
     109         126 :     lua_pushcfunction(L, luaopen_table);
     110         126 :     lua_call(L, 0, 0);
     111         126 :     lua_pushcfunction(L, luaopen_math);
     112         126 :     lua_call(L, 0, 0);
     113         126 :     lua_pushcfunction(L, luaopen_debug);
     114         126 :     lua_call(L, 0, 0);
     115         125 :     lua_pushcfunction(L, luaopen_os);
     116         125 :     lua_call(L, 0, 0);
     117             : 
     118             :     // Install our own string library
     119         125 :     lua_pushcfunction(L, luasandbox_open_string);
     120         125 :     lua_call(L, 0, 0);
     121             : 
     122             :     // Filter the os library
     123         125 :     lua_getglobal(L, "os");
     124         125 :     luasandbox_lib_filter_table(L, luasandbox_allowed_os_members);
     125         125 :     lua_setglobal(L, "os");
     126             : 
     127             :     // Filter the debug library
     128         125 :     lua_getglobal(L, "debug");
     129         125 :     luasandbox_lib_filter_table(L, luasandbox_allowed_debug_members);
     130         125 :     lua_setglobal(L, "debug");
     131             : 
     132             :     // Remove any globals that aren't in a whitelist. This is mostly to remove
     133             :     // unsafe functions from the base library.
     134         125 :     lua_pushnil(L);
     135        4500 :     while (lua_next(L, LUA_GLOBALSINDEX) != 0) {
     136             :         const char * key;
     137             :         size_t key_len;
     138        4375 :         lua_pop(L, 1);
     139        4375 :         if (lua_type(L, -1) != LUA_TSTRING) {
     140           0 :             continue;
     141             :         }
     142        4375 :         key = lua_tolstring(L, -1, &key_len);
     143        8750 :         if (!zend_hash_str_exists(luasandbox_lib_get_allowed_globals(), key, key_len)) {
     144             :             // Not allowed, delete it
     145        1500 :             lua_pushnil(L);
     146        1500 :             lua_setglobal(L, key);
     147             :         }
     148             :     }
     149             : 
     150             :     // Install our own versions of tostring, pcall, xpcall
     151         125 :     lua_pushcfunction(L, luasandbox_base_tostring);
     152         125 :     lua_setglobal(L, "tostring");
     153         125 :     lua_pushcfunction(L, luasandbox_base_pcall);
     154         125 :     lua_setglobal(L, "pcall");
     155         125 :     lua_pushcfunction(L, luasandbox_base_xpcall);
     156         125 :     lua_setglobal(L, "xpcall");
     157             : 
     158             :     // Remove string.dump: may expose private data
     159         125 :     lua_getglobal(L, "string");
     160         125 :     lua_pushnil(L);
     161         125 :     lua_setfield(L, -2, "dump");
     162         125 :     lua_pop(L, 1);
     163             : 
     164             :     // Install our own versions of math.random and math.randomseed
     165         125 :     lua_getglobal(L, "math");
     166         125 :     lua_pushcfunction(L, luasandbox_math_random);
     167         125 :     lua_setfield(L, -2, "random");
     168         125 :     lua_pushcfunction(L, luasandbox_math_randomseed);
     169         125 :     lua_setfield(L, -2, "randomseed");
     170         125 :     lua_pop(L, 1);
     171             : 
     172             :     // Install our own version of os.clock(), which uses our high-resolution
     173             :     // usage timer
     174         125 :     lua_getglobal(L, "os");
     175         125 :     lua_pushcfunction(L, luasandbox_os_clock);
     176         125 :     lua_setfield(L, -2, "clock");
     177         125 :     lua_pop(L, 1);
     178             : 
     179             :     // Install our own versions of pairs and ipairs, if necessary
     180             : #if LUA_VERSION_NUM < 502
     181         125 :     lua_getfield(L, LUA_GLOBALSINDEX, "pairs");
     182         125 :     lua_setfield(L, LUA_REGISTRYINDEX, "luasandbox_old_pairs");
     183         125 :     lua_getfield(L, LUA_GLOBALSINDEX, "ipairs");
     184         125 :     lua_setfield(L, LUA_REGISTRYINDEX, "luasandbox_old_ipairs");
     185         125 :     lua_pushcfunction(L, luasandbox_base_pairs);
     186         125 :     lua_setglobal(L, "pairs");
     187         125 :     lua_pushcfunction(L, luasandbox_base_ipairs);
     188         125 :     lua_setglobal(L, "ipairs");
     189             : #endif
     190         125 : }
     191             : /* }}} */
     192             : 
     193             : /** {{{ luasandbox_lib_filter_table
     194             :  *
     195             :  * Make a copy of the table at the top of the stack, and remove any members
     196             :  * from it that aren't in the given whitelist.
     197             :  */
     198         250 : static void luasandbox_lib_filter_table(lua_State * L, char ** member_names)
     199             : {
     200             :     int i, n;
     201         250 :     int si = lua_gettop(L);
     202         750 :     for (n = 0; member_names[n]; n++);
     203         250 :     lua_createtable(L, 0, n);
     204         750 :     for (i = 0; member_names[i]; i++) {
     205         500 :         lua_getfield(L, si, member_names[i]);
     206         500 :         lua_setfield(L, si+1, member_names[i]);
     207             :     }
     208         250 :     lua_replace(L, si);
     209         250 : }
     210             : /* }}} */
     211             : 
     212             : /** {{{ luasandbox_lib_destroy_globals */
     213          23 : void luasandbox_lib_destroy_globals()
     214             : {
     215          23 :     if (LUASANDBOX_G(allowed_globals)) {
     216          19 :         zend_hash_destroy(LUASANDBOX_G(allowed_globals));
     217          19 :         FREE_HASHTABLE(LUASANDBOX_G(allowed_globals));
     218          19 :         LUASANDBOX_G(allowed_globals) = NULL;
     219             :     }
     220          23 : }
     221             : /* }}} */
     222             : 
     223             : /** {{{ luasandbox_lib_get_allowed_globals
     224             :  *
     225             :  * Get a HashTable of allowed global variables
     226             :  */
     227        4375 : static HashTable * luasandbox_lib_get_allowed_globals()
     228             : {
     229             :     int i, n;
     230        4375 :     if (LUASANDBOX_G(allowed_globals)) {
     231        4356 :         return LUASANDBOX_G(allowed_globals);
     232             :     }
     233             : 
     234         456 :     for (n = 0; luasandbox_allowed_globals[n]; n++);
     235             : 
     236          19 :     ALLOC_HASHTABLE(LUASANDBOX_G(allowed_globals));
     237          19 :     zend_hash_init(LUASANDBOX_G(allowed_globals), n, NULL, NULL, 0);
     238             : 
     239             :     zval zv;
     240          19 :     ZVAL_TRUE(&zv);
     241             : 
     242         456 :     for (i = 0; luasandbox_allowed_globals[i]; i++) {
     243         874 :         zend_hash_str_update(LUASANDBOX_G(allowed_globals),
     244         437 :             luasandbox_allowed_globals[i], strlen(luasandbox_allowed_globals[i]), &zv);
     245             :     }
     246             : 
     247          19 :     return LUASANDBOX_G(allowed_globals);
     248             : }
     249             : /* }}} */
     250             : 
     251             : /** {{{ luasandbox_base_tostring
     252             :  *
     253             :  * This is identical to luaB_tostring except that it does not call lua_topointer().
     254             :  */
     255           0 : static int luasandbox_base_tostring(lua_State * L)
     256             : {
     257           0 :     luaL_checkany(L, 1);
     258           0 :     if (luaL_callmeta(L, 1, "__tostring"))  /* is there a metafield? */
     259           0 :         return 1;  /* use its value */
     260           0 :     switch (lua_type(L, 1)) {
     261           0 :         case LUA_TNUMBER:
     262           0 :             lua_pushstring(L, lua_tostring(L, 1));
     263           0 :             break;
     264           0 :         case LUA_TSTRING:
     265           0 :             lua_pushvalue(L, 1);
     266           0 :             break;
     267           0 :         case LUA_TBOOLEAN:
     268           0 :             lua_pushstring(L, (lua_toboolean(L, 1) ? "true" : "false"));
     269           0 :             break;
     270           0 :         case LUA_TNIL:
     271           0 :             lua_pushliteral(L, "nil");
     272           0 :             break;
     273           0 :         default:
     274           0 :             lua_pushfstring(L, "%s", luaL_typename(L, 1));
     275           0 :             break;
     276             :     }
     277           0 :     return 1;
     278             : }
     279             : /* }}} */
     280             : 
     281             : /** {{{ luasandbox_math_random
     282             :  *
     283             :  * A math.random implementation that doesn't share state with PHP's rand()
     284             :  */
     285           0 : static int luasandbox_math_random(lua_State * L)
     286             : {
     287           0 :     php_luasandbox_obj * sandbox = luasandbox_get_php_obj(L);
     288             : 
     289             : #ifdef PHP_WIN32
     290             :     // MSVC does not provide rand_r(). Note that srand/rand still does not
     291             :     // share state with PHP's rand() because PHP's rand() is an alias for mt_rand()
     292             :     // (and does not use C srand/rand) as of PHP 7.1.
     293             :     int i = rand();
     294             : #else
     295           0 :     int i = rand_r(&sandbox->random_seed);
     296             : #endif
     297             : 
     298           0 :     if (i >= RAND_MAX) {
     299           0 :         i -= RAND_MAX;
     300             :     }
     301           0 :     lua_Number r = (lua_Number)i / (lua_Number)RAND_MAX;
     302           0 :     switch (lua_gettop(L)) {  /* check number of arguments */
     303           0 :         case 0: {  /* no arguments */
     304           0 :             lua_pushnumber(L, r);  /* Number between 0 and 1 */
     305           0 :             break;
     306             :         }
     307           0 :         case 1: {  /* only upper limit */
     308           0 :             int u = luaL_checkint(L, 1);
     309           0 :             luaL_argcheck(L, 1<=u, 1, "interval is empty");
     310           0 :             lua_pushnumber(L, floor(r*u)+1);  /* int between 1 and `u' */
     311           0 :             break;
     312             :         }
     313           0 :         case 2: {  /* lower and upper limits */
     314           0 :             int l = luaL_checkint(L, 1);
     315           0 :             int u = luaL_checkint(L, 2);
     316           0 :             luaL_argcheck(L, l<=u, 2, "interval is empty");
     317           0 :             lua_pushnumber(L, floor(r*(u-l+1))+l);  /* int between `l' and `u' */
     318           0 :             break;
     319             :         }
     320           0 :         default: return luaL_error(L, "wrong number of arguments");
     321             :     }
     322           0 :     return 1;
     323             : }
     324             : /* }}} */
     325             : 
     326             : /** {{{ luasandbox_math_randomseed
     327             :  *
     328             :  * Set the seed for the custom math.random.
     329             :  */
     330           0 : static int luasandbox_math_randomseed(lua_State * L)
     331             : {
     332           0 :     php_luasandbox_obj * sandbox = luasandbox_get_php_obj(L);
     333           0 :     sandbox->random_seed = (unsigned int)luaL_checkint(L, 1);
     334             : #ifdef PHP_WIN32
     335             :     srand(sandbox->random_seed);
     336             : #endif
     337           0 :     return 0;
     338             : }
     339             : /* }}} */
     340             : 
     341             : /** {{{ luasandbox_lib_rethrow_fatal
     342             :  *
     343             :  * If the error on the top of the stack with the error return code given as a
     344             :  * parameter is a fatal, rethrow the error. If the error is rethrown, the
     345             :  * function does not return.
     346             :  */
     347          26 : static void luasandbox_lib_rethrow_fatal(lua_State * L, int status)
     348             : {
     349          26 :     switch (status) {
     350           2 :         case 0:
     351             :             // No error
     352           2 :             return;
     353          15 :         case LUA_ERRRUN:
     354             :             // A runtime error which we can rethrow in a normal way
     355          15 :             if (luasandbox_is_fatal(L, -1)) {
     356          10 :                 lua_error(L);
     357             :             }
     358           5 :             break;
     359           9 :         case LUA_ERRMEM:
     360             :         case LUA_ERRERR:
     361             :             // Lua doesn't provide a public API for rethrowing these, so we
     362             :             // have to convert them to our own fatal error type
     363           9 :             if (!luasandbox_is_fatal(L, -1)) {
     364           8 :                 luasandbox_wrap_fatal(L);
     365             :             }
     366           9 :             lua_error(L);
     367           0 :             break; // not reached
     368             :     }
     369             : }
     370             : /* }}} */
     371             : 
     372             : /** {{{ luasandbox_lib_error_wrapper
     373             :  *
     374             :  * Wrapper for the xpcall error function
     375             :  */
     376          15 : static int luasandbox_lib_error_wrapper(lua_State * L)
     377             : {
     378             :     int status;
     379          15 :     luaL_checkany(L, 1);
     380             : 
     381             :     // This function is only called from luaG_errormsg(), which will later
     382             :     // unconditionally set the status code to LUA_ERRRUN, so we can assume
     383             :     // that the error type is equivalent to LUA_ERRRUN.
     384          15 :     if (luasandbox_is_fatal(L, 1)) {
     385             :         // Just return to whatever called lua_pcall(), don't call the user
     386             :         // function
     387           7 :         return lua_gettop(L);
     388             :     }
     389             : 
     390             :     // Put the user error function at the bottom of the stack
     391           8 :     lua_pushvalue(L, lua_upvalueindex(1));
     392           8 :     lua_insert(L, 1);
     393             :     // Call it, passing through the arguments to this function
     394           8 :     status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0);
     395           8 :     if (status != 0) {
     396           6 :         luasandbox_lib_rethrow_fatal(L, LUA_ERRERR);
     397             :     }
     398           2 :     return lua_gettop(L);
     399             : }
     400             : /* }}} */
     401             : 
     402             : /** {{{ luasandbox_base_pcall
     403             :  *
     404             :  * This is our implementation of the Lua function pcall(). It allows Lua code
     405             :  * to handle its own errors, but forces internal errors to be rethrown.
     406             :  */
     407           9 : static int luasandbox_base_pcall(lua_State * L)
     408             : {
     409             :     int status;
     410           9 :     luaL_checkany(L, 1);
     411           9 :     status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0);
     412           9 :     luasandbox_lib_rethrow_fatal(L, status);
     413           4 :     lua_pushboolean(L, (status == 0));
     414           4 :     lua_insert(L, 1);
     415           4 :     return lua_gettop(L);  // return status + all results
     416             : }
     417             : /* }}} */
     418             : 
     419             : /** {{{ luasandbox_base_xpcall
     420             :  *
     421             :  * This is our implementation of the Lua function xpcall(). It allows Lua code
     422             :  * to handle its own errors, but forces internal errors to be rethrown.
     423             :  */
     424          11 : static int luasandbox_base_xpcall (lua_State *L)
     425             : {
     426             :     int status;
     427          11 :     luaL_checkany(L, 2);
     428          11 :     lua_settop(L, 2);
     429             : 
     430             :     // We wrap the error function in a C closure. The error function already
     431             :     // happens to be at the top of the stack, so we don't need to push it before
     432             :     // calling lua_pushcfunction to make it an upvalue
     433          11 :     lua_pushcclosure(L, luasandbox_lib_error_wrapper, 1);
     434          11 :     lua_insert(L, 1);  // put error function under function to be called
     435             : 
     436          11 :     status = lua_pcall(L, 0, LUA_MULTRET, 1);
     437          11 :     luasandbox_lib_rethrow_fatal(L, status);
     438           3 :     lua_pushboolean(L, (status == 0));
     439           3 :     lua_replace(L, 1);
     440           3 :     return lua_gettop(L);  // return status + all results
     441             : }
     442             : /* }}} */
     443             : 
     444             : /** {{{ luasandbox_os_clock
     445             :  *
     446             :  * Implementation of os.clock() which uses our high-resolution usage timer,
     447             :  * if available, to provide an accurate measure of Lua CPU usage since a
     448             :  * particular LuaSandbox object was created.
     449             :  */
     450     1386474 : static int luasandbox_os_clock(lua_State * L)
     451             : {
     452             :     double clk;
     453             : 
     454             : #ifdef LUASANDBOX_NO_CLOCK
     455             :     clk = ((double)clock())/(double)CLOCKS_PER_SEC;
     456             : #else
     457             :     struct timespec ts;
     458     1386474 :     php_luasandbox_obj * sandbox = luasandbox_get_php_obj(L);
     459     1386474 :     luasandbox_timer_get_usage(&sandbox->timer, &ts);
     460     1386474 :     clk = ts.tv_sec + 1e-9 * ts.tv_nsec;
     461             : #endif
     462             : 
     463             :     // Reduce precision to 20μs to mitigate timing attacks
     464     1386474 :     clk = round(clk * 50000) / 50000;
     465             : 
     466     1386474 :     lua_pushnumber(L, (lua_Number)clk);
     467     1386474 :     return 1;
     468             : }
     469             : 
     470             : /* }}} */
     471             : 
     472             : #if LUA_VERSION_NUM < 502
     473             : /** {{{ luasandbox_base_pairs
     474             :  *
     475             :  * This is our implementation of the Lua function pairs(). It allows the Lua
     476             :  * 5.2 __pairs metamethod to override the standard behavior.
     477             :  */
     478           8 : static int luasandbox_base_pairs (lua_State *L)
     479             : {
     480           8 :     if (!luaL_getmetafield(L, 1, "__pairs")) {
     481           5 :         luaL_checktype(L, 1, LUA_TTABLE);
     482           4 :         lua_getfield(L, LUA_REGISTRYINDEX, "luasandbox_old_pairs");
     483             :     }
     484           7 :     lua_pushvalue(L, 1);
     485           7 :     lua_call(L, 1, 3);
     486           6 :     return 3;
     487             : }
     488             : /* }}} */
     489             : 
     490             : /** {{{ luasandbox_base_ipairs
     491             :  *
     492             :  * This is our implementation of the Lua function ipairs(). It allows the Lua
     493             :  * 5.2 __ipairs metamethod to override the standard behavior.
     494             :  */
     495           3 : static int luasandbox_base_ipairs (lua_State *L)
     496             : {
     497           3 :     if (!luaL_getmetafield(L, 1, "__ipairs")) {
     498           2 :         luaL_checktype(L, 1, LUA_TTABLE);
     499           1 :         lua_getfield(L, LUA_REGISTRYINDEX, "luasandbox_old_ipairs");
     500             :     }
     501           2 :     lua_pushvalue(L, 1);
     502           2 :     lua_call(L, 1, 3);
     503           2 :     return 3;
     504             : }
     505             : /* }}} */
     506             : #endif

Generated by: LCOV version 1.13