From 122132a8a00fc373d359cf6beb831b5fdc040e72 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Tue, 6 Feb 2018 14:11:31 -0500 Subject: [PATCH] Sanify handling of array keys When passing an array from PHP to Lua, stringify integer array keys beyond the range a lua_Number can represent. When passing a table from Lua to PHP, * Convert integers encoded as strings to actual integers * Convert numbers outside of PHP's int range to strings * Detect collisions, e.g. { [0] = 'foo', ["0"] = 'bar' } Bug: T186240 Change-Id: I386245966529fc109c44b24f8a0209bdbfa30ffc --- data_conversion.c | 81 +++++++++++++++++++++++++++++---- tests/array-key-conversion.phpt | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 tests/array-key-conversion.phpt diff --git a/data_conversion.c b/data_conversion.c index 798fe71..c466402 100644 --- a/data_conversion.c +++ b/data_conversion.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "php.h" #include "php_luasandbox.h" @@ -210,6 +211,10 @@ void luasandbox_push_zval_userdata(lua_State * L, zval * z) */ static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable * recursionGuard) { +#if SIZEOF_LONG > 4 + char buffer[MAX_LENGTH_OF_LONG + 1]; +#endif + // Recursion requires an arbitrary amount of stack space so we have to // check the stack. luaL_checkstack(L, 10, "converting PHP array to Lua"); @@ -232,6 +237,17 @@ static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable * int key_type = zend_hash_get_current_key_ex(ht, &key, &key_length, &lkey, 0, &p); zend_hash_get_current_data_ex(ht, (void**)&value, &p); + + // Lua doesn't represent most integers with absolute value over 2**53, + // so stringify them. +#if SIZEOF_LONG > 4 + if (key_type == HASH_KEY_IS_LONG && + ((int64_t)lkey > INT64_C(9007199254740992) || (int64_t)lkey < INT64_C(-9007199254740992)) + ) { + key_length = snprintf(buffer, sizeof(buffer), "%" PRId64, (int64_t)lkey); + lua_pushlstring(L, buffer, key_length - 1); + } else +#endif if (key_type == HASH_KEY_IS_STRING) { lua_pushlstring(L, key, key_length - 1); } else { @@ -253,6 +269,16 @@ static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable * zval *value; ZEND_HASH_FOREACH_KEY_VAL(ht, lkey, key, value) { + // Lua doesn't represent most integers with absolute value over 2**53, + // so stringify them. +#if SIZEOF_LONG > 4 + if (!key && + ((int64_t)lkey > INT64_C(9007199254740992) || (int64_t)lkey < INT64_C(-9007199254740992)) + ) { + size_t len = snprintf(buffer, sizeof(buffer), "%" PRId64, (int64_t)lkey); + lua_pushlstring(L, buffer, len); + } else +#endif if (key) { lua_pushlstring(L, ZSTR_VAL(key), ZSTR_LEN(key)); } else { @@ -533,6 +559,7 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L, const char * str; size_t length; lua_Number n; + zend_ulong zn; #if PHP_VERSION_ID < 70000 zval *value, *valp; @@ -553,14 +580,9 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L, // Convert key, but leave it there if (lua_type(L, -1) == LUA_TNUMBER) { n = lua_tonumber(L, -1); - if (isfinite(n) && n == floor(n)) { - // Integer key -#if PHP_VERSION_ID < 70000 - zend_hash_index_update(ht, n, (void*)&value, sizeof(zval*), NULL); -#else - zend_hash_index_update(ht, n, valp); -#endif - return 1; + if (isfinite(n) && n == floor(n) && n >= LONG_MIN && n <= LONG_MAX) { + zn = (long)n; + goto add_int_key; } } @@ -575,7 +597,31 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L, ); luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC); efree(message); + return 0; + } + lua_pop(L, 1); + // See if the string is convertable to a number +#if PHP_VERSION_ID < 70000 + ZEND_HANDLE_NUMERIC_EX(str, length + 1, zn, goto add_int_key); +#else + if (ZEND_HANDLE_NUMERIC_STR(str, length, zn)) { + goto add_int_key; + } +#endif + + // Nope, use it as a string +#if PHP_VERSION_ID < 70000 + if (zend_hash_exists(ht, str, length + 1)) +#else + if (zend_hash_str_exists(ht, str, length)) +#endif + { + // Collision, probably the key is an integer-like string + char *message; + spprintf(&message, 0, "Collision for array key %s when passing data from Lua to PHP", str ); + luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC); + efree(message); return 0; } #if PHP_VERSION_ID < 70000 @@ -583,7 +629,24 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L, #else zend_hash_str_update(ht, str, length, valp); #endif - lua_pop(L, 1); + return 1; + +add_int_key: + if (zend_hash_index_exists(ht, zn)) { + // Collision, probably with a integer-like string + char *message; + spprintf(&message, 0, "Collision for array key %" PRId64 " when passing data from Lua to PHP", + (int64_t)zn + ); + luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC); + efree(message); + return 0; + } +#if PHP_VERSION_ID < 70000 + zend_hash_index_update(ht, zn, (void*)&value, sizeof(zval*), NULL); +#else + zend_hash_index_update(ht, zn, valp); +#endif return 1; } /* }}} */ diff --git a/tests/array-key-conversion.phpt b/tests/array-key-conversion.phpt new file mode 100644 index 0000000..ee74574 --- /dev/null +++ b/tests/array-key-conversion.phpt @@ -0,0 +1,80 @@ +--TEST-- +Array key conversion +--FILE-- +setMemoryLimit( 100000 ); + $sandbox->setCPULimit( 0.1 ); + try { + $ret = $sandbox + ->loadString( 'local t, r = ..., {}; for k, v in pairs( t ) do r[v] = type(k) end return r' ) + ->call( $array ); + if ( is_array( $ret[0] ) ) { + ksort( $ret[0], SORT_STRING ); + } + printf( "%s\n", preg_replace( '/\s+/', ' ', var_export( $ret[0], 1 ) ) ); + } catch ( LuaSandboxError $e ) { + printf( "EXCEPTION: %s\n", $e->getMessage() ); + } +} + +function testLuaToPhp( $test, $lua ) { + printf( "Lua→PHP %-30s ", "$test:" ); + + $sandbox = new LuaSandbox; + $sandbox->setMemoryLimit( 100000 ); + $sandbox->setCPULimit( 0.1 ); + try { + $ret = $sandbox->loadString( "return { $lua }" )->call(); + if ( is_array( $ret[0] ) ) { + ksort( $ret[0], SORT_STRING ); + } + printf( "%s\n", preg_replace( '/\s+/', ' ', var_export( $ret[0], 1 ) ) ); + } catch ( LuaSandboxError $e ) { + printf( "EXCEPTION: %s\n", $e->getMessage() ); + } +} + +if ( PHP_INT_MAX > 9007199254740992 ) { + $a = [ + '9007199254740992' => 'max', '9007199254740993' => 'max+1', + '-9007199254740992' => 'min', '-9007199254740993' => 'min-1', + ]; + $max = '9223372036854775807'; + $max2 = '9223372036854775808'; + $min = '-9223372036854775808'; + $min2 = '-9223372036854775809'; +} else { + $a = [ + '2147483647' => 'max', '2147483648' => 'max+1', + '-2147483648' => 'min', '-2147483649' => 'min-1', + ]; + $max = '2147483647'; + $max2 = '2147483648'; + $min = '-2147483648'; + $min2 = '-2147483649'; +} + +testPhpToLua( 'simple integers', [ -10 => 'minus ten', 0 => 'zero', 10 => 'ten' ] ); +testPhpToLua( 'maximal values', $a ); + +testLuaToPhp( 'simple integers', '[-10] = "minus ten", [0] = "zero", [10] = "ten"' ); +testLuaToPhp( 'stringified integers', '["-10"] = "minus ten", ["0"] = "zero", ["10"] = "ten"' ); +testLuaToPhp( 'maximal integers', "['$max'] = 'max', ['$max2'] = 'max+1', ['$min'] = 'min', ['$min2'] = 'min-1'" ); +testLuaToPhp( 'collision (0)', '[0] = "number zero", ["0"] = "string zero"' ); +testLuaToPhp( 'collision (float)', '[1.5] = "number 1.5", ["1.5"] = "string 1.5"' ); +testLuaToPhp( 'collision (inf)', '[1/0] = "number inf", ["inf"] = "string inf"' ); + +--EXPECTF-- +PHP→Lua simple integers: array ( 'minus ten' => 'number', 'ten' => 'number', 'zero' => 'number', ) +PHP→Lua maximal values: array ( 'max' => 'number', 'max+1' => 'string', 'min' => 'number', 'min-1' => 'string', ) +Lua→PHP simple integers: array ( -10 => 'minus ten', 0 => 'zero', 10 => 'ten', ) +Lua→PHP stringified integers: array ( -10 => 'minus ten', 0 => 'zero', 10 => 'ten', ) +Lua→PHP maximal integers: array ( -%d => 'min', '-%d' => 'min-1', %d => 'max', '%d' => 'max+1', ) +Lua→PHP collision (0): EXCEPTION: Collision for array key 0 when passing data from Lua to PHP +Lua→PHP collision (float): EXCEPTION: Collision for array key 1.5 when passing data from Lua to PHP +Lua→PHP collision (inf): EXCEPTION: Collision for array key inf when passing data from Lua to PHP