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:
private boolean allowTake = false;
private boolean allowPlace = false;
private boolean allowDrag = 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 exploits
event.setCancelled(true);
return;
}Prevents:
- Shift-clicking items from player inventory into GUI
- Moving items between player inventory and GUI
- Hotbar key exploits
boolean isShiftClick = event.isShiftClick();
boolean isNumberKey = event.getHotbarButton() >= 0;
// Always cancel shift-click and number key presses to prevent duping
if (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
IonGui gui = IonGui.builder()
.title("Shop")
.item(10, diamondItem, click -> {
// Handle purchase
click.close();
})
.build();
// No duping possible - all interactions cancelledIonGui gui = IonGui.builder()
.title("Rewards")
.allowTake(true) // ⚠️ Only use when giving items to players
.item(10, rewardItem)
.build();
// Shift-click and number keys still blockedIonGui gui = IonGui.builder()
.title("Trading")
.onClick(click -> {
// Validate the action
if (!isValidTrade(click.getPlayer())) {
click.setCancelled(true);
return;
}
// Process trade...
})
.build();// ✅ SAFE - Uses prepared statements
db.execute("UPDATE players SET coins = ? WHERE uuid = ?", newBalance, playerUuid);
// ❌ UNSAFE - Never do this
db.execute("UPDATE players SET coins = " + newBalance + " WHERE uuid = '" + playerUuid + "'");// QueryBuilderImpl sanitizes all identifiers
private static final Pattern VALID_IDENTIFIER = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
private void validateIdentifier(String identifier) {
if (!VALID_IDENTIFIER.matcher(identifier).matches()) {
throw new DatabaseException("Invalid identifier: " + identifier);
}
}Prevents:
- SQL injection through column names
- SQL injection through table names
- SQL injection through operators
private static final Set<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 (Exception e) {
conn.rollback(); // ✅ Automatic rollback on error
throw new DatabaseException("Transaction failed", e);
}try (Connection conn = getConnection()) {
// Operations...
} catch (SQLException e) {
// Connection automatically closed
}@Column(name = "balance", columnDefinition = "DECIMAL(19,4)")
private BigDecimal balance;Prevents:
- Floating-point rounding errors
- Balance manipulation through precision exploits
public CompletableFuture<TransactionResult> withdraw(UUID playerId, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return CompletableFuture.completedFuture(
new TransactionResultImpl(false, "Amount must be positive", BigDecimal.ZERO)
);
}
// Validate balance before withdrawal...
}// Database transactions ensure atomicity
db.transaction(tx -> {
EconomyAccount account = tx.find(EconomyAccount.class, playerId);
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
account.setBalance(account.getBalance().subtract(amount));
tx.save(account);
});IonRedis redis = 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()) {
throw new IllegalArgumentException("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:
RateLimiter limiter = 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.