From 5236f32c30b1d9e982aa31009ca1150b2a296406 Mon Sep 17 00:00:00 2001 From: MidSpike Date: Tue, 4 Aug 2026 21:47:39 -0400 Subject: [PATCH 1/3] Fix: `/back` onto nether roof exploit when destination is unsafe. - Created crude `isAboveNetherRoof` safeguard for `getSafeDestination`. --- .../com/earth2me/essentials/utils/LocationUtil.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java index f9eb69c20cd..b8fceb3a1eb 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java @@ -125,6 +125,13 @@ public static boolean isBlockOutsideWorldBorder(final World world, final int x, return x < x1 || x > x2 || z < z1 || z > z2; } + public static boolean isAboveNetherRoof(final World world, final int x, final int y, final int z) { + if (world.getEnvironment() != World.Environment.NETHER) { + return false; + } + return y >= world.getHighestBlockYAt(x, z); + } + public static int getXInsideWorldBorder(final World world, final int x) { final Location center = world.getWorldBorder().getCenter(); final int radius = (int) world.getWorldBorder().getSize() / 2; @@ -277,6 +284,9 @@ public static Location getSafeDestination(IEssentials ess, final Location loc) t } } } + if (isAboveNetherRoof(world, x, y, z)) { + y = world.getHighestBlockYAt(x, z) - 1; + } return new Location(world, x + 0.5, y, z + 0.5, loc.getYaw(), loc.getPitch()); } From 45cbd085fa00037d48ca139978b204e95aaed6a2 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:15:08 -0700 Subject: [PATCH 2/3] Fix Nether teleport safety height handling --- .../essentials/utils/LocationUtil.java | 22 ++++------ .../com/earth2me/essentials/UtilTest.java | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java index b8fceb3a1eb..6a806b7e259 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java @@ -125,13 +125,6 @@ public static boolean isBlockOutsideWorldBorder(final World world, final int x, return x < x1 || x > x2 || z < z1 || z > z2; } - public static boolean isAboveNetherRoof(final World world, final int x, final int y, final int z) { - if (world.getEnvironment() != World.Environment.NETHER) { - return false; - } - return y >= world.getHighestBlockYAt(x, z); - } - public static int getXInsideWorldBorder(final World world, final int x) { final Location center = world.getWorldBorder().getCenter(); final int radius = (int) world.getWorldBorder().getSize() / 2; @@ -174,6 +167,10 @@ public static boolean isBlockUnsafe(IEssentials ess, final World world, final in return isBlockDamaging(world, x, y, z) || isBlockAboveAir(ess, world, x, y, z); } + private static boolean isBlockUnsafe(IEssentials ess, final World world, final int x, final int y, final int z, final int maxY) { + return y >= maxY || isBlockUnsafe(ess, world, x, y, z); + } + public static boolean isBlockDamaging(final World world, final int x, final int y, final int z) { final Material block = world.getBlockAt(x, y, z).getType(); final Material below = world.getBlockAt(x, y - 1, z).getType(); @@ -249,12 +246,12 @@ public static Location getSafeDestination(IEssentials ess, final Location loc) t break; } } - if (isBlockUnsafe(ess, world, x, y, z)) { + if (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) { x = Math.round(loc.getX()) == origX ? x - 1 : x + 1; z = Math.round(loc.getZ()) == origZ ? z - 1 : z + 1; } int i = 0; - while (isBlockUnsafe(ess, world, x, y, z)) { + while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) { i++; if (i >= VOLUME.length) { x = origX; @@ -266,14 +263,14 @@ public static Location getSafeDestination(IEssentials ess, final Location loc) t y = NumberUtil.constrainToRange(origY + VOLUME[i].y, worldMinY, worldMaxY); z = origZ + VOLUME[i].z; } - while (isBlockUnsafe(ess, world, x, y, z)) { + while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) { y += 1; if (y >= worldMaxY) { x += 1; break; } } - while (isBlockUnsafe(ess, world, x, y, z)) { + while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) { y -= 1; if (y <= worldMinY + 1) { x += 1; @@ -284,9 +281,6 @@ public static Location getSafeDestination(IEssentials ess, final Location loc) t } } } - if (isAboveNetherRoof(world, x, y, z)) { - y = world.getHighestBlockYAt(x, z) - 1; - } return new Location(world, x + 0.5, y, z + 0.5, loc.getYaw(), loc.getPitch()); } diff --git a/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java b/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java index 33c88ea4485..19043849879 100644 --- a/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java +++ b/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java @@ -3,6 +3,12 @@ import com.earth2me.essentials.utils.DateUtil; import com.earth2me.essentials.utils.LocationUtil; import com.earth2me.essentials.utils.VersionUtil; +import net.ess3.provider.WorldInfoProvider; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.WorldBorder; +import org.bukkit.block.Block; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,6 +24,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class UtilTest { @@ -66,6 +75,38 @@ public void testSafeLocation() { assertEquals(diameter * diameter * diameter, count); } + @Test + public void testSafeLocationRespectsLogicalHeight() throws Exception { + final IEssentials essentials = mock(IEssentials.class); + final WorldInfoProvider worldInfoProvider = mock(WorldInfoProvider.class); + final World world = mock(World.class); + final WorldBorder worldBorder = mock(WorldBorder.class); + final Block solid = mock(Block.class); + final Block hollow = mock(Block.class); + + when(essentials.provider(WorldInfoProvider.class)).thenReturn(worldInfoProvider); + when(worldInfoProvider.getMinHeight(world)).thenReturn(0); + when(worldInfoProvider.getLogicalHeight(world)).thenReturn(128); + when(worldInfoProvider.getMaxHeight(world)).thenReturn(256); + when(world.getWorldBorder()).thenReturn(worldBorder); + when(worldBorder.getCenter()).thenReturn(new Location(world, 0, 0, 0)); + when(worldBorder.getSize()).thenReturn(60_000_000D); + when(solid.getType()).thenReturn(Material.BEDROCK); + when(hollow.getType()).thenReturn(Material.LIGHT); + when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + final int x = invocation.getArgument(0); + final int y = invocation.getArgument(1); + final int z = invocation.getArgument(2); + return y >= 128 || x == 1 && z == 0 && (y == 100 || y == 101) ? hollow : solid; + }); + + final Location safe = LocationUtil.getSafeDestination(essentials, new Location(world, 0, 64, 0)); + + assertEquals(1, safe.getBlockX()); + assertEquals(100, safe.getBlockY()); + assertFalse(LocationUtil.isBlockUnsafe(essentials, world, safe.getBlockX(), safe.getBlockY(), safe.getBlockZ())); + } + @Test public void testFDDnow() { final Calendar c = new GregorianCalendar(); From 3aab6e4d3d537c34cee59eafb0821fe917ffc235 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:43:24 -0700 Subject: [PATCH 3/3] setting --- .../com/earth2me/essentials/ISettings.java | 2 ++ .../com/earth2me/essentials/Settings.java | 5 ++++ .../essentials/utils/LocationUtil.java | 4 +++- Essentials/src/main/resources/config.yml | 4 ++++ .../com/earth2me/essentials/UtilTest.java | 24 +++++++++++++++---- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 11566ca0459..7542e2b39f8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -136,6 +136,8 @@ public interface ISettings extends IConf { boolean isAlwaysTeleportSafety(); + boolean isConsiderWorldHeightForTeleportSafety(); + boolean isTeleportPassengerDismount(); boolean isForcePassengerTeleport(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index fbec7ea904a..69c8a0b3e17 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -297,6 +297,11 @@ public boolean isAlwaysTeleportSafety() { return config.getBoolean("force-safe-teleport-location", false); } + @Override + public boolean isConsiderWorldHeightForTeleportSafety() { + return config.getBoolean("consider-world-height-for-teleport-safety", false); + } + @Override public boolean isTeleportPassengerDismount() { return config.getBoolean("teleport-passenger-dismount", true); diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java index 6a806b7e259..18faec97f30 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/LocationUtil.java @@ -228,7 +228,9 @@ public static Location getSafeDestination(IEssentials ess, final Location loc) t final World world = loc.getWorld(); final int worldMinY = worldInfoProvider.getMinHeight(world); final int worldLogicalY = worldInfoProvider.getLogicalHeight(world); - final int worldMaxY = loc.getBlockY() < worldLogicalY ? worldLogicalY : worldInfoProvider.getMaxHeight(world); + final int worldMaxY = ess.getSettings().isConsiderWorldHeightForTeleportSafety() && loc.getBlockY() < worldLogicalY + ? worldLogicalY + : worldInfoProvider.getMaxHeight(world); int x = loc.getBlockX(); int y = (int) Math.round(loc.getY()); int z = loc.getBlockZ(); diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index eb441235b6b..5b176ffb807 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -99,6 +99,10 @@ force-disable-teleport-safety: false # safe location. If you'd like players to be teleported to a safe location all of the time, set this option to true. force-safe-teleport-location: false +# When teleporting to an unsafe location, should Essentials consider the world's logical height when finding a safe destination? +# This prevents safety checks from moving players above the Nether roof unless the requested destination is already above it. +consider-world-height-for-teleport-safety: true + # Consider water blocks as "safe", therefore allowing players to teleport # using commands such as /home or /spawn to a location that is occupied by water blocks. is-water-safe: false diff --git a/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java b/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java index 19043849879..2922e56481b 100644 --- a/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java +++ b/Essentials/src/test/java/com/earth2me/essentials/UtilTest.java @@ -77,7 +77,23 @@ public void testSafeLocation() { @Test public void testSafeLocationRespectsLogicalHeight() throws Exception { + final Location result = getSafeDestinationWithLogicalHeightSetting(true); + + assertEquals(1, result.getBlockX()); + assertEquals(100, result.getBlockY()); + } + + @Test + public void testSafeLocationIgnoresLogicalHeightWhenDisabled() throws Exception { + final Location result = getSafeDestinationWithLogicalHeightSetting(false); + + assertEquals(0, result.getBlockX()); + assertEquals(128, result.getBlockY()); + } + + private Location getSafeDestinationWithLogicalHeightSetting(final boolean considerWorldHeight) throws Exception { final IEssentials essentials = mock(IEssentials.class); + final ISettings settings = mock(ISettings.class); final WorldInfoProvider worldInfoProvider = mock(WorldInfoProvider.class); final World world = mock(World.class); final WorldBorder worldBorder = mock(WorldBorder.class); @@ -85,6 +101,8 @@ public void testSafeLocationRespectsLogicalHeight() throws Exception { final Block hollow = mock(Block.class); when(essentials.provider(WorldInfoProvider.class)).thenReturn(worldInfoProvider); + when(essentials.getSettings()).thenReturn(settings); + when(settings.isConsiderWorldHeightForTeleportSafety()).thenReturn(considerWorldHeight); when(worldInfoProvider.getMinHeight(world)).thenReturn(0); when(worldInfoProvider.getLogicalHeight(world)).thenReturn(128); when(worldInfoProvider.getMaxHeight(world)).thenReturn(256); @@ -100,11 +118,7 @@ public void testSafeLocationRespectsLogicalHeight() throws Exception { return y >= 128 || x == 1 && z == 0 && (y == 100 || y == 101) ? hollow : solid; }); - final Location safe = LocationUtil.getSafeDestination(essentials, new Location(world, 0, 64, 0)); - - assertEquals(1, safe.getBlockX()); - assertEquals(100, safe.getBlockY()); - assertFalse(LocationUtil.isBlockUnsafe(essentials, world, safe.getBlockX(), safe.getBlockY(), safe.getBlockZ())); + return LocationUtil.getSafeDestination(essentials, new Location(world, 0, 64, 0)); } @Test