From 8ca67e832183b4cfb11202f92cab97f602d802d1 Mon Sep 17 00:00:00 2001 From: Aapo Talvensaari Date: Fri, 13 Dec 2024 17:56:26 +0200 Subject: [PATCH] refactor(sandbox): improve sandbox (#10900) --- .luacheckrc | 2 +- kong-3.10.0-0.rockspec | 11 + kong.conf.default | 10 +- kong/db/init.lua | 5 +- kong/db/migrations/state.lua | 2 +- kong/db/strategies/postgres/connector.lua | 2 +- kong/plugins/file-log/handler.lua | 5 +- kong/plugins/http-log/handler.lua | 4 +- kong/plugins/loggly/handler.lua | 5 +- kong/plugins/pre-function/_handler.lua | 19 +- kong/plugins/syslog/handler.lua | 5 +- kong/plugins/tcp-log/handler.lua | 5 +- kong/plugins/udp-log/handler.lua | 5 +- kong/tools/kong-lua-sandbox.lua | 181 +--------- kong/tools/sandbox.lua | 205 ----------- kong/tools/sandbox/environment/handler.lua | 199 +++++++++++ kong/tools/sandbox/environment/init.lua | 185 ++++++++++ kong/tools/sandbox/environment/lua.lua | 39 ++ kong/tools/sandbox/environment/schema.lua | 10 + kong/tools/sandbox/init.lua | 334 ++++++++++++++++++ kong/tools/sandbox/kong.lua | 125 +++++++ kong/tools/sandbox/require/handler.lua | 62 ++++ kong/tools/sandbox/require/init.lua | 48 +++ kong/tools/sandbox/require/lua.lua | 15 + kong/tools/sandbox/require/schema.lua | 13 + spec/01-unit/20-sandbox_spec.lua | 4 +- spec/03-plugins/18-acl/02-access_spec.lua | 12 +- .../36-request-transformer/02-access_spec.lua | 8 +- 28 files changed, 1071 insertions(+), 449 deletions(-) delete mode 100644 kong/tools/sandbox.lua create mode 100644 kong/tools/sandbox/environment/handler.lua create mode 100644 kong/tools/sandbox/environment/init.lua create mode 100644 kong/tools/sandbox/environment/lua.lua create mode 100644 kong/tools/sandbox/environment/schema.lua create mode 100644 kong/tools/sandbox/init.lua create mode 100644 kong/tools/sandbox/kong.lua create mode 100644 kong/tools/sandbox/require/handler.lua create mode 100644 kong/tools/sandbox/require/init.lua create mode 100644 kong/tools/sandbox/require/lua.lua create mode 100644 kong/tools/sandbox/require/schema.lua diff --git a/.luacheckrc b/.luacheckrc index 6bb537398eff..03e0b7c5a0b8 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -30,7 +30,7 @@ exclude_files = { "bazel-kong", } -files["kong/tools/kong-lua-sandbox.lua"] = { +files["kong/tools/sandbox/kong.lua"] = { read_globals = { "_ENV", "table.pack", diff --git a/kong-3.10.0-0.rockspec b/kong-3.10.0-0.rockspec index 2ba7c9e54f2c..9916f4d0ea25 100644 --- a/kong-3.10.0-0.rockspec +++ b/kong-3.10.0-0.rockspec @@ -217,6 +217,17 @@ build = { ["kong.tools.redis.schema"] = "kong/tools/redis/schema.lua", ["kong.tools.aws_stream"] = "kong/tools/aws_stream.lua", + ["kong.tools.sandbox"] = "kong/tools/sandbox/init.lua", + ["kong.tools.sandbox.kong"] = "kong/tools/sandbox/kong.lua", + ["kong.tools.sandbox.environment"] = "kong/tools/sandbox/environment/init.lua", + ["kong.tools.sandbox.environment.handler"] = "kong/tools/sandbox/environment/handler.lua", + ["kong.tools.sandbox.environment.lua"] = "kong/tools/sandbox/environment/lua.lua", + ["kong.tools.sandbox.environment.schema"] = "kong/tools/sandbox/environment/schema.lua", + ["kong.tools.sandbox.require"] = "kong/tools/sandbox/require/init.lua", + ["kong.tools.sandbox.require.handler"] = "kong/tools/sandbox/require/handler.lua", + ["kong.tools.sandbox.require.lua"] = "kong/tools/sandbox/require/lua.lua", + ["kong.tools.sandbox.require.schema"] = "kong/tools/sandbox/require/schema.lua", + ["kong.runloop.handler"] = "kong/runloop/handler.lua", ["kong.runloop.events"] = "kong/runloop/events.lua", ["kong.runloop.log_level"] = "kong/runloop/log_level.lua", diff --git a/kong.conf.default b/kong.conf.default index 9721e0d6449b..c711c98e11a6 100644 --- a/kong.conf.default +++ b/kong.conf.default @@ -1894,9 +1894,10 @@ # them. The sandboxed function has # restricted access to the global # environment and only has access - # to standard Lua functions that - # will generally not cause harm to - # the Kong Gateway node. + # to Kong PDK, OpenResty, and + # standard Lua functions that will + # generally not cause harm to the + # Kong Gateway node. # # * `on`: Functions have unrestricted # access to the global environment and @@ -1920,9 +1921,6 @@ # functions are not allowed, like: # `os.execute('rm -rf /*')`. # - # For a full allowed/disallowed list, see: - # https://github.com/kikito/sandbox.lua/blob/master/sandbox.lua - # # To customize the sandbox environment, use # the `untrusted_lua_sandbox_requires` and # `untrusted_lua_sandbox_environment` diff --git a/kong/db/init.lua b/kong/db/init.lua index edf44f2ac46d..78df157d82e1 100644 --- a/kong/db/init.lua +++ b/kong/db/init.lua @@ -102,7 +102,7 @@ function DB.new(kong_config, strategy) strategy = strategy, errors = errors, infos = connector:infos(), - kong_config = kong_config, + loaded_plugins = kong_config.loaded_plugins, -- left for MigrationsState.load } do @@ -444,8 +444,7 @@ do return nil, prefix_err(self, err) end - local ok, err = self.connector:schema_bootstrap(self.kong_config, - DEFAULT_LOCKS_TTL) + local ok, err = self.connector:schema_bootstrap(DEFAULT_LOCKS_TTL) self.connector:close() diff --git a/kong/db/migrations/state.lua b/kong/db/migrations/state.lua index a703a1fc1b38..ec22d9b9c617 100644 --- a/kong/db/migrations/state.lua +++ b/kong/db/migrations/state.lua @@ -184,7 +184,7 @@ function State.load(db) log.debug("loading subsystems migrations...") - local subsystems, err = load_subsystems(db, db.kong_config.loaded_plugins) + local subsystems, err = load_subsystems(db, db.loaded_plugins) if not subsystems then return nil, prefix_err(db, err) end diff --git a/kong/db/strategies/postgres/connector.lua b/kong/db/strategies/postgres/connector.lua index 4220ba01f2d1..0c5964f9230f 100644 --- a/kong/db/strategies/postgres/connector.lua +++ b/kong/db/strategies/postgres/connector.lua @@ -775,7 +775,7 @@ function _mt:schema_migrations() end -function _mt:schema_bootstrap(kong_config, default_locks_ttl) +function _mt:schema_bootstrap(default_locks_ttl) local conn = self:get_stored_connection() if not conn then error("no connection") diff --git a/kong/plugins/file-log/handler.lua b/kong/plugins/file-log/handler.lua index c7d4fe9a1954..b1e86b9311f4 100644 --- a/kong/plugins/file-log/handler.lua +++ b/kong/plugins/file-log/handler.lua @@ -24,9 +24,6 @@ local oflags = bit.bor(O_WRONLY, O_CREAT, O_APPEND) local mode = ffi.new("int", bit.bor(S_IRUSR, S_IWUSR, S_IRGRP, S_IROTH)) -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - - local C = ffi.C @@ -73,7 +70,7 @@ function FileLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/plugins/http-log/handler.lua b/kong/plugins/http-log/handler.lua index fd1d0cd48eeb..0e524cfe79de 100644 --- a/kong/plugins/http-log/handler.lua +++ b/kong/plugins/http-log/handler.lua @@ -16,8 +16,6 @@ local pairs = pairs local max = math.max -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - -- Create a function that concatenates multiple JSON objects into a JSON array. -- This saves us from rendering all entries into one large JSON string. -- Each invocation of the function returns the next bit of JSON, i.e. the opening @@ -183,7 +181,7 @@ function HttpLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/plugins/loggly/handler.lua b/kong/plugins/loggly/handler.lua index 39e70547e66f..7d408815dba7 100644 --- a/kong/plugins/loggly/handler.lua +++ b/kong/plugins/loggly/handler.lua @@ -14,9 +14,6 @@ local concat = table.concat local insert = table.insert -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - - local HOSTNAME = get_host_name() local SENDER_NAME = "kong" local LOG_LEVELS = { @@ -127,7 +124,7 @@ function LogglyLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/plugins/pre-function/_handler.lua b/kong/plugins/pre-function/_handler.lua index ebac8fada298..3c06421953da 100644 --- a/kong/plugins/pre-function/_handler.lua +++ b/kong/plugins/pre-function/_handler.lua @@ -1,24 +1,13 @@ -local resty_mlcache = require "kong.resty.mlcache" local sandbox = require "kong.tools.sandbox" local kong_meta = require "kong.meta" + -- handler file for both the pre-function and post-function plugin local config_cache do - local no_op = function() end - local shm_name = "kong_db_cache" - local cache_name = "serverless_" .. shm_name - local cache = resty_mlcache.new(cache_name, shm_name, { lru_size = 1e4 }) - local sandbox_kong = setmetatable({ - cache = cache, - configuration = kong.configuration.remove_sensitive() - }, { __index = kong }) - - local sandbox_opts = { env = { kong = sandbox_kong, ngx = ngx } } - -- compiles the array for a phase into a single function local function compile_phase_array(phase_funcs) if not phase_funcs or #phase_funcs == 0 then @@ -28,7 +17,7 @@ local config_cache do -- compile the functions we got local compiled = {} for i, func_string in ipairs(phase_funcs) do - local func = assert(sandbox.sandbox(func_string, sandbox_opts)) + local func = assert(sandbox.sandbox(func_string)) local first_run_complete = false compiled[i] = function() @@ -73,11 +62,9 @@ local config_cache do end end - local phases = { "certificate", "rewrite", "access", "header_filter", "body_filter", "log" } - config_cache = setmetatable({}, { __mode = "k", __index = function(self, config) @@ -96,9 +83,7 @@ local config_cache do end - return function(priority) - local ServerlessFunction = { PRIORITY = priority, VERSION = kong_meta.version, diff --git a/kong/plugins/syslog/handler.lua b/kong/plugins/syslog/handler.lua index f9932db804d7..5d6dd076b443 100644 --- a/kong/plugins/syslog/handler.lua +++ b/kong/plugins/syslog/handler.lua @@ -56,9 +56,6 @@ local FACILITIES = { local7 = lsyslog.FACILITY_LOCAL7 } -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - - local function send_to_syslog(log_level, severity, message, facility) if LOG_PRIORITIES[severity] <= LOG_PRIORITIES[log_level] then lsyslog.open(SENDER_NAME, FACILITIES[facility]) @@ -94,7 +91,7 @@ function SysLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/plugins/tcp-log/handler.lua b/kong/plugins/tcp-log/handler.lua index 06fddb1a0765..9723c44ec8fb 100644 --- a/kong/plugins/tcp-log/handler.lua +++ b/kong/plugins/tcp-log/handler.lua @@ -8,9 +8,6 @@ local ngx = ngx local timer_at = ngx.timer.at -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - - local function log(premature, conf, message) if premature then return @@ -71,7 +68,7 @@ function TcpLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/plugins/udp-log/handler.lua b/kong/plugins/udp-log/handler.lua index ca586237d6cd..2592f92df46a 100644 --- a/kong/plugins/udp-log/handler.lua +++ b/kong/plugins/udp-log/handler.lua @@ -9,9 +9,6 @@ local timer_at = ngx.timer.at local udp = ngx.socket.udp -local sandbox_opts = { env = { kong = kong, ngx = ngx } } - - local function log(premature, conf, str) if premature then return @@ -52,7 +49,7 @@ function UdpLogHandler:log(conf) if conf.custom_fields_by_lua then local set_serialize_value = kong.log.set_serialize_value for key, expression in pairs(conf.custom_fields_by_lua) do - set_serialize_value(key, sandbox(expression, sandbox_opts)()) + set_serialize_value(key, sandbox(expression)()) end end diff --git a/kong/tools/kong-lua-sandbox.lua b/kong/tools/kong-lua-sandbox.lua index d91d6dadfe84..103b4717fc55 100644 --- a/kong/tools/kong-lua-sandbox.lua +++ b/kong/tools/kong-lua-sandbox.lua @@ -1,178 +1,3 @@ -local sandbox = { - _VERSION = "kong-lua-sandbox 1.1", - _DESCRIPTION = "A pure-lua solution for running untrusted Lua code.", - _URL = "https://github.com/kong/kong-lua-sandbox", - _LICENSE = [[ - MIT LICENSE - - Copyright (c) 2021 Kong Inc - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ]], - -} - --- quotas don't work in LuaJIT since debug.sethook works differently there -local quota_supported = type(_G.jit) == "nil" -sandbox.quota_supported = quota_supported - --- PUC-Rio Lua 5.1 does not support deactivation of bytecode -local bytecode_blocked = _ENV or type(_G.jit) == "table" -sandbox.bytecode_blocked = bytecode_blocked - --- The base environment is merged with the given env option (or an empty table, if no env provided) --- -local BASE_ENV = {} - --- List of unsafe packages/functions: --- --- * {set|get}metatable: can be used to modify the metatable of global objects (strings, integers) --- * collectgarbage: can affect performance of other systems --- * dofile: can access the server filesystem --- * _G: It has access to everything. It can be mocked to other things though. --- * load{file|string}: All unsafe because they can grant acces to global env --- * raw{get|set|equal}: Potentially unsafe --- * module|require|module: Can modify the host settings --- * string.dump: Can display confidential server info (implementation of functions) --- * math.randomseed: Can affect the host sytem --- * io.*, os.*: Most stuff there is unsafe, see below for exceptions - - --- Safe packages/functions below -([[ - -_VERSION assert error ipairs next pairs -pcall select tonumber tostring type unpack xpcall - -coroutine.create coroutine.resume coroutine.running coroutine.status -coroutine.wrap coroutine.yield - -math.abs math.acos math.asin math.atan math.atan2 math.ceil -math.cos math.cosh math.deg math.exp math.fmod math.floor -math.frexp math.huge math.ldexp math.log math.log10 math.max -math.min math.modf math.pi math.pow math.rad math.random -math.sin math.sinh math.sqrt math.tan math.tanh - -os.clock os.date os.difftime os.time - -string.byte string.char string.find string.format string.gmatch -string.gsub string.len string.lower string.match string.rep -string.reverse string.sub string.upper - -table.concat table.insert table.maxn table.remove table.sort - -]]):gsub('%S+', function(id) - local module, method = id:match('([^%.]+)%.([^%.]+)') - if module then - BASE_ENV[module] = BASE_ENV[module] or {} - BASE_ENV[module][method] = _G[module][method] - else - BASE_ENV[id] = _G[id] - end -end) - -local function protect_module(module, module_name) - return setmetatable({}, { - __index = module, - __newindex = function(_, attr_name, _) - error('Can not modify ' .. module_name .. '.' .. attr_name .. '. Protected by the sandbox.') - end - }) -end - -('coroutine math os string table'):gsub('%S+', function(module_name) - BASE_ENV[module_name] = protect_module(BASE_ENV[module_name], module_name) -end) - --- auxiliary functions/variables - -local function sethook(f, key, quota) - if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end - debug.sethook(f, key, quota) -end - -local function cleanup() - sethook() -end - --- Public interface: sandbox.protect -function sandbox.protect(code, options) - options = options or {} - - local quota = false - if options.quota and not quota_supported then - error("options.quota is not supported on this environment (usually LuaJIT). Please unset options.quota") - end - if options.quota ~= false then - quota = options.quota or 500000 - end - - assert(type(code) == 'string', "expected a string") - - local passed_env = options.env or {} - local env = {} - for k, v in pairs(BASE_ENV) do - local pv = passed_env[k] - if pv ~= nil then - env[k] = pv - else - env[k] = v - end - end - setmetatable(env, { __index = options.env }) - env._G = env - - local f - if bytecode_blocked then - f = assert(load(code, nil, 't', env)) - else - f = assert(loadstring(code)) - setfenv(f, env) - end - - return function(...) - - if quota and quota_supported then - local timeout = function() - cleanup() - error('Quota exceeded: ' .. tostring(quota)) - end - sethook(timeout, "", quota) - end - - local t = table.pack(pcall(f, ...)) - - cleanup() - - if not t[1] then error(t[2]) end - - return table.unpack(t, 2, t.n) - end -end - --- Public interface: sandbox.run -function sandbox.run(code, options, ...) - return sandbox.protect(code, options)(...) -end - --- make sandbox(f) == sandbox.protect(f) -setmetatable(sandbox, {__call = function(_,code,o) return sandbox.protect(code,o) end}) - -return sandbox +-- this file was moved to sandbox directory, so this is +-- just left in place for backward compatibility reasons +return require("kong.tools.sandbox.kong") diff --git a/kong/tools/sandbox.lua b/kong/tools/sandbox.lua deleted file mode 100644 index 0121ba7156d6..000000000000 --- a/kong/tools/sandbox.lua +++ /dev/null @@ -1,205 +0,0 @@ -local _sandbox = require "kong.tools.kong-lua-sandbox" - -local table = table -local fmt = string.format -local setmetatable = setmetatable -local require = require -local ipairs = ipairs -local pcall = pcall -local type = type -local error = error -local rawset = rawset -local assert = assert -local kong = kong - - --- deep copy tables using dot notation, like --- one: { foo = { bar = { hello = {}, ..., baz = 42 } } } --- target: { hey = { "hello } } --- link("foo.bar.baz", one, target) --- target -> { hey = { "hello" }, foo = { bar = { baz = 42 } } } -local function link(q, o, target) - if not q then return end - - local h, r = q:match("([^%.]+)%.?(.*)") - local mod = o[h] - - if not mod then return end - - if r == "" then - if type(mod) == 'table' then - -- changes on target[h] won't affect mod - target[h] = setmetatable({}, { __index = mod }) - - else - target[h] = mod - end - - return - end - - if not target[h] then target[h] = {} end - - link(r, o[h], target[h]) -end - - -local lazy_conf_methods = { - enabled = function(self) - return kong and - kong.configuration and - kong.configuration.untrusted_lua and - kong.configuration.untrusted_lua ~= 'off' - end, - sandbox_enabled = function(self) - return kong and - kong.configuration and - kong.configuration.untrusted_lua and - kong.configuration.untrusted_lua == 'sandbox' - end, - requires = function(self) - local conf_r = kong and - kong.configuration and - kong.configuration.untrusted_lua_sandbox_requires or {} - local requires = {} - for _, r in ipairs(conf_r) do requires[r] = true end - return requires - end, - env_vars = function(self) - return kong and - kong.configuration and - kong.configuration.untrusted_lua_sandbox_environment or {} - end, - environment = function(self) - local env = { - -- home brewed require function that only requires what we consider - -- safe :) - ["require"] = function(m) - if not self.requires[m] then - error(fmt("require '%s' not allowed within sandbox", m)) - end - - return require(m) - end, - } - - for _, m in ipairs(self.env_vars) do link(m, _G, env) end - - return env - end, -} - - -local conf_values = { - clear = table.clear, - reload = table.clear, - err_msg = "loading of untrusted Lua code disabled because " .. - "'untrusted_lua' config option is set to 'off'" -} - - -local configuration = setmetatable({}, { - __index = function(self, key) - local l = lazy_conf_methods[key] - - if not l then - return conf_values[key] - end - - local value = l(self) - rawset(self, key, value) - - return value - end, -}) - - -local sandbox = function(fn, opts) - if not configuration.enabled then - error(configuration.err_msg) - end - - opts = opts or {} - - local opts = { - -- default set load string mode to only 'text chunks' - mode = opts.mode or 't', - env = opts.env or {}, - chunk_name = opts.chunk_name, - } - - if not configuration.sandbox_enabled then - -- sandbox disabled, all arbitrary Lua code can execute unrestricted - setmetatable(opts.env, { __index = _G}) - - return assert(load(fn, opts.chunk_name, opts.mode, opts.env)) - end - - -- set (discard-able) function context - setmetatable(opts.env, { __index = configuration.environment }) - - return _sandbox(fn, opts) -end - - -local function validate_function(fun, opts) - local ok, func1 = pcall(sandbox, fun, opts) - if not ok then - return false, "Error parsing function: " .. func1 - end - - local success, func2 = pcall(func1) - - if not success then - return false, func2 - end - - if type(func2) == "function" then - return func2 - end - - -- the code returned something unknown - return false, "Bad return value from function, expected function type, got " - .. type(func2) -end - - -local function parse(fn_str, opts) - return assert(validate_function(fn_str, opts)) -end - - -local _M = {} - - -_M.validate_function = validate_function - - -_M.validate = function(fn_str, opts) - local _, err = validate_function(fn_str, opts) - if err then return false, err end - - return true -end - - --- meant for schema, do not execute arbitrary lua! --- https://github.com/Kong/kong/issues/5110 -_M.validate_safe = function(fn_str, opts) - local ok, func1 = pcall(sandbox, fn_str, opts) - - if not ok then - return false, "Error parsing function: " .. func1 - end - - return true -end - - -_M.sandbox = sandbox -_M.parse = parse --- useful for testing -_M.configuration = configuration - - -return _M diff --git a/kong/tools/sandbox/environment/handler.lua b/kong/tools/sandbox/environment/handler.lua new file mode 100644 index 000000000000..ca36b135e9a1 --- /dev/null +++ b/kong/tools/sandbox/environment/handler.lua @@ -0,0 +1,199 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +return require("kong.tools.sandbox.environment.lua") .. [[ +kong.cache.get kong.cache.get_bulk kong.cache.probe +kong.cache.invalidate kong.cache.invalidate_local kong.cache.safe_set +kong.cache.renew + +kong.client.authenticate kong.client.authenticate_consumer_group_by_consumer_id +kong.client.get_consumer kong.client.get_consumer_group +kong.client.get_consumer_groups kong.client.get_credential +kong.client.get_forwarded_ip kong.client.get_forwarded_port +kong.client.get_ip kong.client.get_port +kong.client.get_protocol kong.client.load_consumer +kong.client.set_authenticated_consumer_group kong.client.set_authenticated_consumer_groups + +kong.client.tls.disable_session_reuse kong.client.tls.get_full_client_certificate_chain +kong.client.tls.request_client_certificate kong.client.tls.set_client_verify + +kong.cluster.get_id + +kong.db.certificates.cache_key kong.db.certificates.select +kong.db.certificates.select_by_cache_key +kong.db.consumers.cache_key kong.db.consumers.select +kong.db.consumers.select_by_cache_key kong.db.consumers.select_by_custom_id +kong.db.consumers.select_by_username kong.db.consumers.select_by_username_ignore_case +kong.db.keys.cache_key kong.db.keys.select +kong.db.keys.select_by_cache_key kong.db.keys.select_by_name +kong.db.plugins.cache_key kong.db.plugins.select +kong.db.plugins.select_by_cache_key kong.db.plugins.select_by_instance_name +kong.db.routes.cache_key kong.db.routes.select +kong.db.routes.select_by_cache_key kong.db.routes.select_by_name +kong.db.services.cache_key kong.db.services.select +kong.db.services.select_by_cache_key kong.db.services.select_by_name +kong.db.snis.cache_key kong.db.snis.select +kong.db.snis.select_by_cache_key kong.db.snis.select_by_name +kong.db.targets.cache_key kong.db.targets.select +kong.db.targets.select_by_cache_key kong.db.targets.select_by_target +kong.db.upstreams.cache_key kong.db.upstreams.select +kong.db.upstreams.select_by_cache_key kong.db.upstreams.select_by_name + +kong.default_workspace + +kong.dns.resolve kong.dns.toip + +kong.ip.is_trusted + +kong.jwe.decode kong.jwe.decrypt kong.jwe.encrypt + +kong.log.alert kong.log.crit kong.log.debug +kong.log.emerg kong.log.err kong.log.info +kong.log.notice kong.log.serialize kong.log.set_serialize_value +kong.log.warn + +kong.log.deprecation.write + +kong.log.inspect.on kong.log.inspect.off + +kong.nginx.get_statistics kong.nginx.get_subsystem + +kong.node.get_hostname kong.node.get_id kong.node.get_memory_stats + +kong.plugin.get_id + +kong.request.get_body kong.request.get_forwarded_host +kong.request.get_forwarded_path kong.request.get_forwarded_port +kong.request.get_forwarded_prefix kong.request.get_forwarded_scheme +kong.request.get_header kong.request.get_headers +kong.request.get_host kong.request.get_http_version +kong.request.get_method kong.request.get_path +kong.request.get_path_with_query kong.request.get_port +kong.request.get_query kong.request.get_query_arg +kong.request.get_raw_body kong.request.get_raw_path +kong.request.get_raw_query kong.request.get_scheme +kong.request.get_start_time kong.request.get_uri_captures + +kong.response.add_header kong.response.clear_header +kong.response.error kong.response.exit +kong.response.get_header kong.response.get_headers +kong.response.get_raw_body kong.response.get_source +kong.response.get_status kong.response.set_header +kong.response.set_headers kong.response.set_raw_body +kong.response.set_status + +kong.router.get_route kong.router.get_service + +kong.service.set_retries kong.service.set_target +kong.service.set_target_retry_callback kong.service.set_timeouts +kong.service.set_tls_cert_key kong.service.set_tls_cert_key +kong.service.set_tls_verify kong.service.set_tls_verify_depth +kong.service.set_tls_verify_store kong.service.set_tls_verify_store +kong.service.set_upstream + +kong.service.request.add_header kong.service.request.clear_header +kong.service.request.clear_query_arg kong.service.request.enable_buffering +kong.service.request.set_body kong.service.request.set_header +kong.service.request.set_headers kong.service.request.set_method +kong.service.request.set_path kong.service.request.set_query +kong.service.request.set_raw_body kong.service.request.set_raw_query +kong.service.request.set_scheme + +kong.service.response.get_body kong.service.response.get_header +kong.service.response.get_headers kong.service.response.get_raw_body +kong.service.response.get_status + +kong.table.clear kong.table.merge + +kong.telemetry.log + +kong.tracing.create_span kong.tracing.get_sampling_decision +kong.tracing.link_span kong.tracing.process_span +kong.tracing.set_active_span kong.tracing.set_should_sample +kong.tracing.start_span + +kong.vault.get kong.vault.is_reference kong.vault.parse_reference +kong.vault.try kong.vault.update + +kong.version kong.version_num + +kong.websocket.client.close kong.websocket.client.drop_frame +kong.websocket.client.get_frame kong.websocket.client.set_frame_data +kong.websocket.client.set_max_payload_size kong.websocket.client.set_status + +kong.websocket.upstream.close kong.websocket.upstream.drop_frame +kong.websocket.upstream.get_frame kong.websocket.upstream.set_frame_data +kong.websocket.upstream.set_max_payload_size kong.websocket.upstream.set_status + +ngx.AGAIN ngx.ALERT +ngx.CRIT ngx.DEBUG +ngx.DECLINED ngx.DONE +ngx.EMERG ngx.ERR +ngx.ERROR ngx.HTTP_ACCEPTED +ngx.HTTP_BAD_GATEWAY ngx.HTTP_BAD_REQUEST +ngx.HTTP_CLOSE ngx.HTTP_CONFLICT +ngx.HTTP_CONTINUE ngx.HTTP_COPY +ngx.HTTP_CREATED ngx.HTTP_DELETE +ngx.HTTP_FORBIDDEN ngx.HTTP_GATEWAY_TIMEOUT +ngx.HTTP_GET ngx.HTTP_GONE +ngx.HTTP_HEAD ngx.HTTP_ILLEGAL +ngx.HTTP_INSUFFICIENT_STORAGE ngx.HTTP_INTERNAL_SERVER_ERROR +ngx.HTTP_LOCK ngx.HTTP_METHOD_NOT_IMPLEMENTED +ngx.HTTP_MKCOL ngx.HTTP_MOVE +ngx.HTTP_MOVED_PERMANENTLY ngx.HTTP_MOVED_TEMPORARILY +ngx.HTTP_NOT_ACCEPTABLE ngx.HTTP_NOT_ALLOWED +ngx.HTTP_NOT_FOUND ngx.HTTP_NOT_IMPLEMENTED +ngx.HTTP_NOT_MODIFIED ngx.HTTP_NO_CONTENT +ngx.HTTP_OK ngx.HTTP_OPTIONS +ngx.HTTP_PARTIAL_CONTENT ngx.HTTP_PATCH +ngx.HTTP_PAYMENT_REQUIRED ngx.HTTP_PERMANENT_REDIRECT +ngx.HTTP_POST ngx.HTTP_PROPFIND +ngx.HTTP_PROPPATCH ngx.HTTP_PUT +ngx.HTTP_REQUEST_TIMEOUT ngx.HTTP_SEE_OTHER +ngx.HTTP_SERVICE_UNAVAILABLE ngx.HTTP_SPECIAL_RESPONSE +ngx.HTTP_SWITCHING_PROTOCOLS ngx.HTTP_TEMPORARY_REDIRECT +ngx.HTTP_TOO_MANY_REQUESTS ngx.HTTP_TRACE +ngx.HTTP_UNAUTHORIZED ngx.HTTP_UNLOCK +ngx.HTTP_UPGRADE_REQUIRED ngx.HTTP_VERSION_NOT_SUPPORTED +ngx.INFO ngx.NOTICE +ngx.OK ngx.STDERR +ngx.WARN + +ngx.cookie_time ngx.crc32_long ngx.crc32_short ngx.decode_args +ngx.decode_base64 ngx.encode_args ngx.encode_base64 ngx.eof +ngx.escape_uri ngx.exit ngx.flush ngx.get_phase +ngx.get_raw_phase ngx.hmac_sha1 ngx.http_time ngx.localtime +ngx.log ngx.md5 ngx.md5_bin ngx.now +ngx.null ngx.parse_http_time ngx.print ngx.quote_sql_str +ngx.redirect ngx.say ngx.send_headers ngx.sha1_bin +ngx.sleep ngx.time ngx.today ngx.unescape_uri +ngx.update_time ngx.utctime + +ngx.config.debug ngx.config.nginx_version ngx.config.ngx_lua_version +ngx.config.subsystem + +ngx.location.capture ngx.location.capture_multi + +ngx.re.find ngx.re.gmatch ngx.re.gsub ngx.re.match ngx.re.sub + +ngx.req.append_body ngx.req.clear_header ngx.req.discard_body +ngx.req.finish_body ngx.req.get_body_data ngx.req.get_body_file +ngx.req.get_headers ngx.req.get_method ngx.req.get_post_args +ngx.req.get_uri_args ngx.req.http_version ngx.req.init_body +ngx.req.is_internal ngx.req.raw_header ngx.req.read_body +ngx.req.set_body_data ngx.req.set_body_file ngx.req.set_header +ngx.req.set_method ngx.req.set_uri ngx.req.set_uri_args +ngx.req.socket ngx.req.start_time ngx.resp.get_headers + +ngx.thread.kill ngx.thread.spawn ngx.thread.wait + +ngx.socket.connect ngx.socket.stream ngx.socket.tcp ngx.socket.udp + +ngx.worker.count ngx.worker.exiting ngx.worker.id ngx.worker.pid +ngx.worker.pids +]] diff --git a/kong/tools/sandbox/environment/init.lua b/kong/tools/sandbox/environment/init.lua new file mode 100644 index 000000000000..5d2966b2d812 --- /dev/null +++ b/kong/tools/sandbox/environment/init.lua @@ -0,0 +1,185 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +local ENVIRONMENT do + ENVIRONMENT = {} + + local setmetatable = setmetatable + local getmetatable = getmetatable + local require = require + local package = package + local rawset = rawset + local ipairs = ipairs + local pairs = pairs + local error = error + local type = type + local _G = _G + + local function wrap_method(self, method) + return function(_, ...) + return self[method](self, ...) + end + end + + local function include(env, id) + -- The code here checks a lot of types and stuff, just to please our test suite + -- to not error out when used with mocks. + local m, sm, lf, f = id:match("([^%.]+)%.([^%.]+)%.([^%.]+)%.([^%.]+)") + if m then + env[m] = env[m] or {} + env[m][sm] = env[m][sm] or {} + env[m][sm][lf] = env[m][sm][lf] or {} + + if m == "kong" and sm == "db" then + env[m][sm][lf][f] = type(_G[m]) == "table" + and type(_G[m][sm]) == "table" + and type(_G[m][sm][lf]) == "table" + and type(_G[m][sm][lf][f]) == "function" + and wrap_method(_G[m][sm][lf], f) + else + env[m][sm][lf][f] = type(_G[m]) == "table" + and type(_G[m][sm]) == "table" + and type(_G[m][sm][lf]) == "table" + and _G[m][sm][lf][f] + end + + else + m, sm, f = id:match("([^%.]+)%.([^%.]+)%.([^%.]+)") + if m then + env[m] = env[m] or {} + env[m][sm] = env[m][sm] or {} + + if m == "kong" and sm == "cache" then + env[m][sm][f] = type(_G[m]) == "table" + and type(_G[m][sm]) == "table" + and type(_G[m][sm][f]) == "function" + and wrap_method(_G[m][sm], f) + + else + env[m][sm][f] = type(_G[m]) == "table" + and type(_G[m][sm]) == "table" + and _G[m][sm][f] + end + + else + m, f = id:match("([^%.]+)%.([^%.]+)") + if m then + env[m] = env[m] or {} + env[m][f] = type(_G[m]) == "table" and _G[m][f] + + else + env[id] = _G[id] + end + end + end + end + + + local function protect_module(modname, mod) + return setmetatable(mod, { + __newindex = function(_, k, _) + return error(("Cannot modify %s.%s. Protected by the sandbox."): format(modname, k), -1) + end + }) + end + + local function protect_modules(mod, modname) + for k, v in pairs(mod) do + if type(v) == "table" then + protect_modules(v, modname and (modname .. "." .. k) or k) + end + end + + if modname and modname ~= "ngx" then + protect_module(modname, mod) + end + end + + local function protect(env) + protect_modules(env, "_G") + rawset(env, "_G", env) + + local kong = kong + local ngx = ngx + + if type(ngx) == "table" and type(env.ngx) == "table" then + -- this is needed for special ngx.{ctx|headers_sent|is_subrequest|status) + setmetatable(env.ngx, getmetatable(ngx)) + + -- libraries having metatable logic + rawset(env.ngx, "var", ngx.var) + rawset(env.ngx, "arg", ngx.arg) + rawset(env.ngx, "header", ngx.header) + end + + if type(kong) == "table" and type(env.kong) == "table" then + -- __call meta-method for kong log + if type(kong.log) == "table" and type(env.kong.log) == "table" then + getmetatable(env.kong.log).__call = (getmetatable(kong.log) or {}).__call + + if type(kong.log.inspect) == "table" and type(env.kong.log.inspect) == "table" then + getmetatable(env.kong.log.inspect).__call = (getmetatable(kong.log.inspect) or {}).__call + end + if type(kong.log.deprecation) == "table" and type(env.kong.log.deprecation) == "table" then + getmetatable(env.kong.log.deprecation).__call = (getmetatable(kong.log.deprecation) or {}).__call + end + end + + if type(kong.configuration) == "table" and type(kong.configuration.remove_sensitive) == "function" then + -- only expose the non-sensitive parts of kong.configuration + rawset(env.kong, "configuration", + protect_module("kong.configuration", kong.configuration.remove_sensitive())) + end + + if type(kong.ctx) == "table" then + -- only support kong.ctx.shared and kong.ctx.plugin + local ctx = kong.ctx + rawset(env.kong, "ctx", protect_module("kong.ctx", { + shared = setmetatable({}, { + __newindex = function(_, k, v) + ctx.shared[k] = v + end, + __index = function(_, k) + return ctx.shared[k] + end, + }), + plugin = setmetatable({}, { + __newindex = function(_, k, v) + ctx.plugin[k] = v + end, + __index = function(_, k) + return ctx.plugin[k] + end, + }) + })) + end + end + + return env + end + + local sandbox_require = require("kong.tools.sandbox.require") + + -- the order is from the biggest to the smallest so that package + -- unloading works properly (just to not leave garbage around) + for _, t in ipairs({ "handler", "schema", "lua" }) do + local env = {} + local package_name = "kong.tools.sandbox.environment." .. t + require(package_name):gsub("%S+", function(id) + include(env, id) + end) + package.loaded[package_name] = nil + rawset(env, "require", sandbox_require[t]) + ENVIRONMENT[t] = protect(env) + end + + package.loaded["kong.tools.sandbox.require"] = nil +end + + +return ENVIRONMENT diff --git a/kong/tools/sandbox/environment/lua.lua b/kong/tools/sandbox/environment/lua.lua new file mode 100644 index 000000000000..29ce8c0ad67a --- /dev/null +++ b/kong/tools/sandbox/environment/lua.lua @@ -0,0 +1,39 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +return [[ +_VERSION assert error ipairs next pairs pcall print select +tonumber tostring type unpack xpcall + +bit.arshift bit.band bit.bnot bit.bor bit.bswap bit.bxor +bit.lshift bit.rol bit.ror bit.rshift bit.tobit bit.tohex + +coroutine.create coroutine.resume coroutine.running +coroutine.status coroutine.wrap coroutine.yield + +io.type + +jit.os jit.arch jit.version jit.version_num + +math.abs math.acos math.asin math.atan math.atan2 math.ceil +math.cos math.cosh math.deg math.exp math.floor math.fmod +math.frexp math.huge math.ldexp math.log math.log10 math.max +math.min math.modf math.pi math.pow math.rad math.random +math.sin math.sinh math.sqrt math.tan math.tanh + +os.clock os.date os.difftime os.time + +string.byte string.char string.find string.format string.gmatch +string.gsub string.len string.lower string.match string.rep +string.reverse string.sub string.upper + +table.clear table.clone table.concat table.foreach table.foreachi +table.getn table.insert table.isarray table.isempty table.maxn +table.move table.new table.nkeys table.pack table.remove +table.sort table.unpack +]] diff --git a/kong/tools/sandbox/environment/schema.lua b/kong/tools/sandbox/environment/schema.lua new file mode 100644 index 000000000000..fc17e742b472 --- /dev/null +++ b/kong/tools/sandbox/environment/schema.lua @@ -0,0 +1,10 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +-- for now the schema is just using the most restricted environment +return require("kong.tools.sandbox.environment.lua") diff --git a/kong/tools/sandbox/init.lua b/kong/tools/sandbox/init.lua new file mode 100644 index 000000000000..f2ef433a4a76 --- /dev/null +++ b/kong/tools/sandbox/init.lua @@ -0,0 +1,334 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +local sb_kong = require("kong.tools.sandbox.kong") + + +local table = table +local setmetatable = setmetatable +local require = require +local ipairs = ipairs +local pcall = pcall +local type = type +local load = load +local error = error +local rawset = rawset +local assert = assert + + +-- deep copy tables using dot notation, like +-- one: { foo = { bar = { hello = {}, ..., baz = 42 } } } +-- target: { hey = { "hello } } +-- link("foo.bar.baz", one, target) +-- target -> { hey = { "hello" }, foo = { bar = { baz = 42 } } } +local function link(q, o, target) + if not q then + return + end + + local h, r = q:match("([^%.]+)%.?(.*)") + + local mod = o[h] + if not mod then + return + end + + if r == "" then + if type(mod) == "table" then + -- changes on target[h] won't affect mod + target[h] = setmetatable({}, { __index = mod }) + + else + target[h] = mod + end + + return + end + + if not target[h] then + target[h] = {} + end + + link(r, o[h], target[h]) +end + + +local function get_conf(name) + return kong + and kong.configuration + and kong.configuration[name] +end + + +local function link_vars(vars, env) + if vars then + for _, m in ipairs(vars) do + link(m, _G, env) + end + end + + env._G = env + + return env +end + + +local function denied_table(modname) + return setmetatable({}, { __index = {}, __newindex = function(_, k) + return error(("Cannot modify %s.%s. Protected by the sandbox."):format(modname, k), -1) + end, __tostring = function() + return "nil" + end }) +end + + +local function denied_require(modname) + return error(("require '%s' not allowed within sandbox"):format(modname), -1) +end + + +local function get_backward_compatible_sandboxed_kong() + -- this is a more like a blacklist where we try to keep backwards + -- compatibility, but still improve the default sandboxing not leaking + -- secrets like pg_password. + -- + -- just to note, kong.db.:truncate() and kong.db.connector:query(...) + -- are quite powerful, but they are not disallowed for backwards compatibility. + -- + -- of course this to work, the `getmetatable` and `require "inspect"` and such + -- need to be disabled as well. + + local k + if type(kong) == "table" then + k = setmetatable({ + licensing = denied_table("kong.licensing"), + }, { __index = kong }) + + if type(kong.cache) == "table" then + k.cache = setmetatable({ + cluster_events = denied_table("kong.cache.cluster_events") + }, { __index = kong.cache }) + end + + if type(kong.core_cache) == "table" then + k.core_cache = setmetatable({ + cluster_events = denied_table("kong.cache.cluster_events") + }, { __index = kong.core_cache }) + end + + if type(kong.configuration) == "table" and type(kong.configuration.remove_sensitive) == "function" then + k.configuration = kong.configuration.remove_sensitive() + end + + if type(kong.db) == "table" then + k.db = setmetatable({}, { __index = kong.db }) + if type(kong.db.connector) == "table" then + k.db.connector = setmetatable({ + config = denied_table("kong.db.connector.config") + }, { __index = kong.db.connector }) + end + end + end + return k +end + + +local lazy_conf_methods = { + enabled = function() + return get_conf("untrusted_lua") ~= "off" + end, + sandbox_enabled = function() + return get_conf("untrusted_lua") == "sandbox" + end, + requires = function() + local sandbox_requires = get_conf("untrusted_lua_sandbox_requires") + if type(sandbox_requires) ~= "table" or #sandbox_requires == 0 then + return + end + local requires = {} + for _, r in ipairs(sandbox_requires) do + requires[r] = true + end + return requires + end, + env_vars = function() + local env_vars = get_conf("untrusted_lua_sandbox_environment") + if type(env_vars) ~= "table" or #env_vars == 0 then + return + end + return env_vars + end, + environment = function(self) + local requires = self.requires + return link_vars(self.env_vars, requires and { + -- home brewed require function that only requires what we consider safe :) + require = function(modname) + if not requires[modname] then + return denied_require(modname) + end + return require(modname) + end, + -- allow almost full non-sandboxed access to everything in kong global + kong = get_backward_compatible_sandboxed_kong(), + -- allow full non-sandboxed access to everything in ngx global (including timers, :-() + ngx = ngx, + } or { + require = denied_require, + -- allow almost full non-sandboxed access to everything in kong global + kong = get_backward_compatible_sandboxed_kong(), + -- allow full non-sandboxed access to everything in ngx global (including timers, :-() + ngx = ngx, + }) + end, + sandbox_mt = function(self) + return { __index = self.environment } + end, + global_mt = function() + return { __index = _G } + end, +} + + +local conf_values = { + clear = table.clear, + reload = table.clear, + err_msg = "loading of untrusted Lua code disabled because " .. + "'untrusted_lua' config option is set to 'off'" +} + + +local configuration = setmetatable({}, { + __index = function(self, key) + local l = lazy_conf_methods[key] + if not l then + return conf_values[key] + end + + local value = l(self) + rawset(self, key, value) + + return value + end, +}) + + +local function sandbox_backward_compatible(chunk, chunkname_or_options, mode, env) + if not configuration.enabled then + return error(configuration.err_msg, -1) + end + + local chunkname + if type(chunkname_or_options) == "table" then + chunkname = chunkname_or_options.chunkname or chunkname_or_options.chunk_name + mode = mode or chunkname_or_options.mode or "t" + env = env or chunkname_or_options.env or {} + else + chunkname = chunkname_or_options + mode = mode or "t" + env = env or {} + end + + if not configuration.sandbox_enabled then + -- sandbox disabled, all arbitrary Lua code can execute unrestricted, + -- but do not allow direct modification of the global environment + return assert(load(chunk, chunkname, mode, setmetatable(env, configuration.global_mt))) + end + + return sb_kong(chunk, chunkname, mode, setmetatable(env, configuration.sandbox_mt)) +end + + +local function sandbox(chunk, chunkname, func) + if not configuration.enabled then + return error(configuration.err_msg, -1) + end + + if not configuration.sandbox_enabled then + -- sandbox disabled, all arbitrary Lua code can execute unrestricted, + -- but do not allow direct modification of the global environment + return assert(load(chunk, chunkname, "t", setmetatable({}, configuration.global_mt))) + end + + return func(chunk, chunkname) +end + + +local function sandbox_lua(chunk, chunkname) + return sandbox(chunk, chunkname, sb_kong.protect_lua) +end + + +local function sandbox_schema(chunk, chunkname) + return sandbox(chunk, chunkname, sb_kong.protect_schema) +end + + +local function sandbox_handler(chunk, chunkname) + return sandbox(chunk, chunkname, sb_kong.protect_handler) +end + + +local function validate_function(chunk, chunkname_or_options, mode, env) + local ok, compiled_chunk = pcall(sandbox_backward_compatible, chunk, chunkname_or_options, mode, env) + if not ok then + return false, "Error parsing function: " .. compiled_chunk + end + + local success, fn = pcall(compiled_chunk) + if not success then + return false, fn + end + + if type(fn) == "function" then + return fn + end + + -- the code returned something unknown + return false, "Bad return value from function, expected function type, got " .. type(fn) +end + + +local function parse(chunk, chunkname_or_options, mode, env) + return assert(validate_function(chunk, chunkname_or_options, mode, env)) +end + + +local function validate(chunk, chunkname_or_options, mode, env) + local _, err = validate_function(chunk, chunkname_or_options, mode, env) + if err then + return false, err + end + + return true +end + + +-- meant for schema, do not execute arbitrary lua! +-- https://github.com/Kong/kong/issues/5110 +local function validate_safe(chunk, chunkname_or_options, mode, env) + local ok, fn = pcall(sandbox_backward_compatible, chunk, chunkname_or_options, mode, env) + if not ok then + return false, "Error parsing function: " .. fn + end + + return true +end + + +return { + validate = validate, + validate_safe = validate_safe, + validate_function = validate_function, + sandbox = sandbox_backward_compatible, + sandbox_lua = sandbox_lua, + sandbox_schema = sandbox_schema, + sandbox_handler = sandbox_handler, + parse = parse, + --useful for testing + configuration = configuration, +} diff --git a/kong/tools/sandbox/kong.lua b/kong/tools/sandbox/kong.lua new file mode 100644 index 000000000000..41ecf20acc88 --- /dev/null +++ b/kong/tools/sandbox/kong.lua @@ -0,0 +1,125 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +local clone = require "table.clone" + + +local setmetatable = setmetatable +local rawset = rawset +local unpack = table.unpack +local assert = assert +local error = error +local pairs = pairs +local pcall = pcall +local type = type +local load = load +local pack = table.pack + + +local function get_lua_env() + return require("kong.tools.sandbox.environment").lua +end + + +local function get_schema_env() + return require("kong.tools.sandbox.environment").schema +end + + +local function get_handler_env() + return require("kong.tools.sandbox.environment").handler +end + + +local function create_lua_env(env) + local new_env = clone(get_lua_env()) + if env then + for k, v in pairs(new_env) do + rawset(new_env, k, env[k] ~= nil and env[k] or v) + end + if env.require ~= nil then + rawset(new_env, "require", env.require) + end + setmetatable(new_env, { __index = env }) + end + return new_env +end + + +local function wrap(compiled) + return function(...) + local t = pack(pcall(compiled, ...)) + if not t[1] then + return error(t[2], -1) + end + return unpack(t, 2, t.n) + end +end + + +local function protect_backward_compatible(chunk, chunkname, mode, env) + assert(type(chunk) == "string", "expected a string") + local compiled, err = load(chunk, chunkname, mode or "t", create_lua_env(env)) + if not compiled then + return error(err, -1) + end + local fn = wrap(compiled) + return fn +end + + +local sandbox = {} + + +function sandbox.protect(chunk, chunkname_or_options, mode, env) + if type(chunkname_or_options) == "table" then + return protect_backward_compatible(chunk, nil, nil, chunkname_or_options and chunkname_or_options.env) + end + return protect_backward_compatible(chunk, chunkname_or_options, mode, env) +end + + +function sandbox.run(chunk, options, ...) + return sandbox.protect(chunk, options)(...) +end + + +local function protect(chunk, chunkname, env) + assert(type(chunk) == "string", "expected a string") + local compiled, err = load(chunk, chunkname, "t", env) + if not compiled then + return error(err, -1) + end + return compiled +end + + +function sandbox.protect_lua(chunk, chunkname) + return protect(chunk, chunkname, get_lua_env()) +end + + +function sandbox.protect_schema(chunk, chunkname) + return protect(chunk, chunkname, get_schema_env()) +end + + +function sandbox.protect_handler(chunk, chunkname) + return protect(chunk, chunkname, get_handler_env()) +end + + +-- make sandbox(f) == sandbox.protect(f) +setmetatable(sandbox, { + __call = function(_, chunk, chunkname_or_options, mode, env) + return sandbox.protect(chunk, chunkname_or_options, mode, env) + end +}) + + +return sandbox diff --git a/kong/tools/sandbox/require/handler.lua b/kong/tools/sandbox/require/handler.lua new file mode 100644 index 000000000000..a25e47468214 --- /dev/null +++ b/kong/tools/sandbox/require/handler.lua @@ -0,0 +1,62 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +return require("kong.tools.sandbox.require.lua") .. [[ +kong.enterprise_edition.tools.redis.v2 + +cjson cjson.safe + +lyaml + +kong.constants kong.concurrency kong.meta + +kong.tools.cjson kong.tools.gzip kong.tools.ip kong.tools.mime_type +kong.tools.rand kong.tools.sha256 kong.tools.string kong.tools.table +kong.tools.time kong.tools.timestamp kong.tools.uri kong.tools.uuid +kong.tools.yield + +ngx.base64 ngx.req ngx.resp ngx.semaphore + +pgmoon pgmoon.arrays pgmoon.hstore + +pl.stringx pl.tablex + +resty.aes resty.lock resty.md5 resty.memcached resty.mysql resty.random +resty.redis resty.sha resty.sha1 resty.sha224 resty.sha256 resty.sha384 +resty.sha512 resty.string resty.upload + +resty.core.time resty.dns.resolver resty.lrucache resty.lrucache.pureffi + +resty.ada resty.ada.search +resty.aws +resty.azure +resty.cookie +resty.evp +resty.gcp +resty.http +resty.ipmatcher +resty.jit-uuid +resty.jq +resty.jwt +resty.passwdqc +resty.session +resty.rediscluster + +resty.openssl resty.openssl.bn resty.openssl.cipher resty.openssl.digest +resty.openssl.hmac resty.openssl.kdf resty.openssl.mac resty.openssl.pkey +resty.openssl.pkcs12 resty.openssl.objects resty.openssl.rand resty.openssl.version +resty.openssl.x509 + +socket.url + +tablepool + +version + +xmlua +]] diff --git a/kong/tools/sandbox/require/init.lua b/kong/tools/sandbox/require/init.lua new file mode 100644 index 000000000000..36b1c4b8fab5 --- /dev/null +++ b/kong/tools/sandbox/require/init.lua @@ -0,0 +1,48 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +local REQUIRES do + local require = require + local package = package + local ipairs = ipairs + local error = error + + local function denied_require(modname) + return error(("require '%s' not allowed within sandbox"):format(modname)) + end + + REQUIRES = setmetatable({}, { + __index = function() + return denied_require + end + }) + + local function generate_require(packages) + return function(modname) + if not packages[modname] then + return denied_require(modname) + end + return require(modname) + end + end + + -- the order is from the biggest to the smallest so that package + -- unloading works properly (just to not leave garbage around) + for _, t in ipairs({ "handler", "schema", "lua" }) do + local packages = {} + local package_name = "kong.tools.sandbox.require." .. t + require(package_name):gsub("%S+", function(modname) + packages[modname] = true + end) + package.loaded[package_name] = nil + REQUIRES[t] = generate_require(packages) + end +end + + +return REQUIRES diff --git a/kong/tools/sandbox/require/lua.lua b/kong/tools/sandbox/require/lua.lua new file mode 100644 index 000000000000..218f54b4a050 --- /dev/null +++ b/kong/tools/sandbox/require/lua.lua @@ -0,0 +1,15 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +return [[ +bit + +string.buffer + +table.clear table.clone table.isarray table.isempty table.new table.nkeys +]] diff --git a/kong/tools/sandbox/require/schema.lua b/kong/tools/sandbox/require/schema.lua new file mode 100644 index 000000000000..852bbb3caef2 --- /dev/null +++ b/kong/tools/sandbox/require/schema.lua @@ -0,0 +1,13 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + + +return require("kong.tools.sandbox.require.lua") .. [[ +kong.db.schema.typedefs kong.tools.redis.schema + +kong.enterprise_edition.tools.redis.v2 kong.enterprise_edition.tools.redis.v2.schema +]] diff --git a/spec/01-unit/20-sandbox_spec.lua b/spec/01-unit/20-sandbox_spec.lua index 4e5fa5deab96..96bed14fd070 100644 --- a/spec/01-unit/20-sandbox_spec.lua +++ b/spec/01-unit/20-sandbox_spec.lua @@ -20,8 +20,8 @@ describe("sandbox functions wrapper", function() -- load and reference module we can spy on load_s = spy.new(load) _G.load = load_s - _sandbox = spy.new(require "kong.tools.kong-lua-sandbox") - package.loaded["kong.tools.kong-lua-sandbox"] = _sandbox + _sandbox = spy.new(require "kong.tools.sandbox.kong") + package.loaded["kong.tools.sandbox.kong"] = _sandbox sandbox = require "kong.tools.sandbox" end) diff --git a/spec/03-plugins/18-acl/02-access_spec.lua b/spec/03-plugins/18-acl/02-access_spec.lua index 8b69f0f24346..e8f62fd58b9d 100644 --- a/spec/03-plugins/18-acl/02-access_spec.lua +++ b/spec/03-plugins/18-acl/02-access_spec.lua @@ -729,19 +729,15 @@ for _, strategy in helpers.each_strategy() do hosts = { "acl14.test" } }) - local acl_prefunction_code = " local consumer_id = \"" .. tostring(consumer2.id) .. "\"\n" .. [[ - local cache_key = kong.db.acls:cache_key(consumer_id) - - -- we must use shadict to get the cache, because the `kong.cache` was hooked by `kong.plugins.pre-function` - local raw_groups, err = ngx.shared.kong_db_cache:get("kong_db_cache"..cache_key) - if raw_groups then + local acl_prefunction_code = ([[ + local ok, err = kong.cache:get(%q) + if ok then ngx.exit(200) else ngx.log(ngx.ERR, "failed to get cache: ", err) ngx.exit(500) end - - ]] + ]]):format(kong.db.acls:cache_key(tostring(consumer2.id))) bp.plugins:insert { route = { id = route14.id }, diff --git a/spec/03-plugins/36-request-transformer/02-access_spec.lua b/spec/03-plugins/36-request-transformer/02-access_spec.lua index f027aaf267aa..0104054f83be 100644 --- a/spec/03-plugins/36-request-transformer/02-access_spec.lua +++ b/spec/03-plugins/36-request-transformer/02-access_spec.lua @@ -1462,7 +1462,7 @@ describe("Plugin: request-transformer(access) [#" .. strategy .. "]", function() end) end) - describe("append ", function() + describe("append", function() it("new header if header does not exists", function() local r = assert(client:send { method = "GET", @@ -1638,7 +1638,7 @@ describe("Plugin: request-transformer(access) [#" .. strategy .. "]", function() end) end) - describe("remove, replace, add and append ", function() + describe("remove, replace, add and append", function() it("removes a header", function() local r = assert(client:send { method = "GET", @@ -2657,7 +2657,7 @@ describe("Plugin: request-transformer(access) [#" .. strategy .. "] untrusted_lu name = "request-transformer", config = { add = { - headers = {"x-added:$(require('inspect')('somestring'))"}, + headers = {"x-added:$(require('ngx.process')('somestring'))"}, } } } @@ -2722,7 +2722,7 @@ describe("Plugin: request-transformer(access) [#" .. strategy .. "] untrusted_lu end) it("should fail when template tries to require non whitelisted module from sandbox", function() - local pattern = [[require 'inspect' not allowed within sandbox]] + local pattern = [[require 'ngx.process' not allowed within sandbox]] local start_count = count_log_lines(pattern) local r = assert(client:send {