optimize sandbox

This commit is contained in:
Sky Johnson 2025-04-10 09:26:14 -05:00
parent 0abf31ed3a
commit ba9a3db0a0
4 changed files with 138 additions and 557 deletions

View File

@ -1,138 +0,0 @@
package http
import (
"Moonshark/core/runner"
"Moonshark/core/utils"
"Moonshark/core/utils/logger"
"crypto/subtle"
"errors"
"github.com/valyala/fasthttp"
)
// Error for CSRF validation failure
var ErrCSRFValidationFailed = errors.New("CSRF token validation failed")
// ValidateCSRFToken checks if the CSRF token is valid for a request
func ValidateCSRFToken(ctx *runner.Context) bool {
// Only validate for form submissions
method, ok := ctx.Get("method").(string)
if !ok || (method != "POST" && method != "PUT" && method != "PATCH" && method != "DELETE") {
return true
}
// Get form data
formData, ok := ctx.Get("form").(map[string]any)
if !ok || formData == nil {
logger.Warning("CSRF validation failed: no form data")
return false
}
// Get token from form
formToken, ok := formData["csrf"].(string)
if !ok || formToken == "" {
logger.Warning("CSRF validation failed: no token in form")
return false
}
// Get session from context
sessionMap, ok := ctx.Get("session").(map[string]any)
if !ok || sessionMap == nil {
logger.Warning("CSRF validation failed: no session data")
return false
}
// Get session data
sessionData, ok := sessionMap["data"].(map[string]any)
if !ok || sessionData == nil {
logger.Warning("CSRF validation failed: no session data map")
return false
}
// Get token from session
sessionToken, ok := sessionData["_csrf_token"].(string)
if !ok || sessionToken == "" {
logger.Warning("CSRF validation failed: no token in session")
return false
}
// Constant-time comparison to prevent timing attacks
return subtle.ConstantTimeCompare([]byte(formToken), []byte(sessionToken)) == 1
}
// HandleCSRFError handles a CSRF validation error
func HandleCSRFError(ctx *fasthttp.RequestCtx, errorConfig utils.ErrorPageConfig) {
method := string(ctx.Method())
path := string(ctx.Path())
logger.Warning("CSRF validation failed for %s %s", method, path)
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusForbidden)
errorMsg := "Invalid or missing CSRF token. This could be due to an expired form or a cross-site request forgery attempt."
errorHTML := utils.ForbiddenPage(errorConfig, path, errorMsg)
ctx.SetBody([]byte(errorHTML))
}
// GenerateCSRFToken creates a new CSRF token and stores it in the session
func GenerateCSRFToken(ctx *runner.Context, length int) (string, error) {
if length < 16 {
length = 16 // Minimum token length for security
}
// Create secure random token
token, err := GenerateSecureToken(length)
if err != nil {
return "", err
}
// Get session from context
sessionMap, ok := ctx.Get("session").(map[string]any)
if !ok || sessionMap == nil {
return "", errors.New("no session found in context")
}
// Get session data
sessionData, ok := sessionMap["data"].(map[string]any)
if !ok {
// Initialize session data if it doesn't exist
sessionData = make(map[string]any)
sessionMap["data"] = sessionData
}
// Store token in session
sessionData["_csrf_token"] = token
return token, nil
}
// GetCSRFToken retrieves the current CSRF token or generates a new one
func GetCSRFToken(ctx *runner.Context) (string, error) {
// Get session from context
sessionMap, ok := ctx.Get("session").(map[string]any)
if !ok || sessionMap == nil {
return "", errors.New("no session found in context")
}
// Get session data
sessionData, ok := sessionMap["data"].(map[string]any)
if !ok || sessionData == nil {
return GenerateCSRFToken(ctx, 32)
}
// Check if token already exists in session
if token, ok := sessionData["_csrf_token"].(string); ok && token != "" {
return token, nil
}
// Generate new token
return GenerateCSRFToken(ctx, 32)
}
// CSRFMiddleware validates CSRF tokens for state-changing requests
func CSRFMiddleware(ctx *runner.Context) error {
if !ValidateCSRFToken(ctx) {
return ErrCSRFValidationFailed
}
return nil
}

View File

