VYPR
Moderate severityNVD Advisory· Published Oct 27, 2025· Updated Oct 28, 2025

InventoryGui allows item duplication in GUIs which use GuiStorageElement

CVE-2025-62784

Description

InventoryGui is a library for creating chest GUIs for Bukkit/Spigot plugins. Versions before 1.6.5 contain a vulnerability where any plugin using a GUI with the GuiStorageElement and allows taking out items out of that element can allow item duplication when the experimental Bundle item feature is enabled on the server. The vulnerability is resolved in version 1.6.5.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

InventoryGui library before 1.6.5 allows item duplication via GuiStorageElement when the experimental Bundle item feature is enabled.

Vulnerability

Overview

CVE-2025-62784 is a vulnerability in the InventoryGui library (versions before 1.6.5.6) that allows item duplication. The bug resides in the GuiStorageElement class, which is used by plugins to create chest GUIs that interact with inventories. When the experimental Bundle item feature is enabled on the server, a plugin using a GUI with GuiStorageElement and allowing item removal can trigger duplication of items [1][2].

Exploitation

Exploitation requires a server running a Bukkit/Spigot version that supports the experimental Bundle item feature (data version >= 4554, corresponding to Minecraft 1.21.9 or later). An attacker must have access to a GUI that uses GuiStorageElement and permits taking items out of that element. The attack vector is network-based, with low attack complexity and no special privileges required beyond normal player access [2].

Impact

Successful exploitation results in item duplication, which can disrupt server economies and gameplay balance. The vulnerability is classified with the library itself, not any specific plugin, meaning any plugin using the affected GuiStorageElement is potentially vulnerable [3].

Mitigation

