Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ vendor/
.trigger-debug
.trigger
valence/
/certs
85 changes: 85 additions & 0 deletions crates/hyperion/src/simulation/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,23 @@ fn handle_click_slot_inner<'a>(
return;
}

// Validate slot_changes to prevent exploits
if !validate_slot_changes(
&packet.slot_changes,
&inventories_mut,
&cursor_item.0,
player_only,
) {
resync_inventory(
compose,
&inventories_mut,
inv_state,
cursor_item,
packet.connection_id(),
);
return;
}

let mut cursor = cursor_item.0.clone();
let slots = packet.slot_changes.clone();

Expand Down Expand Up @@ -1191,6 +1208,74 @@ fn handle_drop_key(
event_writer.write(event);
}

fn validate_slot_changes(
slot_changes: &[SlotChange],
inventories_mut: &[&mut ItemSlot],
cursor_item: &ItemStack,
player_only: bool,
) -> bool {
// If cursor is empty, no slot changes should be valid
if cursor_item.is_empty() {
return false;
}

for slot_change in slot_changes {
// Validate slot index bounds
let Ok(slot_idx) = usize::try_from(slot_change.idx) else {
return false;
};

// Check if slot exists in our inventory
if slot_idx >= inventories_mut.len() {
return false;
}

let slot = &inventories_mut[slot_idx];

// Skip readonly slots
if slot.readonly {
return false;
}

// For player-only inventories, validate armor slot restrictions
if player_only && (5..=8).contains(&slot_idx) {
let is_valid = match slot_idx {
5 => cursor_item.item.is_helmet(),
6 => cursor_item.item.is_chestplate(),
7 => cursor_item.item.is_leggings(),
8 => cursor_item.item.is_boots(),
_ => true,
};
if !is_valid {
return false;
}
}

// Validate that the slot is either empty or contains the same item type
// This prevents creating items out of thin air
if !slot.stack.is_empty() {
// The slot should either be empty or contain the same item type as cursor
if slot.stack.item != cursor_item.item || slot.stack.nbt != cursor_item.nbt {
return false;
}

// For non-empty slots, they should have space available
if slot.stack.count >= slot.stack.item.max_stack() {
return false;
}
}
}

// Validate that the total number of items we're trying to distribute
// doesn't exceed what's in the cursor
let total_distributed_items = i32::try_from(slot_changes.len()).unwrap();
if total_distributed_items > i32::from(cursor_item.count) {
return false;
}

true
}

fn try_move_to_slot(source: &mut ItemStack, target: &mut ItemSlot) -> bool {
// Try to stack with existing items
if !target.stack.is_empty()
Expand Down