@ -2,7 +2,6 @@ package http
import ( import (
"context" "context"
"errors"
"time" "time"
"Moonshark/core/metadata" "Moonshark/core/metadata"
@ -167,14 +166,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
luaCtx.Set("path", path) luaCtx.Set("path", path)
luaCtx.Set("host", host) luaCtx.Set("host", host)
// Initialize session
session := s.sessionManager.GetSessionFromRequest(ctx)
sessionMap := map[string]any{
"id": session.ID,
"data": session.Data,
}
luaCtx.Set("session", sessionMap)
// URL parameters // URL parameters
if params.Count > 0 { if params.Count > 0 {
paramMap := make(map[string]any, params.Count) paramMap := make(map[string]any, params.Count)
@ -201,25 +192,11 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
luaCtx.Set("form", make(map[string]any)) luaCtx.Set("form", make(map[string]any))
} }
// CSRF middleware for state-changing requests
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
if !ValidateCSRFToken(luaCtx) {
HandleCSRFError(ctx, s.errorConfig)
return
}
}
// Execute Lua script // Execute Lua script
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
if err != nil { if err != nil {
logger.Error("Error executing Lua route: %v", err) logger.Error("Error executing Lua route: %v", err)
// Special handling for specific errors
if errors.Is(err, ErrCSRFValidationFailed) {
HandleCSRFError(ctx, s.errorConfig)
return
}
// General error handling // General error handling
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetStatusCode(fasthttp.StatusInternalServerError)
@ -228,15 +205,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
return return
} }
// Update session if modified
if response.SessionModified {
for k, v := range response.SessionData {
session.Set(k, v)
}
s.sessionManager.ApplySessionCookie(ctx, session)
}
// Apply response to HTTP context // Apply response to HTTP context
runner.ApplyResponse(response, ctx) runner.ApplyResponse(response, ctx)

View File

@ -115,19 +115,7 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
// Create a response object // Create a response object
response := NewResponse() response := NewResponse()
// Load bytecode // Get the execution function first
if err := state.LoadBytecode(bytecode, "script"); err != nil {
ReleaseResponse(response)
return nil, fmt.Errorf("failed to load script: %w", err)
}
// Set up context values for execution
if err := state.PushTable(ctx.Values); err != nil {
ReleaseResponse(response)
return nil, err
}
// Get the execution function
state.GetGlobal("__execute_script") state.GetGlobal("__execute_script")
if !state.IsFunction(-1) { if !state.IsFunction(-1) {
state.Pop(1) state.Pop(1)
@ -135,11 +123,19 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
return nil, ErrSandboxNotInitialized return nil, ErrSandboxNotInitialized
} }
// Push function and bytecode // Load bytecode
state.PushCopy(-2) // Bytecode if err := state.LoadBytecode(bytecode, "script"); err != nil {
state.PushCopy(-2) // Context state.Pop(1) // Pop the __execute_script function
state.Remove(-4) // Remove bytecode duplicate ReleaseResponse(response)
state.Remove(-3) // Remove context duplicate return nil, fmt.Errorf("failed to load script: %w", err)
}
// Push context values
if err := state.PushTable(ctx.Values); err != nil {
state.Pop(2) // Pop bytecode and __execute_script
ReleaseResponse(response)
return nil, err
}
// Execute with 2 args, 1 result // Execute with 2 args, 1 result
if err := state.Call(2, 1); err != nil { if err := state.Call(2, 1); err != nil {
@ -222,32 +218,8 @@ func extractHTTPResponseData(state *luajit.State, response *Response) {
} }
state.Pop(1) state.Pop(1)
// Check session modified flag
state.GetField(-1, "session_modified")
if state.IsBoolean(-1) && state.ToBoolean(-1) {
logger.DebugCont("Found session_modified=true")
response.SessionModified = true
// Get session data (using the new structure)
state.Pop(1) // Remove session_modified
state.GetField(-1, "session_data")
if state.IsTable(-1) {
sessionData, err := state.ToTable(-1)
if err == nil {
for k, v := range sessionData {
response.SessionData[k] = v
}
}
}
state.Pop(1)
} else {
logger.DebugCont("session_modified is not set or not true")
}
state.Pop(1)
// Clean up // Clean up
state.Pop(1) state.Pop(2)
} }
// extractCookie pulls cookie data from the current table on the stack // extractCookie pulls cookie data from the current table on the stack

View File

