Improve metadata sanitization

This commit is contained in:
Intelli committed 2026-04-22 20:04:01 -06:00
1 parent 42a2965cd1
commit 99130fdcdb
1 file changed
+206 -50
@@ -7,10 +7,14 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@@ -36,6 +40,31 @@ public class ItemUtils {
private static final Object UNSERIALIZABLE_VALUE = new Object(); private static final Object UNSERIALIZABLE_VALUE = new Object();
private static final Logger LOGGER = Logger.getLogger("CoreProtect"); private static final Logger LOGGER = Logger.getLogger("CoreProtect");
private static final int MAX_SANITIZE_ATTEMPTS = 2;
private static final Pattern TYPE_TOKEN_PATTERN = Pattern.compile("([A-Za-z_$][A-Za-z0-9_$\\.]*)\\{");
private static final Pattern REGISTRY_LOCATION_PATTERN = Pattern.compile("location=([^,\\]\\s]+)");
private static final class FailureContext {
private final Set<String> typeTokens;
private final String registryLocation;
private FailureContext(Set<String> typeTokens, String registryLocation) {
this.typeTokens = typeTokens;
this.registryLocation = registryLocation;
}
}
private static final class SanitizationResult {
private final Object value;
private final boolean modified;
private SanitizationResult(Object value, boolean modified) {
this.value = value;
this.modified = modified;
}
}
private ItemUtils() { private ItemUtils() {
throw new IllegalStateException("Utility class"); throw new IllegalStateException("Utility class");
@@ -350,25 +379,48 @@ public class ItemUtils {
return serializeByteData(data); return serializeByteData(data);
} }
catch (Exception initialException) { catch (Exception initialException) {
Object sanitizedData = sanitizeByteData(data); Object workingData = data;
if (sanitizedData != UNSERIALIZABLE_VALUE) { Exception failure = initialException;
Set<String> problematicTypes = new LinkedHashSet<>();
String registryLocation = null;
boolean sanitized = false;
for (int attempt = 0; attempt < MAX_SANITIZE_ATTEMPTS; attempt++) {
FailureContext failureContext = parseSerializationFailure(failure);
problematicTypes.addAll(failureContext.typeTokens);
if (registryLocation == null && failureContext.registryLocation != null) {
registryLocation = failureContext.registryLocation;
}
if (problematicTypes.isEmpty()) {
break;
}
SanitizationResult sanitizationResult = sanitizeByteData(workingData, problematicTypes);
if (!sanitizationResult.modified || sanitizationResult.value == UNSERIALIZABLE_VALUE) {
break;
}
sanitized = true;
workingData = sanitizationResult.value;
try { try {
byte[] result = serializeByteData(sanitizedData); byte[] result = serializeByteData(workingData);
if (ConfigHandler.EDITION_BRANCH.contains("-dev")) { if (ConfigHandler.EDITION_BRANCH.contains("-dev")) {
LOGGER.warning("CoreProtect sanitized unsupported metadata during byte serialization; some nested values were omitted."); logSanitizedMetadata(problematicTypes, registryLocation);
} }
return result; return result;
} }
catch (Exception sanitizedException) { catch (Exception retryException) {
if (ConfigHandler.EDITION_BRANCH.contains("-dev")) { failure = retryException;
sanitizedException.printStackTrace();
}
return null;
} }
} }
if (ConfigHandler.EDITION_BRANCH.contains("-dev")) { if (ConfigHandler.EDITION_BRANCH.contains("-dev")) {
initialException.printStackTrace(); if (sanitized) {
LOGGER.warning("CoreProtect failed to serialize metadata after targeted type sanitization.");
}
failure.printStackTrace();
} }
} }
@@ -383,85 +435,189 @@ public class ItemUtils {
} }
} }
private static boolean canSerializeByteData(Object data) { private static FailureContext parseSerializationFailure(Throwable throwable) {
try { Set<String> typeTokens = new LinkedHashSet<>();
serializeByteData(data); String registryLocation = null;
return true; Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null) {
Matcher typeMatcher = TYPE_TOKEN_PATTERN.matcher(message);
while (typeMatcher.find()) {
addTypeToken(typeTokens, typeMatcher.group(1));
}
if (registryLocation == null) {
Matcher locationMatcher = REGISTRY_LOCATION_PATTERN.matcher(message);
if (locationMatcher.find()) {
registryLocation = locationMatcher.group(1);
}
}
}
for (StackTraceElement frame : current.getStackTrace()) {
if ("serialize".equals(frame.getMethodName())) {
addTypeToken(typeTokens, frame.getClassName());
}
}
current = current.getCause();
} }
catch (Exception e) {
return false; return new FailureContext(typeTokens, registryLocation);
}
private static void addTypeToken(Set<String> typeTokens, String token) {
String normalizedToken = normalizeTypeToken(token);
if (normalizedToken != null) {
typeTokens.add(normalizedToken);
} }
} }
private static Object sanitizeByteData(Object data) { private static String normalizeTypeToken(String token) {
if (data == null || canSerializeByteData(data)) { if (token == null) {
return data; return null;
}
String normalized = token.trim();
if (normalized.isEmpty()) {
return null;
}
int dotIndex = normalized.lastIndexOf('.');
if (dotIndex >= 0 && dotIndex + 1 < normalized.length()) {
normalized = normalized.substring(dotIndex + 1);
}
int dollarIndex = normalized.lastIndexOf('$');
if (dollarIndex >= 0 && dollarIndex + 1 < normalized.length()) {
normalized = normalized.substring(dollarIndex + 1);
}
StringBuilder safeToken = new StringBuilder(normalized.length());
for (int i = 0; i < normalized.length(); i++) {
char currentChar = normalized.charAt(i);
if ((currentChar >= 'A' && currentChar <= 'Z') || (currentChar >= 'a' && currentChar <= 'z') || (currentChar >= '0' && currentChar <= '9') || currentChar == '_') {
safeToken.append(currentChar);
}
}
if (safeToken.length() == 0) {
return null;
}
return safeToken.toString().toLowerCase(Locale.ROOT);
}
private static boolean isProblematicType(Class<?> valueType, Set<String> problematicTypes) {
String typeToken = normalizeTypeToken(valueType.getSimpleName());
if (typeToken == null) {
return false;
}
for (String problematicType : problematicTypes) {
if (typeToken.equals(problematicType) || typeToken.startsWith(problematicType) || problematicType.startsWith(typeToken)) {
return true;
}
}
return false;
}
private static SanitizationResult sanitizeByteData(Object data, Set<String> problematicTypes) {
if (data == null) {
return new SanitizationResult(null, false);
}
if (isProblematicType(data.getClass(), problematicTypes)) {
return new SanitizationResult(UNSERIALIZABLE_VALUE, true);
} }
if (data instanceof Map<?, ?>) { if (data instanceof Map<?, ?>) {
Map<?, ?> sourceMap = (Map<?, ?>) data; Map<?, ?> sourceMap = (Map<?, ?>) data;
Map<Object, Object> sanitizedMap = new LinkedHashMap<>(); Map<Object, Object> sanitizedMap = new LinkedHashMap<>();
boolean modified = false;
for (Map.Entry<?, ?> entry : sourceMap.entrySet()) { for (Map.Entry<?, ?> entry : sourceMap.entrySet()) {
Object sanitizedKey = sanitizeByteData(entry.getKey()); SanitizationResult sanitizedKey = sanitizeByteData(entry.getKey(), problematicTypes);
Object sanitizedValue = sanitizeByteData(entry.getValue()); SanitizationResult sanitizedValue = sanitizeByteData(entry.getValue(), problematicTypes);
if (sanitizedKey == UNSERIALIZABLE_VALUE || sanitizedValue == UNSERIALIZABLE_VALUE) { if (sanitizedKey.value == UNSERIALIZABLE_VALUE || sanitizedValue.value == UNSERIALIZABLE_VALUE) {
modified = true;
continue; continue;
} }
sanitizedMap.put(sanitizedKey, sanitizedValue); modified |= sanitizedKey.modified || sanitizedValue.modified;
sanitizedMap.put(sanitizedKey.value, sanitizedValue.value);
} }
if (sanitizedMap.isEmpty() && !sourceMap.isEmpty()) { if (!modified) {
return UNSERIALIZABLE_VALUE; return new SanitizationResult(data, false);
} }
return canSerializeByteData(sanitizedMap) ? sanitizedMap : UNSERIALIZABLE_VALUE; return new SanitizationResult(sanitizedMap, true);
} }
if (data instanceof List<?>) { if (data instanceof List<?>) {
List<?> sourceList = (List<?>) data; List<?> sourceList = (List<?>) data;
List<Object> sanitizedList = new ArrayList<>(); List<Object> sanitizedList = new ArrayList<>(sourceList.size());
boolean modified = false;
for (Object value : sourceList) { for (Object value : sourceList) {
Object sanitizedValue = sanitizeByteData(value); SanitizationResult sanitizedValue = sanitizeByteData(value, problematicTypes);
if (sanitizedValue == UNSERIALIZABLE_VALUE) { if (sanitizedValue.value == UNSERIALIZABLE_VALUE) {
continue; sanitizedList.add(null);
modified = true;
}
else {
sanitizedList.add(sanitizedValue.value);
modified |= sanitizedValue.modified;
} }
sanitizedList.add(sanitizedValue);
} }
if (sanitizedList.isEmpty() && !sourceList.isEmpty()) { if (!modified) {
return UNSERIALIZABLE_VALUE; return new SanitizationResult(data, false);
} }
return canSerializeByteData(sanitizedList) ? sanitizedList : UNSERIALIZABLE_VALUE; return new SanitizationResult(sanitizedList, true);
} }
if (data.getClass().isArray()) { if (data.getClass().isArray()) {
Class<?> componentType = data.getClass().getComponentType();
if (componentType.isPrimitive()) {
return new SanitizationResult(data, false);
}
int length = Array.getLength(data); int length = Array.getLength(data);
List<Object> sanitizedValues = new ArrayList<>(); Object sanitizedArray = Array.newInstance(componentType, length);
boolean modified = false;
for (int index = 0; index < length; index++) { for (int index = 0; index < length; index++) {
Object sanitizedValue = sanitizeByteData(Array.get(data, index)); SanitizationResult sanitizedValue = sanitizeByteData(Array.get(data, index), problematicTypes);
if (sanitizedValue == UNSERIALIZABLE_VALUE) { if (sanitizedValue.value == UNSERIALIZABLE_VALUE) {
continue; Array.set(sanitizedArray, index, null);
modified = true;
}
else {
Array.set(sanitizedArray, index, sanitizedValue.value);
modified |= sanitizedValue.modified;
} }
sanitizedValues.add(sanitizedValue);
} }
if (sanitizedValues.isEmpty() && length > 0) { if (!modified) {
return UNSERIALIZABLE_VALUE; return new SanitizationResult(data, false);
} }
Object sanitizedArray = Array.newInstance(data.getClass().getComponentType(), sanitizedValues.size()); return new SanitizationResult(sanitizedArray, true);
for (int index = 0; index < sanitizedValues.size(); index++) {
Array.set(sanitizedArray, index, sanitizedValues.get(index));
}
return canSerializeByteData(sanitizedArray) ? sanitizedArray : UNSERIALIZABLE_VALUE;
} }
return UNSERIALIZABLE_VALUE; return new SanitizationResult(data, false);
}
private static void logSanitizedMetadata(Set<String> problematicTypes, String registryLocation) {
StringBuilder message = new StringBuilder("CoreProtect sanitized unsupported metadata during byte serialization (types: ");
message.append(String.join(", ", problematicTypes));
if (registryLocation != null) {
message.append(", missing key: ").append(registryLocation);
}
message.append(").");
LOGGER.warning(message.toString());
} }
public static ItemMeta deserializeItemMeta(Class<? extends ItemMeta> itemMetaClass, Map<String, Object> args) { public static ItemMeta deserializeItemMeta(Class<? extends ItemMeta> itemMetaClass, Map<String, Object> args) {