The issue is fixed in InventoryGui version 1.6.5. The fix adds a version check that throws an UnsupportedOperationException on servers older than 1.21.9, preventing use of GuiStorageElement on unsupported versions unless the system property GuiStorageElement.forceAllow is explicitly set [1]. Users should update to version 1.6.5 or later [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
de.themoep:inventoryguiMaven
< 1.6.51.6.5

Affected products

2

Patches

1
690fc91d137c

Revert changes to storage elements

https://github.com/Phoenix616/InventoryGuiPhoenix616Oct 26, 2025via ghsa
2 files changed · +39 43
  • src/main/java/de/themoep/inventorygui/GuiStorageElement.java+37 16 modified
    @@ -22,6 +22,7 @@
      * SOFTWARE.
      */
     
    +import org.bukkit.Bukkit;
     import org.bukkit.ChatColor;
     import org.bukkit.Material;
     import org.bukkit.entity.HumanEntity;
    @@ -40,6 +41,8 @@
      * accessed by the player then it will translate to the second slot in the inventory.
      */
     public class GuiStorageElement extends GuiElement {
    +    private final static String STORAGE_SYSTEM_PROPERTY = GuiStorageElement.class.getName() + ".forceAllow";
    +
         private final Inventory storage;
         private final int invSlot;
         private Runnable applyStorage;
    @@ -54,7 +57,7 @@ public class GuiStorageElement extends GuiElement {
         public GuiStorageElement(char slotChar, Inventory storage) {
             this(slotChar, storage, -1);
         }
    -    
    +
         /**
          * An element used to access a specific slot in an {@link Inventory}.
          * @param slotChar  The character to replace in the gui setup string.
    @@ -64,7 +67,7 @@ public GuiStorageElement(char slotChar, Inventory storage) {
         public GuiStorageElement(char slotChar, Inventory storage, int invSlot) {
             this(slotChar, storage, invSlot, null, null);
         }
    -    
    +
         /**
          * An element used to access a specific slot in an {@link Inventory}.
          * @param slotChar      The character to replace in the gui setup string.
    @@ -91,6 +94,11 @@ public GuiStorageElement(char slotChar, Inventory storage, int invSlot, Runnable
          */
         public GuiStorageElement(char slotChar, Inventory storage, int invSlot, Runnable applyStorage, Function<ValidatorInfo, Boolean> placeValidator, Function<ValidatorInfo, Boolean> takeValidator) {
             super(slotChar, null);
    +        if (Bukkit.getUnsafe().getDataVersion() < 4554 && !Boolean.getBoolean(STORAGE_SYSTEM_PROPERTY)) {
    +            throw new UnsupportedOperationException("GuiStorageElements require server version 1.21.9 or later to work properly!"
    +                    + " If you understand the risk and only want to use this element for internal use you can disable this check"
    +                    + " by setting the system property " + STORAGE_SYSTEM_PROPERTY + "=true");
    +        }
             this.invSlot = invSlot;
             this.applyStorage = applyStorage;
             this.placeValidator = placeValidator;
    @@ -99,15 +107,24 @@ public GuiStorageElement(char slotChar, Inventory storage, int invSlot, Runnable
                 if (getStorageSlot(click.getWhoClicked(), click.getSlot()) < 0) {
                     return true;
                 }
    +            ItemStack storageItem = getStorageItem(click.getWhoClicked(), click.getSlot());
                 ItemStack slotItem = GuiView.of(click.getRawEvent().getView()).getTopInventory().getItem(click.getSlot());
     
                 if (click.getType() == ClickType.RIGHT && (
                         click.getCursor() != null && click.getCursor().getType().getKey().getKey().contains("bundle")
    +                            || storageItem != null && storageItem.getType().getKey().getKey().contains("bundle")
                                 || slotItem != null && slotItem.getType().getKey().getKey().contains("bundle"))) {
                     gui.draw(click.getWhoClicked(), false);
                     return true;
                 }
     
    +            if (slotItem == null && storageItem != null && storageItem.getType() != Material.AIR
    +                    || storageItem == null && slotItem != null && slotItem.getType() != Material.AIR
    +                    || storageItem != null && !storageItem.equals(slotItem)) {
    +                gui.draw(click.getWhoClicked(), false);
    +                return true;
    +            }
    +
                 if (!(click.getRawEvent() instanceof InventoryClickEvent)) {
                     // Only the click event will be handled here, drag event is handled separately
                     return true;
    @@ -245,14 +262,18 @@ public GuiStorageElement(char slotChar, Inventory storage, int invSlot, Runnable
                         click.getRawEvent().getWhoClicked().sendMessage(ChatColor.RED + "The action " + event.getAction() + " is not supported! Sorry about that :(");
                         return true;
                 }
    -            return !validateItemPlace(click.getWhoClicked(), click.getSlot(), movedItem);
    +            return !setStorageItem(click.getWhoClicked(), click.getSlot(), movedItem);
             });
             this.storage = storage;
         }
     
         @Override
         public ItemStack getItem(HumanEntity who, int slot) {
    -        return getStorageItem(who, slot);
    +        int index = getStorageSlot(who, slot);
    +        if (index > -1 && index < storage.getSize()) {
    +            return storage.getItem(index);
    +        }
    +        return null;
         }
     
         /**
    @@ -262,7 +283,7 @@ public ItemStack getItem(HumanEntity who, int slot) {
         public Inventory getStorage() {
             return storage;
         }
    -    
    +
         /**
          * Get the storage slot index that corresponds to the InventoryGui slot
          * @param player    The player which is using the GUI view
    @@ -276,7 +297,7 @@ private int getStorageSlot(HumanEntity player, int slot) {
             }
             return index;
         }
    -    
    +
         /**
          * Get the item in the storage that corresponds to the InventoryGui slot
          * @param slot      The slot in the GUI
    @@ -301,7 +322,7 @@ public ItemStack getStorageItem(HumanEntity player, int slot) {
             }
             return null;
         }
    -    
    +
         /**
          * Set the item in the storage that corresponds to the InventoryGui slot.
          * @param slot  The slot in the GUI
    @@ -335,15 +356,15 @@ public boolean setStorageItem(HumanEntity player, int slot, ItemStack item) {
             }
             return true;
         }
    -    
    +
         /**
          * Get the runnable that applies the storage
          * @return The storage applying runnable; might be null
          */
         public Runnable getApplyStorage() {
             return applyStorage;
         }
    -    
    +
         /**
          * Set what should be done to apply the storage.
          * Not necessary if the storage is directly backed by a real inventory.
    @@ -352,7 +373,7 @@ public Runnable getApplyStorage() {
         public void setApplyStorage(Runnable applyStorage) {
             this.applyStorage = applyStorage;
         }
    -    
    +
         /**
          * Get the item place validator
          * @return The item place validator
    @@ -378,7 +399,7 @@ public Function<ValidatorInfo, Boolean> getPlaceValidator() {
         public Function<ValidatorInfo, Boolean> getTakeValidator() {
             return takeValidator;
         }
    -    
    +
         /**
          * Set a function that can validate whether an item can be placed in the slot
          * @param placeValidator The item validator that takes a {@link ValidatorInfo} and returns <code>true</code> for items that
    @@ -407,7 +428,7 @@ public void setPlaceValidator(Function<ValidatorInfo, Boolean> placeValidator) {
         public void setTakeValidator(Function<ValidatorInfo, Boolean> takeValidator) {
             this.takeValidator = takeValidator;
         }
    -    
    +
         /**
          * Validate whether an item can be placed in a slot with the item validator set in {@link #setItemValidator(Function)}
          * @param slot  The slot the item should be tested for
    @@ -455,21 +476,21 @@ public static class ValidatorInfo {
             private final GuiElement element;
             private final int slot;
             private final ItemStack item;
    -    
    +
             public ValidatorInfo(GuiElement element, int slot, ItemStack item) {
                 this.item = item;
                 this.slot = slot;
                 this.element = element;
             }
    -    
    +
             public GuiElement getElement() {
                 return element;
             }
    -    
    +
             public int getSlot() {
                 return slot;
             }
    -    
    +
             public ItemStack getItem() {
                 return item;
             }
    
  • src/main/java/de/themoep/inventorygui/InventoryGui.java+2 27 modified
    @@ -527,12 +527,8 @@ public void setPageNumber(int pageNumber) {
          * @param pageNumber    The page number to set
          */
         public void setPageNumber(HumanEntity player, int pageNumber) {
    -        Inventory inventory = getInventory(player);
    -        if (inventory != null) {
    -            storeItems(player, inventory);
    -        }
             setPageNumberInternal(player, pageNumber);
    -        draw(player, false, false, false);
    +        draw(player, false, false);
         }
     
         private void setPageNumberInternal(HumanEntity player, int pageNumber) {
    @@ -684,18 +680,11 @@ public void draw(HumanEntity who, boolean updateDynamic) {
          * @param recreateInventory Recreate the inventory
          */
         public void draw(HumanEntity who, boolean updateDynamic, boolean recreateInventory) {
    -        draw(who, updateDynamic, recreateInventory, true);
    -    }
    -
    -    private void draw(HumanEntity who, boolean updateDynamic, boolean recreateInventory, boolean storeItems) {
             if (updateDynamic) {
                 updateElements(who, elements.values());
             }
             calculatePageAmount(who);
             Inventory inventory = getInventory(who);
    -        if (storeItems && inventory != null) {
    -            storeItems(who, inventory);
    -        }
             if (inventory == null || recreateInventory) {
                 build();
                 if (slots.length != inventoryType.getDefaultSize()) {
    @@ -718,19 +707,6 @@ private void draw(HumanEntity who, boolean updateDynamic, boolean recreateInvent
             }
         }
     
    -    private void storeItems(HumanEntity who, Inventory inventory) {
    -        for (int i = 0; i < inventory.getSize(); i++) {
    -            GuiElement element = getElement(i);
    -            if (element != null) {
    -                GuiElement effectiveElement = element.getEffectiveElement(who, i);
    -                if (effectiveElement instanceof GuiStorageElement) {
    -                    GuiStorageElement storageElement = (GuiStorageElement) effectiveElement;
    -                    storageElement.setStorageItem(who, i, inventory.getItem(i));
    -                }
    -            }
    -        }
    -    }
    -
         /**
          * Schedule a task on a {@link HumanEntity}/main thread to run on the next tick
          * @param entity the human entity to schedule a task on
    @@ -1393,7 +1369,7 @@ public void onInventoryDrag(InventoryDragEvent event) {
                         if (items.getKey() < inventory.getSize()) {
                             GuiElement element = getElement(items.getKey());
                             if (!(element instanceof GuiStorageElement)
    -                                || !((GuiStorageElement) element).validateItemPlace(event.getWhoClicked(), items.getKey(), items.getValue())) {
    +                                || !((GuiStorageElement) element).setStorageItem(event.getWhoClicked(), items.getKey(), items.getValue())) {
                                 ItemStack slotItem = event.getInventory().getItem(items.getKey());
                                 if (!items.getValue().isSimilar(slotItem)) {
                                     rest += items.getValue().getAmount();
    @@ -1440,7 +1416,6 @@ public void onInventoryDrag(InventoryDragEvent event) {
             public void onInventoryClose(InventoryCloseEvent event) {
                 Inventory inventory = getInventory(event.getPlayer());
                 if (event.getInventory().equals(inventory)) {
    -                storeItems(event.getPlayer(), inventory);
                     // go back. that checks if the player is in gui and has history
                     if (InventoryGui.this.equals(getOpen(event.getPlayer()))) {
                         if (closeAction == null || closeAction.onClose(new Close(event.getPlayer(), InventoryGui.this, event))) {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.