-- SecureHandlers.lua (Part of the new Secure Headers implementation) -- -- Lua code to support the various handlers and templates which can execute -- code in a secure, but restricted, environment. -- -- Daniel Stephens (iriel@vigilance-committee.org) -- Nevin Flanagan (alestane@comcast.net) --------------------------------------------------------------------------- -- Local references to things so that they can't be subverted local forceinsecure = forceinsecure; local geterrorhandler = geterrorhandler; local issecure = issecure; local newproxy = newproxy; local pairs = pairs; local pcall = pcall; local rawget = rawget; local scrub = scrub; local securecall = securecall; local select = select; local tostring = tostring; local type = type; local wipe = wipe; local GetCursorInfo = GetCursorInfo; local GetTime = GetTime; local InCombatLockdown = InCombatLockdown; local CallRestrictedClosure = CallRestrictedClosure; local GetFrameHandle = GetFrameHandle; local IsFrameHandle = IsFrameHandle; local RestrictedTable_create = rtable.newtable; -- SoftError(message) -- Report an error message without stopping execution local function SoftError_inner(message) local func = geterrorhandler(); func(message); end local function SoftError(message) securecall(pcall, SoftError_inner, message); end --------------------------------------------------------------------------- -- Working environments and control handles local LOCAL_Managed_Control_Frames = {}; setmetatable(LOCAL_Managed_Control_Frames, { __mode = "v"; }); local LOCAL_CTRL = {}; local LOCAL_Managed_Control_Proto = newproxy(true); do local mt = getmetatable(LOCAL_Managed_Control_Proto); mt.__index = LOCAL_CTRL; mt.__metatable = false; end local LOCAL_Managed_Controls = {}; setmetatable(LOCAL_Managed_Controls, { __mode = "k"; }); local function ManagedEnvironmentsIndex(t, k) if (not issecure() or type(k) ~= "table") then error("Invalid access of managed environments table"); return; end; local ownerHandle = GetFrameHandle(k, true); if (not ownerHandle) then error("Invalid access of managed environments table (bad frame)"); return; end local _, explicitProtected = ownerHandle:IsProtected(); if (not explicitProtected) then error("Invalid access of managed environments table (not protected)"); return; end local e = RestrictedTable_create(); e._G = e; e.owner = ownerHandle; local control = newproxy(LOCAL_Managed_Control_Proto); t[k] = e; LOCAL_Managed_Controls[e] = control; LOCAL_Managed_Control_Frames[control] = k; return e; end local LOCAL_Managed_Environments = {}; setmetatable(LOCAL_Managed_Environments, { __index = ManagedEnvironmentsIndex, __mode = "k", }); function GetManagedEnvironment(envKey) return rawget(LOCAL_Managed_Environments, envKey); end --------------------------------------------------------------------------- -- Standard invocation for header executions and child executions local function SecureHandler_Self_Execute(self, signature, body, ...) if (type(body) ~= "string") then return; end local selfHandle = GetFrameHandle(self, true); if (not selfHandle) then error("Invalid 'self' frame handle"); return; end local environment = LOCAL_Managed_Environments[self]; local controlHandle = LOCAL_Managed_Controls[environment]; return CallRestrictedClosure(signature, environment, controlHandle, body, selfHandle, ...); end local function SecureHandler_Other_Execute(header, self, signature, body, ...) if (type(body) ~= "string") then return; end local selfHandle = GetFrameHandle(self, true); if (not selfHandle) then return; end local environment = LOCAL_Managed_Environments[header]; local controlHandle = LOCAL_Managed_Controls[environment]; return CallRestrictedClosure(signature, environment, controlHandle, body, selfHandle, ...); end --------------------------------------------------------------------------- -- Script handlers for various header templates function SecureHandler_OnSimpleEvent(self, snippetAttr) local body = self:GetAttribute(snippetAttr); if (body) then SecureHandler_Self_Execute(self, "self", body); end end function SecureHandler_OnClick(self, snippetAttr, button, down) local body = self:GetAttribute(snippetAttr); if (body) then SecureHandler_Self_Execute(self, "self,button,down", body, button, down); end end function SecureHandler_OnMouseUpDown(self, snippetAttr, button) local body = self:GetAttribute(snippetAttr); if (body) then SecureHandler_Self_Execute(self, "self,button", body, button); end end function SecureHandler_OnMouseWheel(self, snippetAttr, delta) local body = self:GetAttribute(snippetAttr); if (body) then SecureHandler_Self_Execute(self, "self,delta", body, delta); end end function SecureHandler_StateOnAttributeChanged(self, name, value) local stateid = name:match("^state%-(.+)"); if (stateid) then local body = self:GetAttribute("_onstate-" .. stateid); if (body) then SecureHandler_Self_Execute(self, "self,stateid,newstate", body, stateid, value); end end end function SecureHandler_AttributeOnAttributeChanged(self, name, value) if (name:match("^_")) then return; end; local body = self:GetAttribute("_onattributechanged"); if (body) then SecureHandler_Self_Execute(self, "self,name,value", body, name, value); end end local function PickupAny(kind, target, detail, ...) if (kind == "clear") then ClearCursor(); kind, target, detail = target, detail, ...; end if kind == 'action' then PickupAction(target); elseif kind == 'bag' then PickupBagFromSlot(target) elseif kind == 'bagslot' then PickupContainerItem(target, detail) elseif kind == 'inventory' then PickupInventoryItem(target) elseif kind == 'item' then PickupItem(target) elseif kind == 'macro' then PickupMacro(target) elseif kind == 'merchant' then PickupMerchantItem(target) elseif kind == 'petaction' then PickupPetAction(target) elseif kind == 'money' then PickupPlayerMoney(target) elseif kind == 'spell' then PickupSpell(target, detail) elseif kind == 'companion' then PickupCompanion(target, detail) elseif kind == 'equipmentset' then PickupEquipmentSet(target); end end function SecureHandler_OnDragEvent(self, snippetAttr, button) local body = self:GetAttribute(snippetAttr); if (body) then PickupAny( SecureHandler_Self_Execute(self, "self,button,kind,value,...", body, button, GetCursorInfo()) ); end end --------------------------------------------------------------------------- -- "Wrap" handlers for alternate dispatch on various conditions -- such as OnClick, used for child handlers -- -- All wrappers use ... to make sure the original handler gets all of -- its arguments even if WoW is updated and this file is missed. -- 'Marker object' used to trigger unwrap of wrap closure local MAGIC_UNWRAP = newproxy(); local LOCAL_Wrapped_Handlers = {}; setmetatable(LOCAL_Wrapped_Handlers, { __mode = "k"; }); -- Create a closure to hold the data for a specific wrap local function CreateWrapClosure(handler, header, preBody, postBody) local wrap; if (postBody) then wrap = function(self, ...) if (self == MAGIC_UNWRAP) then return header, preBody, postBody; end return handler(self, header, preBody, postBody, wrap, ...); end else wrap = function(self, ...) if (self == MAGIC_UNWRAP) then return header, preBody, nil; end return handler(self, header, preBody, nil, wrap, ...); end end return wrap; end -- Save a hander against a specific wrap (securecall'ed for protection) local function SaveWrapHandler(frame, script, wrap) LOCAL_Wrapped_Handlers[wrap] = frame:GetScript(script) or false; end -- Restore a hander from a specific wrap (securecall'ed for protection) local function RestoreWrapHandler(frame, script, wrap) local old = LOCAL_Wrapped_Handlers[wrap]; if (old == nil) then return; end if (old == false) then old = nil; end frame:SetScript(script, old); return true; end -- Create a new handler wrapper and configure it -- -- frame - the frame that has the script -- script - the script name (such as OnClick) -- header - the secure header that owns the 'wrap' -- handler - the handler function that will process the event -- preBody - the snippet to execute before the wrapped handler -- postBody (optional) - the snippet to execute after the wrapped handler -- -- The resulting closure is called as (self, ...) -- -- The handler is invoked with (self, header, preBody, postBody, wrap, ...) -- where 'wrap' is the wrap closure itself (also the key to the wrapped -- handlers table) local function CreateWrapper(frame, script, header, handler, preBody, postBody) local wrap = CreateWrapClosure(handler, header, preBody, postBody); securecall(SaveWrapHandler, frame, script, wrap); return wrap; end -- Reverse the wrap process, restoring the wrapped handler (does nothing -- if the current handler is not wrapped) local function RemoveWrapper(frame, script) local wrap = frame:GetScript(script); if (not issecure()) then -- not valid return; end if (not securecall(RestoreWrapHandler, frame, script, wrap)) then -- not valid return; end -- Extract header, preBody, postBody return wrap(MAGIC_UNWRAP); end -- Invoke the wrapped handler for a wrapper, passing in all arguments local function SafeCallWrappedHandler(frame, wrap, ...) local handler = LOCAL_Wrapped_Handlers[wrap]; if (type(handler) == "function") then local ok, err = pcall(handler, frame, ...); if (not ok) then SoftError(err); end end end -- Check that a given frame is eligible for wrapped execution local function IsWrapEligible(frame) return (not InCombatLockdown()) or frame:IsProtected(); end -- Wrapper handler for clicks local function Wrapped_Click(self, header, preBody, postBody, wrap, button, down, ...) local message, newbutton; if ( IsWrapEligible(self) ) then newbutton, message = SecureHandler_Other_Execute(header, self, "self,button,down", preBody, button, down); if (newbutton == false) then return; end if (newbutton) then button = tostring(newbutton); end end securecall(SafeCallWrappedHandler, self, wrap, button, down, ...); if (postBody and message ~= nil) then SecureHandler_Other_Execute(header, self, "self,message,button,down", postBody, message, button, down); end; end local function Wrapped_OnEnter(self, header, preBody, postBody, wrap, motion, ...) local allow, message; if ( motion ) then self:SetAttribute("_wrapentered", true); if ( IsWrapEligible(self) ) then allow, message = SecureHandler_Other_Execute(header, self, "self", preBody); end if (allow == false) then return; end end securecall(SafeCallWrappedHandler, self, wrap, motion, ...); if (postBody and message ~= nil) then SecureHandler_Other_Execute(header, self, "self,message", postBody, message); end end local function Wrapped_OnLeave(self, header, preBody, postBody, wrap, motion, ...) local allow, message; if ( motion and self:GetAttribute("_wrapentered")) then self:SetAttribute("_wrapentered", nil); if ( IsWrapEligible(self) ) then allow, message = SecureHandler_Other_Execute(header, self, "self", preBody); if (allow == false) then return; end end end securecall(SafeCallWrappedHandler, self, wrap, motion, ...); if (postBody and message ~= nil) then SecureHandler_Other_Execute(header, self, "self,message", postBody, message); end end local function CreateSimpleWrapper(beforeSignature, afterSignature) return function(self, header, preBody, postBody, wrap, ...) local allow, message; if ( IsWrapEligible(self) ) then allow, message = SecureHandler_Other_Execute(header, self, beforeSignature, preBody, ...); if (allow == false) then return; end end securecall(SafeCallWrappedHandler, self, wrap, ...); if ( postBody and message ~= nil ) then SecureHandler_Other_Execute(header, self, afterSignature, postBody, message, ...); end end; end local Wrapped_ShowHide = CreateSimpleWrapper("self", "self,message"); local Wrapped_MouseWheel = CreateSimpleWrapper("self,offset", "self,message,offset"); local function Wrapped_Drag(self, header, preBody, postBody, wrap, ...) local message; if ( IsWrapEligible(self) ) then local selfHandle = GetFrameHandle(self, true); if (selfHandle) then local environment = LOCAL_Managed_Environments[header]; local controlHandle = LOCAL_Managed_Controls[environment]; local button = ...; local type, target, x1, x2, x3 = CallRestrictedClosure("self,button,kind,value,...", environment, controlHandle, preBody, selfHandle, button, GetCursorInfo()); if (type == false) then return; elseif (type == "message") then message = target; elseif (type) then PickupAny(type, target, x1, x2, x3); return; end end end securecall(SafeCallWrappedHandler, self, wrap, ...); if ( postBody and message ~= nil ) then SecureHandler_Other_Execute(header, self, "self,message,button", postBody, message, ...); end end local function Wrapped_Attribute(self, header, preBody, postBody, wrap, name, value, ...) local allow, message; if ( (not name:match("^_")) and IsWrapEligible(self) ) then allow, message = SecureHandler_Other_Execute(header, self, "self,name,value", preBody, name, value); if (allow == false) then return; end end securecall(SafeCallWrappedHandler, self, wrap, name, value, ...); if ( postBody and message ~= nil ) then SecureHandler_Other_Execute(header, self, "self,message,name,value", postBody, message, name, value); end end local LOCAL_Wrap_Handlers = { OnClick = Wrapped_Click; OnDoubleClick = Wrapped_Click; PreClick = Wrapped_Click; PostClick = Wrapped_Click; OnEnter = Wrapped_OnEnter; OnLeave = Wrapped_OnLeave; OnShow = Wrapped_ShowHide; OnHide = Wrapped_ShowHide; OnDragStart = Wrapped_Drag; OnReceiveDrag = Wrapped_Drag; OnMouseWheel = Wrapped_MouseWheel; OnAttributeChanged = Wrapped_Attribute; }; --------------------------------------------------------------------------- -- External API helpers, all are driven off a single control frame -- using OnAttributeChanged. -- Quick sanity check that a frame looks like a frame local function IsValidFrame(frame) return (type(frame) == "table") and (type(frame[0]) == "userdata"); end -- 'Action' handler for most of the API methods, invoked somewhat indirectly -- via attribute sets so as to allow secure execution (doesn't work from -- combat for user code (or indeed for any code)) local function API_OnAttributeChanged(self, name, value) if (value == nil) then return; end if (InCombatLockdown()) then -- This shouldn't ever happen because API frame is protected, -- but just in case someone does something silly... error("Cannot use SecureHandlers API during combat"); return; end -- _execute runs code in the context of a header if (name == "_execute") then local frame = self:GetAttribute("_apiframe"); self:SetAttribute("_execute", nil); if (type(value) ~= "string") then error("Invalid execute body"); return; end -- Most validation is performed by SecureHandler_Self_Execute SecureHandler_Self_Execute(frame, "self", value); return; end -- _wrap wraps a script handler in a secure wrapper if (name == "_wrap") then local frame = self:GetAttribute("_apiframe"); local header = self:GetAttribute("_apiheader"); local preBody = self:GetAttribute("_apiprebody"); local postBody = self:GetAttribute("_apipostbody"); self:SetAttribute("_wrap", nil); if (type(value) ~= "string") then error("Invalid wrap script id"); end if (not IsValidFrame(frame)) then error("Invalid wrap frame"); end if (not IsValidFrame(header)) then error("Invalid header frame"); end if (type(preBody) ~= "string") then error("Invalid pre-handler body"); end if (postBody ~= nil and type(postBody) ~= "string") then error("Invalid post-handler body"); end if (not select(2, header:IsProtected())) then error("Header frame must be explicitly protected"); end local script = value; if (not frame:HasScript(script)) then error("Frame does not support script '" .. script .. "'"); end if (not issecure()) then error("Wrap frame cannot be used"); end local handler = LOCAL_Wrap_Handlers[value]; if (not handler) then error("Unsupported script type '" .. value .. "'"); end local wrapper = CreateWrapper(frame, value, header, handler, preBody, postBody); frame:SetScript(script, wrapper); return; end -- _unwrap restores a previously wrapped handler if (name == "_unwrap") then local frame = self:GetAttribute("_apiframe"); local data = self:GetAttribute("_apidata"); if (data) then self:SetAttribute("_apidata", nil); end self:SetAttribute("_unwrap", nil); if (type(value) ~= "string") then error("Invalid unwrap script id"); end if (not IsValidFrame(frame)) then error("Invalid unwrap frame"); end local script = value; if (not frame:HasScript(script)) then error("Frame does not support script '" .. script .. "'"); end local header, preBody, postBody = RemoveWrapper(frame, script); if (type(data) == "table") then forceinsecure(); wipe(data); data[1] = frame; data[2] = script; data[3] = header; data[4] = preBody; data[5] = postBody; end return; end -- _frame-