UIDropDownMenu_GetSelectedID taints dropdown initialization

Calling UIDropDownMenu_Initialize may taint the current execution if an addon has used the dropdown infrastructure recently, and if the dropdown being initialized does not have a selected name, but does have a selected value that doesn't correspond to its first entry.

In Blizzard_PVPUI, a dropdown is used to select a PvP queue type, preventing the player from joining PvP queues if the dropdown becomes tainted. In patch 7.3.5, this could happen when Blizzard_PVPUI loaded, without additional player interaction.

Fixed in: UIDropDownMenu_GetSelectedID only iterates through entries in the current menu, and UIDropDownMenu_CreateInfo always returns fresh tables.

Affected versions: 7.3.5, 8.0.1.

To reproduce (HonorFrame.type)

  1. Create the following macro:
    Patch 8.0.1
    /click LFDMicroButton
    /click PVEFrameTab2
    /click HonorFrameTypeDropDownButton
    /click DropDownList1Button2
    /run WhoFrameDropDownButton:Click()
    /click HonorFrameTypeDropDownButton
    /click DropDownList1Button1
    /click HonorFrameQueueButton
    The WhoFrameDropDownButton click simulates opening a tainted dropdown, keeping this within 255 chars.
    Patch 7.3.5
    /run EasyMenu({{text="Boo"},{text="Hiss"}}, CreateFrame("Frame", "T", nil, "UIDropDownMenuTemplate"), "cursor", 0,0,"MENU")
    /click LFDMicroButton
    /click PVEFrameTab2
    /click HonorFrameQueueButton
  2. Place it on your action bars.
  3. Reload the UI: run /reload
  4. Click the macro on your action bar.
  5. A macro script has been blocked form an action only available to the Blizzard UI.

How this gets tainted

  1. HonorFrame_Queue accesses HonorFrame.type prior to calling JoinBattlefield.
  2. HonorFrame.type is set by HonorFrame_OnLoad calling HonorFrame_SetType after initializing HonorFrameTypeDropDown, or via the dropdown calling HonorFrameTypeDropDown_OnClick.
  3. Execution is tainted when HonorFrameTypeDropDown_Initialize calls UIDropDownMenu_AddButton:
    • If things are already very wrong, the secure info table returned by UIDropDownMenu_CreateInfo may already have tainted keys (associated with nil values) from another initially-secure dropdown becoming tainted halfway through its initialization. These will keys will taint the execution when _AddButton copies them from the info table to the widget table.
    • Otherwise, having the frame.selectedValue key set (as it would be on subsequent initializations of HonorFrameTypeDropDown) causes UIDropDownMenu_GetSelectedID to check all DropDownList1ButtonX.value entries until a match is found, including those on buttons which are not yet initialized for the current dropdown, and may be tainted if used by an addon.

Possible fixes

  1. "I don't want to touch dropdowns."
    Changing HonorFrame.type to an attribute would prevent the dropdown from causing issues for PVPUI's Join Battle button.
  2. "Tainted keys in the secure info table?"
    securecall(wipe, UIDropDownMenu_SecureInfo) does not clear taint associated with the table entries it removes:
    /run x = {x="1"} wipe(x) print(issecurevariable(x, "x")) -- false
    Patching wipe to also cleanse tainted keys would remove some of the potential propagation routes; alternatively, UIDropDownMenu_CreateInfo could always return a fresh table if the execution is secure.
    Tangent: accessing a tainted key associated with a nil value should not taint the execution (current behavior affords no actual security, as forcing a table rehash will clear the tainted key).
  3. "How do you taint a dropdown mid-initialization?"
    UIDropDownMenu_GetSelectedID, called by _AddButton, scans all buttons up to UIDROPDOWNMENU_MAXBUTTONS (rather than just those present in the current dropdown per list.numButtons). This can cause otherwise secure FrameXML dropdown initializations to access buttons previously used by addons, and hence tainted button.value keys.

AddOn workaround

You can work around the UIDropDownMenu_GetSelectedID issue by including the following code in your addon:

if (UIDROPDOWNMENU_VALUE_PATCH_VERSION or 0) < 2 then UIDROPDOWNMENU_VALUE_PATCH_VERSION = 2 hooksecurefunc("UIDropDownMenu_InitializeHelper", function() if UIDROPDOWNMENU_VALUE_PATCH_VERSION ~= 2 then return end for i=1, UIDROPDOWNMENU_MAXLEVELS do for j=1, UIDROPDOWNMENU_MAXBUTTONS do local b = _G["DropDownList" .. i .. "Button" .. j] if not (issecurevariable(b, "value") or b:IsShown()) then b.value = nil repeat j, b["fx" .. j] = j+1 until issecurevariable(b, "value") end end end end) end