This document outlines the security measures implemented in IonAPI to prevent common exploits and vulnerabilities.
IonAPI's GUI system includes multiple layers of protection against item duping exploits:
privatebooleanallowTake = false;
privatebooleanallowPlace = false;
privatebooleanallowDrag = false;By default, all item manipulation is disabled. This prevents:
- Taking items from GUI slots
- Placing items into GUI slots
- Dragging items between slots
// Clicking in player's own inventory (bottom inventory)if (slot < 0 || slot >= inventory.getSize()) {
// Always cancel to prevent shift-click duping exploitsevent.setCancelled(true);
return;
}Prevents:
- Shift-clicking items from player inventory into GUI
- Moving items between player inventory and GUI
- Hotbar key exploits
booleanisShiftClick = event.isShiftClick();
booleanisNumberKey = event.getHotbarButton() >= 0;
// Always cancel shift-click and number key presses to prevent dupingif (isShiftClick || isNumberKey) {
event.setCancelled(true);
}Prevents:
- Shift-click duping (most common exploit)
- Number key (1-9) item swapping
- Quick-move exploits
if (!allowDrag) {
event.setCancelled(true);
return;
}Prevents:
- Drag-and-drop duping
- Multi-slot item distribution exploits
IonGuigui = IonGui.builder()
.title("Shop")
.item(10, diamondItem, click -> {
// Handle purchaseclick.close();
})
.build();
// No duping possible - all interactions cancelledIonGuigui = IonGui.builder()
.title("Rewards")
.allowTake(true) // ⚠️ Only use when giving items to players
.item(10, rewardItem)
.build();
// Shift-click and number keys still blockedIonGuigui = IonGui.builder()
.title("Trading")
.onClick(click -> {
// Validate the actionif (!isValidTrade(click.getPlayer())) {
click.setCancelled(true);
return;
}
// Process trade...
})
.build();// ✅ SAFE - Uses prepared statementsdb.execute("UPDATE players SET coins = ? WHERE uuid = ?", newBalance, playerUuid);
// ❌ UNSAFE - Never do thisdb.execute("UPDATE players SET coins = " + newBalance + " WHERE uuid = '" + playerUuid + "'");// QueryBuilderImpl sanitizes all identifiersprivatestaticfinalPatternVALID_IDENTIFIER = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
privatevoidvalidateIdentifier(Stringidentifier) {
if (!VALID_IDENTIFIER.matcher(identifier).matches()) {
thrownewDatabaseException("Invalid identifier: " + identifier);
}
}Prevents:
- SQL injection through column names
- SQL injection through table names
- SQL injection through operators
privatestaticfinalSet<String> VALID_OPERATORS = Set.of(
"=", "!=", "<>", ">", "<", ">=", "<=", "LIKE", "NOT LIKE", "IN", "NOT IN", "IS", "IS NOT"
);Prevents:
- Malicious operators
- SQL command injection
- Subquery injection
try {
transaction.accept(this);
conn.commit();
} catch (Exceptione) {
conn.rollback(); // ✅ Automatic rollback on errorthrownewDatabaseException("Transaction failed", e);
}try (Connectionconn = getConnection()) {
// Operations...
} catch (SQLExceptione) {
// Connection automatically closed
}@Column(name = "balance", columnDefinition = "DECIMAL(19,4)")
privateBigDecimalbalance;Prevents:
- Floating-point rounding errors
- Balance manipulation through precision exploits
publicCompletableFuture<TransactionResult> withdraw(UUIDplayerId, BigDecimalamount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
returnCompletableFuture.completedFuture(
newTransactionResultImpl(false, "Amount must be positive", BigDecimal.ZERO)
);
}
// Validate balance before withdrawal...
}// Database transactions ensure atomicitydb.transaction(tx -> {
EconomyAccountaccount = tx.find(EconomyAccount.class, playerId);
if (account.getBalance().compareTo(amount) < 0) {
thrownewInsufficientFundsException();
}
account.setBalance(account.getBalance().subtract(amount));
tx.save(account);
});IonRedisredis = IonRedis.builder()
.host("localhost")
.port(6379)
.password("your-secure-password") // Optional but recommended
.ssl(true) // Enable SSL/TLS
.build();// HikariCP-style connection management// Prevents connection exhaustion attacksAlways validate user input before processing:
if (input == null || input.isEmpty()) {
thrownewIllegalArgumentException("Input cannot be empty");
}Always check permissions before sensitive operations:
if (!player.hasPermission("myplugin.admin")) {
player.sendMessage("No permission!");
return;
}Use the built-in RateLimiter to prevent spam:
RateLimiterlimiter = RateLimiter.create("command", 5, 10, TimeUnit.SECONDS);
if (!limiter.tryAcquire(player.getUniqueId())) {
player.sendMessage("Slow down!");
return;
}Use async operations for expensive tasks:
db.findAsync(PlayerData.class, uuid).thenAccept(data -> {
// Process on async thread
});If you discover a security vulnerability in IonAPI:
- DO NOT open a public GitHub issue
- DO email: mattdabigsigma@gmail.com
- DO provide:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We take security seriously and will respond within 48 hours.
When using IonAPI in your plugin:
- Never use string concatenation for SQL queries
- Always validate user input
- Use parameterized queries for database operations
- Check permissions before sensitive operations
- Use rate limiting for commands and actions
- Enable SSL/TLS for Redis connections
- Use BigDecimal for currency calculations
- Test GUI interactions for duping exploits
- Implement proper error handling
- Log security-relevant events
- ✅ Enhanced GUI duping prevention
- ✅ Added shift-click protection
- ✅ Added number key protection
- ✅ Improved bottom inventory handling
- ✅ Fixed SQL injection in QueryBuilder
- ✅ Fixed resource leak in Transaction
- ✅ Added input validation
- ✅ Initial security implementation
- ✅ Parameterized queries
- ✅ GUI event cancellation
IonAPI is provided "as is" without warranty. See LICENSE file for details.