optimize sandbox
This commit is contained in:
parent
0abf31ed3a
commit
ba9a3db0a0
|
@ -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
|
||||
}
|
|
@ -2,7 +2,6 @@ package http
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"Moonshark/core/metadata"
|
||||
|
@ -167,14 +166,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
|||
luaCtx.Set("path", path)
|
||||
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
|
||||
if params.Count > 0 {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
||||
if err != nil {
|
||||
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
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
|
@ -228,15 +205,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
|||
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
|
||||
runner.ApplyResponse(response, ctx)
|
||||
|
||||
|
|
|
@ -115,19 +115,7 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
|
|||
// Create a response object
|
||||
response := NewResponse()
|
||||
|
||||
// Load bytecode
|
||||
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
|
||||
// Get the execution function first
|
||||
state.GetGlobal("__execute_script")
|
||||
if !state.IsFunction(-1) {
|
||||
state.Pop(1)
|
||||
|
@ -135,11 +123,19 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
|
|||
return nil, ErrSandboxNotInitialized
|
||||
}
|
||||
|
||||
// Push function and bytecode
|
||||
state.PushCopy(-2) // Bytecode
|
||||
state.PushCopy(-2) // Context
|
||||
state.Remove(-4) // Remove bytecode duplicate
|
||||
state.Remove(-3) // Remove context duplicate
|
||||
// Load bytecode
|
||||
if err := state.LoadBytecode(bytecode, "script"); err != nil {
|
||||
state.Pop(1) // Pop the __execute_script function
|
||||
ReleaseResponse(response)
|
||||
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
|
||||
if err := state.Call(2, 1); err != nil {
|
||||
|
@ -222,32 +218,8 @@ func extractHTTPResponseData(state *luajit.State, response *Response) {
|
|||
}
|
||||
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
|
||||
state.Pop(1)
|
||||
state.Pop(2)
|
||||
}
|
||||
|
||||
// extractCookie pulls cookie data from the current table on the stack
|
||||
|
|
|
@ -6,14 +6,10 @@ including core modules and utilities. It's designed to be embedded in the
|
|||
Go binary at build time.
|
||||
]]--
|
||||
|
||||
-- Global tables for execution context
|
||||
__http_responses = {}
|
||||
__http_response = {}
|
||||
__module_paths = {}
|
||||
__module_bytecode = {}
|
||||
__ready_modules = {}
|
||||
__session_data = {}
|
||||
__session_id = nil
|
||||
__session_modified = false
|
||||
|
||||
-- ======================================================================
|
||||
-- CORE SANDBOX FUNCTIONALITY
|
||||
|
@ -21,15 +17,12 @@ __session_modified = false
|
|||
|
||||
-- Create environment inheriting from _G
|
||||
function __create_env(ctx)
|
||||
-- Create environment with metatable inheriting from _G
|
||||
local env = setmetatable({}, {__index = _G})
|
||||
|
||||
-- Add context if provided
|
||||
if ctx then
|
||||
env.ctx = ctx
|
||||
end
|
||||
|
||||
-- Add proper require function to this environment
|
||||
if __setup_require then
|
||||
__setup_require(env)
|
||||
end
|
||||
|
@ -39,49 +32,27 @@ end
|
|||
|
||||
-- Execute script with clean environment
|
||||
function __execute_script(fn, ctx)
|
||||
-- Clear previous responses
|
||||
__http_responses[1] = nil
|
||||
__http_response = nil
|
||||
|
||||
-- Create environment with metatable inheriting from _G
|
||||
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
|
||||
local env = __create_env(ctx)
|
||||
setfenv(fn, env)
|
||||
|
||||
-- Execute with protected call
|
||||
local ok, result = pcall(fn)
|
||||
if not ok then
|
||||
error(result, 0)
|
||||
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
|
||||
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
|
||||
-- ======================================================================
|
||||
|
@ -94,9 +65,8 @@ local http = {
|
|||
error("http.set_status: status code must be a number", 2)
|
||||
end
|
||||
|
||||
local resp = __http_responses[1] or {}
|
||||
local resp = __ensure_response()
|
||||
resp.status = code
|
||||
__http_responses[1] = resp
|
||||
end,
|
||||
|
||||
-- Set HTTP header
|
||||
|
@ -105,10 +75,9 @@ local http = {
|
|||
error("http.set_header: name and value must be strings", 2)
|
||||
end
|
||||
|
||||
local resp = __http_responses[1] or {}
|
||||
local resp = __ensure_response()
|
||||
resp.headers = resp.headers or {}
|
||||
resp.headers[name] = value
|
||||
__http_responses[1] = resp
|
||||
end,
|
||||
|
||||
-- Set content type; set_header helper
|
||||
|
@ -122,10 +91,9 @@ local http = {
|
|||
error("http.set_metadata: key must be a string", 2)
|
||||
end
|
||||
|
||||
local resp = __http_responses[1] or {}
|
||||
local resp = __ensure_response()
|
||||
resp.metadata = resp.metadata or {}
|
||||
resp.metadata[key] = value
|
||||
__http_responses[1] = resp
|
||||
end,
|
||||
|
||||
-- HTTP client submodule
|
||||
|
@ -235,15 +203,10 @@ local cookie = {
|
|||
error("cookie.set: name must be a string", 2)
|
||||
end
|
||||
|
||||
-- Get or create response
|
||||
local resp = __http_responses[1] or {}
|
||||
local resp = __ensure_response()
|
||||
resp.cookies = resp.cookies or {}
|
||||
__http_responses[1] = resp
|
||||
|
||||
-- Handle options as table
|
||||
local opts = options or {}
|
||||
|
||||
-- Create cookie table
|
||||
local cookie = {
|
||||
name = name,
|
||||
value = value or "",
|
||||
|
@ -251,7 +214,6 @@ local cookie = {
|
|||
domain = opts.domain
|
||||
}
|
||||
|
||||
-- Handle expiry
|
||||
if opts.expires then
|
||||
if type(opts.expires) == "number" then
|
||||
if opts.expires > 0 then
|
||||
|
@ -266,13 +228,10 @@ local cookie = {
|
|||
end
|
||||
end
|
||||
|
||||
-- Security flags
|
||||
cookie.secure = (opts.secure ~= false)
|
||||
cookie.http_only = (opts.http_only ~= false)
|
||||
|
||||
-- Store in cookies table
|
||||
local n = #resp.cookies + 1
|
||||
resp.cookies[n] = cookie
|
||||
table.insert(resp.cookies, cookie)
|
||||
|
||||
return true
|
||||
end,
|
||||
|
@ -283,15 +242,12 @@ local cookie = {
|
|||
error("cookie.get: name must be a string", 2)
|
||||
end
|
||||
|
||||
-- Access values directly from current environment
|
||||
local env = getfenv(2)
|
||||
|
||||
-- Check if context exists and has cookies
|
||||
if env.ctx and env.ctx.cookies then
|
||||
return env.ctx.cookies[name]
|
||||
end
|
||||
|
||||
-- If context has request_cookies map
|
||||
if env.ctx and env.ctx._request_cookies then
|
||||
return env.ctx._request_cookies[name]
|
||||
end
|
||||
|
@ -305,185 +261,10 @@ local cookie = {
|
|||
error("cookie.remove: name must be a string", 2)
|
||||
end
|
||||
|
||||
-- Create an expired cookie
|
||||
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
|
||||
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
|
||||
-- ======================================================================
|
||||
|
@ -575,6 +356,4 @@ local util = {
|
|||
-- Install modules in global scope
|
||||
_G.http = http
|
||||
_G.cookie = cookie
|
||||
_G.session = session
|
||||
_G.csrf = csrf
|
||||
_G.util = util
|
||||
|
|
Loading…
Reference in New Issue
Block a user