@ -6,14 +6,10 @@ including core modules and utilities. It's designed to be embedded in the
Go binary at build time. Go binary at build time.
]]-- ]]--
-- Global tables for execution context __http_response = {}
__http_responses = {}
__module_paths = {} __module_paths = {}
__module_bytecode = {} __module_bytecode = {}
__ready_modules = {} __ready_modules = {}
__session_data = {}
__session_id = nil
__session_modified = false
-- ====================================================================== -- ======================================================================
-- CORE SANDBOX FUNCTIONALITY -- CORE SANDBOX FUNCTIONALITY
@ -21,15 +17,12 @@ __session_modified = false
-- Create environment inheriting from _G -- Create environment inheriting from _G
function __create_env(ctx) function __create_env(ctx)
-- Create environment with metatable inheriting from _G
local env = setmetatable({}, {__index = _G}) local env = setmetatable({}, {__index = _G})
-- Add context if provided
if ctx then if ctx then
env.ctx = ctx env.ctx = ctx
end end
-- Add proper require function to this environment
if __setup_require then if __setup_require then
__setup_require(env) __setup_require(env)
end end
@ -39,188 +32,163 @@ end
-- Execute script with clean environment -- Execute script with clean environment
function __execute_script(fn, ctx) function __execute_script(fn, ctx)
-- Clear previous responses __http_response = nil
__http_responses[1] = nil
-- Create environment with metatable inheriting from _G local env = __create_env(ctx)
local env = setmetatable({}, {__index = _G})
-- Add context if provided
if ctx then
env.ctx = ctx
end
-- Initialize local session variables in the environment
local sessionData = {}
local sessionId = ""
if ctx.session then
sessionId = ctx.session.id or ""
sessionData = ctx.session.data or {}
end
env.__session_data = sessionData
env.__session_id = sessionId
env.__session_modified = false
-- Set environment for function
setfenv(fn, env) setfenv(fn, env)
-- Execute with protected call
local ok, result = pcall(fn) local ok, result = pcall(fn)
if not ok then if not ok then
error(result, 0) error(result, 0)
end end
-- If session was modified, add to response
if env.__session_modified then
__http_responses[1] = __http_responses[1] or {}
__http_responses[1].session_data = env.__session_data
__http_responses[1].session_modified = true
end
return result return result
end end
-- Ensure __http_response exists, then return it
function __ensure_response()
if not __http_response then
__http_response = {}
end
return __http_response
end
-- ====================================================================== -- ======================================================================
-- HTTP MODULE -- HTTP MODULE
-- ====================================================================== -- ======================================================================
-- HTTP module implementation -- HTTP module implementation
local http = { local http = {
-- Set HTTP status code -- Set HTTP status code
set_status = function(code) set_status = function(code)
if type(code) ~= "number" then if type(code) ~= "number" then
error("http.set_status: status code must be a number", 2) error("http.set_status: status code must be a number", 2)
end end
local resp = __http_responses[1] or {} local resp = __ensure_response()
resp.status = code resp.status = code
__http_responses[1] = resp end,
end,
-- Set HTTP header -- Set HTTP header
set_header = function(name, value) set_header = function(name, value)
if type(name) ~= "string" or type(value) ~= "string" then if type(name) ~= "string" or type(value) ~= "string" then
error("http.set_header: name and value must be strings", 2) error("http.set_header: name and value must be strings", 2)
end end
local resp = __http_responses[1] or {} local resp = __ensure_response()
resp.headers = resp.headers or {} resp.headers = resp.headers or {}
resp.headers[name] = value resp.headers[name] = value
__http_responses[1] = resp end,
end,
-- Set content type; set_header helper -- Set content type; set_header helper
set_content_type = function(content_type) set_content_type = function(content_type)
http.set_header("Content-Type", content_type) http.set_header("Content-Type", content_type)
end, end,
-- Set metadata (arbitrary data to be returned with response) -- Set metadata (arbitrary data to be returned with response)
set_metadata = function(key, value) set_metadata = function(key, value)
if type(key) ~= "string" then if type(key) ~= "string" then
error("http.set_metadata: key must be a string", 2) error("http.set_metadata: key must be a string", 2)
end end
local resp = __http_responses[1] or {} local resp = __ensure_response()
resp.metadata = resp.metadata or {} resp.metadata = resp.metadata or {}
resp.metadata[key] = value resp.metadata[key] = value
__http_responses[1] = resp end,
end,
-- HTTP client submodule -- HTTP client submodule
client = { client = {
-- Generic request function -- Generic request function
request = function(method, url, body, options) request = function(method, url, body, options)
if type(method) ~= "string" then if type(method) ~= "string" then
error("http.client.request: method must be a string", 2) error("http.client.request: method must be a string", 2)
end end
if type(url) ~= "string" then if type(url) ~= "string" then
error("http.client.request: url must be a string", 2) error("http.client.request: url must be a string", 2)
end end
-- Call native implementation -- Call native implementation
local result = __http_request(method, url, body, options) local result = __http_request(method, url, body, options)
return result return result
end, end,
-- Simple GET request -- Simple GET request
get = function(url, options) get = function(url, options)
return http.client.request("GET", url, nil, options) return http.client.request("GET", url, nil, options)
end, end,
-- Simple POST request with automatic content-type -- Simple POST request with automatic content-type
post = function(url, body, options) post = function(url, body, options)
options = options or {} options = options or {}
return http.client.request("POST", url, body, options) return http.client.request("POST", url, body, options)
end, end,
-- Simple PUT request with automatic content-type -- Simple PUT request with automatic content-type
put = function(url, body, options) put = function(url, body, options)
options = options or {} options = options or {}
return http.client.request("PUT", url, body, options) return http.client.request("PUT", url, body, options)
end, end,
-- Simple DELETE request -- Simple DELETE request
delete = function(url, options) delete = function(url, options)
return http.client.request("DELETE", url, nil, options) return http.client.request("DELETE", url, nil, options)
end, end,
-- Simple PATCH request -- Simple PATCH request
patch = function(url, body, options) patch = function(url, body, options)
options = options or {} options = options or {}
return http.client.request("PATCH", url, body, options) return http.client.request("PATCH", url, body, options)
end, end,
-- Simple HEAD request -- Simple HEAD request
head = function(url, options) head = function(url, options)
options = options or {} options = options or {}
return http.client.request("HEAD", url, nil, options) return http.client.request("HEAD", url, nil, options)
end, end,
-- Simple OPTIONS request -- Simple OPTIONS request
options = function(url, options) options = function(url, options)
return http.client.request("OPTIONS", url, nil, options) return http.client.request("OPTIONS", url, nil, options)
end, end,
-- Shorthand function to directly get JSON -- Shorthand function to directly get JSON
get_json = function(url, options) get_json = function(url, options)
options = options or {} options = options or {}
local response = http.client.get(url, options) local response = http.client.get(url, options)
if response.ok and response.json then if response.ok and response.json then
return response.json return response.json
end end
return nil, response return nil, response
end, end,
-- Utility to build a URL with query parameters -- Utility to build a URL with query parameters
build_url = function(base_url, params) build_url = function(base_url, params)
if not params or type(params) ~= "table" then if not params or type(params) ~= "table" then
return base_url return base_url
end end
local query = {} local query = {}
for k, v in pairs(params) do for k, v in pairs(params) do
if type(v) == "table" then if type(v) == "table" then
for _, item in ipairs(v) do for _, item in ipairs(v) do
table.insert(query, k .. "=" .. tostring(item)) table.insert(query, k .. "=" .. tostring(item))
end end
else else
table.insert(query, k .. "=" .. tostring(v)) table.insert(query, k .. "=" .. tostring(v))
end end
end end
if #query > 0 then if #query > 0 then
if base_url:find("?") then if base_url:find("?") then
return base_url .. "&" .. table.concat(query, "&") return base_url .. "&" .. table.concat(query, "&")
else else
return base_url .. "?" .. table.concat(query, "&") return base_url .. "?" .. table.concat(query, "&")
end end
end end
return base_url return base_url
end end
} }
} }
-- ====================================================================== -- ======================================================================
@ -235,15 +203,10 @@ local cookie = {
error("cookie.set: name must be a string", 2) error("cookie.set: name must be a string", 2)
end end
-- Get or create response local resp = __ensure_response()
local resp = __http_responses[1] or {}
resp.cookies = resp.cookies or {} resp.cookies = resp.cookies or {}
__http_responses[1] = resp
-- Handle options as table
local opts = options or {} local opts = options or {}
-- Create cookie table
local cookie = { local cookie = {
name = name, name = name,
value = value or "", value = value or "",
@ -251,7 +214,6 @@ local cookie = {
domain = opts.domain domain = opts.domain
} }
-- Handle expiry
if opts.expires then if opts.expires then
if type(opts.expires) == "number" then if type(opts.expires) == "number" then
if opts.expires > 0 then if opts.expires > 0 then
@ -266,13 +228,10 @@ local cookie = {
end end
end end
-- Security flags
cookie.secure = (opts.secure ~= false) cookie.secure = (opts.secure ~= false)
cookie.http_only = (opts.http_only ~= false) cookie.http_only = (opts.http_only ~= false)
-- Store in cookies table table.insert(resp.cookies, cookie)
local n = #resp.cookies + 1
resp.cookies[n] = cookie
return true return true
end, end,
@ -283,15 +242,12 @@ local cookie = {
error("cookie.get: name must be a string", 2) error("cookie.get: name must be a string", 2)
end end
-- Access values directly from current environment
local env = getfenv(2) local env = getfenv(2)
-- Check if context exists and has cookies
if env.ctx and env.ctx.cookies then if env.ctx and env.ctx.cookies then
return env.ctx.cookies[name] return env.ctx.cookies[name]
end end
-- If context has request_cookies map
if env.ctx and env.ctx._request_cookies then if env.ctx and env.ctx._request_cookies then
return env.ctx._request_cookies[name] return env.ctx._request_cookies[name]
end end
@ -305,185 +261,10 @@ local cookie = {
error("cookie.remove: name must be a string", 2) error("cookie.remove: name must be a string", 2)
end end
-- Create an expired cookie
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain}) return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
end end
} }
-- ======================================================================
-- SESSION MODULE
-- ======================================================================
local session = {
-- Get session value
get = function(key)
if type(key) ~= "string" then
error("session.get: key must be a string", 2)
end
local env = getfenv(2)
return env.__session_data and env.__session_data[key]
end,
-- Set session value
set = function(key, value)
if type(key) ~= "string" then
error("session.set: key must be a string", 2)
end
local env = getfenv(2)
print("SET ENV:", tostring(env)) -- Debug the environment
if not env.__session_data then
env.__session_data = {}
print("CREATED NEW SESSION TABLE")
end
env.__session_data[key] = value
env.__session_modified = true
print("SET:", key, "=", tostring(value), "MODIFIED:", env.__session_modified)
return true
end,
-- Delete session value
delete = function(key)
if type(key) ~= "string" then
error("session.delete: key must be a string", 2)
end
local env = getfenv(2)
if env.__session_data and env.__session_data[key] ~= nil then
env.__session_data[key] = nil
env.__session_modified = true
end
return true
end,
-- Clear all session data
clear = function()
local env = getfenv(2)
if env.__session_data and next(env.__session_data) then
env.__session_data = {}
env.__session_modified = true
end
return true
end,
-- Get session ID
get_id = function()
local env = getfenv(2)
return env.__session_id or ""
end,
-- Get all session data
get_all = function()
local env = getfenv(2)
return env.__session_data or {}
end,
-- Check if session has key
has = function(key)
if type(key) ~= "string" then
error("session.has: key must be a string", 2)
end
local env = getfenv(2)
return env.__session_data ~= nil and env.__session_data[key] ~= nil
end
}
-- ======================================================================
-- CSRF MODULE
-- ======================================================================
-- CSRF protection module
local csrf = {
-- Session key where the token is stored
TOKEN_KEY = "_csrf_token",
-- Default form field name
DEFAULT_FIELD = "csrf",
-- Generate a new CSRF token and store it in the session
generate = function(length)
-- Default length is 32 characters
length = length or 32
if length < 16 then
-- Enforce minimum security
length = 16
end
-- Check if we have a session module
if not session then
error("CSRF protection requires the session module", 2)
end
local token = __generate_token(length)
session.set(csrf.TOKEN_KEY, token)
return token
end,
-- Get the current token or generate a new one
token = function()
-- Get from session if exists
local token = session.get(csrf.TOKEN_KEY)
-- Generate if needed
if not token then
token = csrf.generate()
end
return token
end,
-- Generate a hidden form field with the CSRF token
field = function(field_name)
field_name = field_name or csrf.DEFAULT_FIELD
local token = csrf.token()
return string.format('<input type="hidden" name="%s" value="%s">', field_name, token)
end,
-- Verify a given token against the session token
verify = function(token, field_name)
field_name = field_name or csrf.DEFAULT_FIELD
local env = getfenv(2)
local form = nil
if env.ctx and env.ctx._request_form then
form = env.ctx._request_form
elseif env.ctx and env.ctx.form then
form = env.ctx.form
else
return false
end
token = token or form[field_name]
if not token then
return false
end
local session_token = session.get(csrf.TOKEN_KEY)
if not session_token then
return false
end
-- Constant-time comparison to prevent timing attacks
if #token ~= #session_token then
return false
end
local result = true
for i = 1, #token do
if token:sub(i, i) ~= session_token:sub(i, i) then
result = false
-- Don't break early - continue to prevent timing attacks
end
end
return result
end
}
-- ====================================================================== -- ======================================================================
-- UTIL MODULE -- UTIL MODULE
-- ====================================================================== -- ======================================================================
@ -575,6 +356,4 @@ local util = {
-- Install modules in global scope -- Install modules in global scope
_G.http = http _G.http = http
_G.cookie = cookie _G.cookie = cookie
_G.session = session
_G.csrf = csrf
_G.util = util _G.util = util