From 4c012e983a8f2cb429b750cc61d36db8697473d3 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 18:58:22 -0600 Subject: [PATCH 01/63] Bind JavaScript placeholders automatically as data --- AdvancedCore/pom.xml | 6 + .../api/javascript/JavascriptEngine.java | 24 +- .../JavascriptPlaceholderBinder.java | 469 ++++++++++++++++++ .../JavascriptPlaceholderValue.java | 34 ++ .../api/messages/PlaceholderUtils.java | 127 +++-- .../api/rewards/builtin/RewardJavascript.java | 13 +- .../requirements/RequirementJavascript.java | 9 +- .../JavascriptPlaceholderBinderTest.java | 142 ++++++ ...laceholderUtilsJavascriptBoundaryTest.java | 47 ++ 9 files changed, 827 insertions(+), 44 deletions(-) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index a3f17cc16..ae2ed4bc6 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -231,6 +231,12 @@ 2.12.2 provided + + org.openjdk.nashorn + nashorn-core + 15.7 + provided + org.slf4j slf4j-simple diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java index 2a784bdad..4f0a59352 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.api.javascript; import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; import javax.script.ScriptEngine; @@ -16,13 +17,17 @@ import com.bencodez.simpleapi.messages.MessageAPI; public class JavascriptEngine { - private HashMap engineAPI; + private final HashMap engineAPI; + private final HashMap placeholders; + private OfflinePlayer placeholderPlayer; public JavascriptEngine() { engineAPI = new HashMap<>(); + placeholders = new HashMap<>(); } public JavascriptEngine addPlayer(AdvancedCoreUser user) { + placeholderPlayer = user.getOfflinePlayer(); addToEngine("PlayerName", user.getPlayerName()); addToEngine("PlayerUUID", user.getUUID()); addToEngine("AdvancedCoreUser", user); @@ -42,6 +47,7 @@ public JavascriptEngine addPlayer(CommandSender player) { addToEngine("CommandSender", player); if (player instanceof Player) { Player p = (Player) player; + placeholderPlayer = p; addToEngine("Player", p); addToEngine("PlayerName", p.getName()); addToEngine("PlayerUUID", p.getUniqueId().toString()); @@ -58,6 +64,7 @@ public JavascriptEngine addPlayer(CommandSender player) { } public JavascriptEngine addPlayer(OfflinePlayer player) { + placeholderPlayer = player; addToEngine("Player", player); addToEngine("PlayerName", player.getName()); addToEngine("PlayerUUID", player.getUniqueId().toString()); @@ -76,6 +83,7 @@ public JavascriptEngine addPlayer(OfflinePlayer player) { public JavascriptEngine addPlayer(Player player) { if (player != null) { + placeholderPlayer = player; addToEngine("Player", player); addToEngine("PlayerName", player.getName()); addToEngine("PlayerUUID", player.getUniqueId().toString()); @@ -90,6 +98,13 @@ public JavascriptEngine addPlayer(Player player) { return this; } + public JavascriptEngine addPlaceholders(Map placeholders) { + if (placeholders != null && !placeholders.isEmpty()) { + this.placeholders.putAll(placeholders); + } + return this; + } + public JavascriptEngine addToEngine(HashMap engineAPI) { if (engineAPI != null && !engineAPI.isEmpty()) { this.engineAPI.putAll(engineAPI); @@ -119,7 +134,7 @@ public boolean getBooleanValue(String expression) { } public Object getResult(String expression) { - if (!expression.equals("")) { + if (expression != null && !expression.equals("")) { if (!AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { return null; } @@ -128,6 +143,9 @@ public Object getResult(String expression) { AdvancedCorePlugin.getInstance().debug("Failed to process javascript, engine == null"); return null; } + + String preparedExpression = JavascriptPlaceholderBinder.bind(expression, placeholderPlayer, placeholders, this); + engine.put("Bukkit", Bukkit.getServer()); engine.put("AdvancedCore", AdvancedCorePlugin.getInstance()); engine.put("Console", Bukkit.getConsoleSender()); @@ -142,7 +160,7 @@ public Object getResult(String expression) { } try { - return engine.eval(expression); + return engine.eval(preparedExpression); } catch (ScriptException e) { AdvancedCorePlugin.getInstance().getLogger().warning( "Error occoured while evaluating javascript, turn debug on to see stacktrace: " + e.toString()); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java new file mode 100644 index 000000000..ef58cbc34 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -0,0 +1,469 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.script.ScriptEngine; + +import org.bukkit.OfflinePlayer; + +import com.bencodez.advancedcore.AdvancedCorePlugin; + +import me.clip.placeholderapi.PlaceholderAPI; + +/** + * Resolves JavaScript placeholders without allowing placeholder output to become + * executable source. + *

+ * AdvancedCore asks the already-loaded Nashorn parser to identify whether each + * placeholder is in executable code, a string, template text, or a regular + * expression. This preserves existing JavaScript syntax without maintaining a + * second JavaScript lexer inside AdvancedCore. + */ +public final class JavascriptPlaceholderBinder { + private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\s]+)%"); + private static final Pattern INTEGER = Pattern.compile("[-+]?\\d+"); + private static final Pattern DECIMAL = Pattern + .compile("[-+]?(?:\\d+\\.\\d*|\\d*\\.\\d+|\\d+)(?:[eE][-+]?\\d+)?"); + private static final String VARIABLE_PREFIX = "__advancedCorePlaceholder"; + private static final String PARSER_CLASS = "org.openjdk.nashorn.api.tree.Parser"; + private static final String DIAGNOSTIC_LISTENER_CLASS = "org.openjdk.nashorn.api.tree.DiagnosticListener"; + private static final String TREE_CLASS = "org.openjdk.nashorn.api.tree.Tree"; + private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; + + private JavascriptPlaceholderBinder() { + } + + public static String bind(String expression, OfflinePlayer player, Map placeholders, + JavascriptEngine engine) { + return bind(expression, token -> resolve(token, player, placeholders), engine::addToEngine); + } + + static String bind(String expression, Function resolver, BiConsumer bindings) { + if (expression == null || expression.isEmpty()) { + return expression; + } + + Matcher matcher = PLACEHOLDER.matcher(expression); + List matches = new ArrayList<>(); + StringBuilder sanitized = new StringBuilder(expression); + while (matcher.find()) { + String token = matcher.group(); + String value = JavascriptPlaceholderValue.decode(token); + if (value == null) { + value = resolver.apply(token); + } + matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); + // Keep all source offsets unchanged while making a bare %placeholder% + // parse as an ordinary identifier. + for (int i = matcher.start(); i < matcher.end(); i++) { + sanitized.setCharAt(i, 'p'); + } + } + if (matches.isEmpty()) { + return expression; + } + + JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); + String[] replacements = new String[matches.size()]; + int bindingIndex = 0; + for (int i = 0; i < matches.size(); i++) { + PlaceholderMatch match = matches.get(i); + if (match.value == null || match.value.equals(match.token)) { + replacements[i] = match.token; + continue; + } + + Range regex = contexts.containing(contexts.regexes, match.start); + Range string = contexts.containing(contexts.strings, match.start); + Range template = contexts.containing(contexts.templates, match.start); + if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (string != null) { + replacements[i] = escapeString(match.value, expression.charAt(string.start)); + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + String variable = VARIABLE_PREFIX + bindingIndex++; + bindings.accept(variable, coerce(match.value)); + replacements[i] = variable; + } + } + + // Apply from right to left so source positions from the parser remain valid. + StringBuilder result = new StringBuilder(expression); + for (int i = matches.size() - 1; i >= 0; i--) { + PlaceholderMatch match = matches.get(i); + result.replace(match.start, match.end, replacements[i]); + } + return result.toString(); + } + + private static String resolve(String token, OfflinePlayer player, Map placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + } + return token; + } + + private static Object coerce(String value) { + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return Boolean.valueOf(value); + } + if (INTEGER.matcher(value).matches()) { + try { + return Long.valueOf(value); + } catch (NumberFormatException ignored) { + } + } + if (DECIMAL.matcher(value).matches()) { + try { + return Double.valueOf(value); + } catch (NumberFormatException ignored) { + } + } + return value; + } + + private static String escapeString(String value, char quote) { + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + switch (current) { + case '\\': + result.append("\\\\"); + break; + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + case '\u2028': + result.append("\\u2028"); + break; + case '\u2029': + result.append("\\u2029"); + break; + default: + if (current == quote) { + result.append('\\'); + } + result.append(current); + break; + } + } + return result.toString(); + } + + private static String escapeTemplate(String value) { + return value.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${") + .replace("\r", "\\r").replace("\n", "\\n").replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029"); + } + + private static String escapeRegex(String value, String expression, Range regex, int placeholderStart) { + boolean characterClass = false; + boolean escaped = false; + for (int i = regex.start + 1; i < placeholderStart; i++) { + char current = expression.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + } else if (current == '[') { + characterClass = true; + } else if (current == ']') { + characterClass = false; + } + } + + String special = characterClass ? "\\/]^-" : "\\/.*+?^${}()|[]"; + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + if (current == '\n') { + result.append("\\n"); + } else if (current == '\r') { + result.append("\\r"); + } else if (current == '\u2028') { + result.append("\\u2028"); + } else if (current == '\u2029') { + result.append("\\u2029"); + } else { + if (special.indexOf(current) >= 0) { + result.append('\\'); + } + result.append(current); + } + } + return result.toString(); + } + + private static final class PlaceholderMatch { + private final int start; + private final int end; + private final String token; + private final String value; + + private PlaceholderMatch(int start, int end, String token, String value) { + this.start = start; + this.end = end; + this.token = token; + this.value = value; + } + } + + private static final class Range { + private final int start; + private final int end; + + private Range(long start, long end) { + this.start = (int) start; + this.end = (int) end; + } + + private boolean contains(int position) { + return position >= start && position < end; + } + } + + /** + * Context ranges obtained from Nashorn's parser API. Parser classes are loaded + * reflectively because AdvancedCore can download Nashorn into its own + * URLClassLoader at runtime rather than putting it on the plugin classpath. + */ + private static final class JavascriptContexts { + private final List strings = new ArrayList<>(); + private final List regexes = new ArrayList<>(); + private final List templates = new ArrayList<>(); + private final List templateExpressions = new ArrayList<>(); + + private static JavascriptContexts parse(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + try { + ClassLoader loader = parserClassLoader(); + if (loader == null) { + return contexts; + } + + Class parserClass = Class.forName(PARSER_CLASS, true, loader); + Class diagnosticClass = Class.forName(DIAGNOSTIC_LISTENER_CLASS, true, loader); + Class treeClass = Class.forName(TREE_CLASS, true, loader); + Object parser = createParser(parserClass); + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> null); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null) { + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + // If a script cannot be parsed, expression placeholders still fall back to + // engine bindings below. Placeholder output is never copied into source code. + } + contexts.sort(); + return contexts; + } + + private static Object createParser(Class parserClass) throws ReflectiveOperationException { + for (Method method : parserClass.getMethods()) { + if (!method.getName().equals("create") || !Modifier.isStatic(method.getModifiers())) { + continue; + } + if (method.getParameterCount() == 0) { + return method.invoke(null); + } + if (method.getParameterCount() == 1 && method.getParameterTypes()[0].isArray() + && method.getParameterTypes()[0].getComponentType() == String.class) { + return method.invoke(null, (Object) new String[] { "--language=es6" }); + } + } + throw new NoSuchMethodException("Nashorn Parser.create"); + } + + private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + if (handler.getNashornClassLoader() != null) { + return handler.getNashornClassLoader(); + } + ScriptEngine cached = handler.getCachedEngine(); + if (cached != null && cached.getClass().getClassLoader() != null) { + return cached.getClass().getClassLoader(); + } + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + try { + Class.forName(PARSER_CLASS, false, own); + return own; + } catch (ClassNotFoundException ignored) { + return null; + } + } + + private static void walk(Object node, Class treeClass, JavascriptContexts contexts, + IdentityHashMap visited) { + if (node == null || !treeClass.isInstance(node) || visited.put(node, Boolean.TRUE) != null) { + return; + } + + String kind = stringValue(invokeTreeMethod(node, "getKind")); + long start = longValue(invokeTreeMethod(node, "getStartPosition")); + long end = longValue(invokeTreeMethod(node, "getEndPosition")); + if (start >= 0 && end >= start) { + if ("STRING_LITERAL".equals(kind)) { + contexts.strings.add(new Range(start, end)); + } else if (kind != null && kind.contains("REGEXP")) { + contexts.regexes.add(new Range(start, end)); + } else if ("TEMPLATE_LITERAL".equals(kind)) { + contexts.templates.add(new Range(start, end)); + Object expressions = invokeTreeMethod(node, "getExpressions"); + if (expressions instanceof Iterable) { + for (Object expression : (Iterable) expressions) { + long expressionStart = longValue(invokeTreeMethod(expression, "getStartPosition")); + long expressionEnd = longValue(invokeTreeMethod(expression, "getEndPosition")); + if (expressionStart >= 0 && expressionEnd >= expressionStart) { + contexts.templateExpressions.add(new Range(expressionStart, expressionEnd)); + } + } + } + } + } + + for (Method method : treeApiMethods(node.getClass())) { + if (method.getParameterCount() != 0 || Modifier.isStatic(method.getModifiers())) { + continue; + } + String name = method.getName(); + if (name.equals("getKind") || name.equals("getStartPosition") || name.equals("getEndPosition") + || name.equals("getSourceName") || name.equals("getClass")) { + continue; + } + try { + Object value = method.invoke(node); + walkValue(value, treeClass, contexts, visited); + } catch (ReflectiveOperationException | RuntimeException ignored) { + } + } + } + + private static void walkValue(Object value, Class treeClass, JavascriptContexts contexts, + IdentityHashMap visited) { + if (value == null) { + return; + } + if (treeClass.isInstance(value)) { + walk(value, treeClass, contexts, visited); + } else if (value instanceof Iterable) { + for (Object element : (Iterable) value) { + if (treeClass.isInstance(element)) { + walk(element, treeClass, contexts, visited); + } + } + } else if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object element = Array.get(value, i); + if (treeClass.isInstance(element)) { + walk(element, treeClass, contexts, visited); + } + } + } + } + + private static Set treeApiMethods(Class type) { + LinkedHashSet methods = new LinkedHashSet<>(); + collectTreeApiMethods(type, methods, new LinkedHashSet<>()); + return methods; + } + + private static void collectTreeApiMethods(Class type, Set methods, Set> visited) { + if (type == null || !visited.add(type)) { + return; + } + for (Class iface : type.getInterfaces()) { + Package pkg = iface.getPackage(); + if (pkg != null && TREE_PACKAGE.equals(pkg.getName())) { + for (Method method : iface.getMethods()) { + methods.add(method); + } + } + collectTreeApiMethods(iface, methods, visited); + } + collectTreeApiMethods(type.getSuperclass(), methods, visited); + } + + private static Object invokeTreeMethod(Object node, String methodName) { + if (node == null) { + return null; + } + for (Method method : treeApiMethods(node.getClass())) { + if (method.getName().equals(methodName) && method.getParameterCount() == 0) { + try { + return method.invoke(node); + } catch (ReflectiveOperationException | RuntimeException ignored) { + return null; + } + } + } + return null; + } + + private static String stringValue(Object value) { + return value == null ? null : value.toString(); + } + + private static long longValue(Object value) { + return value instanceof Number ? ((Number) value).longValue() : -1; + } + + private Range containing(List ranges, int position) { + for (Range range : ranges) { + if (range.contains(position)) { + return range; + } + } + return null; + } + + private boolean insideTemplateExpression(int position) { + return containing(templateExpressions, position) != null; + } + + private void sort() { + Comparator comparator = Comparator.comparingInt(range -> range.start); + strings.sort(comparator); + regexes.sort(comparator); + templates.sort(comparator); + templateExpressions.sort(comparator); + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java new file mode 100644 index 000000000..d9318d130 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java @@ -0,0 +1,34 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Encodes placeholder output so it can pass through normal text replacement + * without ever becoming JavaScript source. + */ +public final class JavascriptPlaceholderValue { + private static final String PREFIX = "%__advancedcore_bound_"; + private static final String SUFFIX = "%"; + + private JavascriptPlaceholderValue() { + } + + public static String encode(String value) { + String encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(value.getBytes(StandardCharsets.UTF_8)); + return PREFIX + encoded + SUFFIX; + } + + static String decode(String token) { + if (token == null || !token.startsWith(PREFIX) || !token.endsWith(SUFFIX)) { + return null; + } + String encoded = token.substring(PREFIX.length(), token.length() - SUFFIX.length()); + try { + return new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java index 8e74bb82d..eb6ad61d6 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; +import java.util.function.Function; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; @@ -10,6 +11,7 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; +import com.bencodez.advancedcore.api.javascript.JavascriptPlaceholderValue; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.simpleapi.messages.MessageAPI; @@ -39,7 +41,6 @@ public static TextComponent parseJson(String msg) { TextComponent t = new TextComponent(text); String typeMsg = msg; - // types boolean parsing = true; while (parsing) { int nextTypeIndex = typeMsg.indexOf("\","); @@ -69,11 +70,6 @@ public static TextComponent parseJson(String msg) { } } - /* - * int secondMiddle = msg.indexOf("=\"", middle); String type = - * msg.substring(middle + "\",".length(), secondMiddle); String typeData = - * msg.substring(secondMiddle + "=\"".length(), endIndex); - */ comp.addExtra(parseJson(preMessage)); @@ -126,7 +122,7 @@ public static ArrayList replaceJavascript(AdvancedCoreUser user, ArrayLi public static String replaceJavascript(AdvancedCoreUser user, String text) { if (user.getPlugin().getOptions().isJavascriptEngineEnabled()) { JavascriptEngine engine = new JavascriptEngine().addPlayer(user); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, user.getOfflinePlayer()); } return text; } @@ -157,7 +153,7 @@ public static String replaceJavascript(CommandSender player, String text) { return replaceJavascript((Player) player, text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, null); } return text; } @@ -176,7 +172,7 @@ public static String replaceJavascript(OfflinePlayer player, String text) { return replaceJavascript(player.getPlayer(), text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, player); } return text; } @@ -195,7 +191,7 @@ public static String replaceJavascriptOnly(OfflinePlayer player, String text) { return replaceJavascriptOnly(player.getPlayer(), text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, player); } return text; } @@ -212,7 +208,7 @@ public static String replaceJavascript(Player player, String text) { String msg = replacePlaceHolders(player, text); if (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(msg, engine); + return replaceJavascript(msg, engine, player); } return msg; } @@ -228,7 +224,7 @@ public static ArrayList replaceJavascriptOnly(Player player, ArrayList replacePlaceHolder(ArrayList list, HashM } public static String replacePlaceHolder(String str, HashMap placeholders) { - if (placeholders != null) { - for (Entry entry : placeholders.entrySet()) { - str = replacePlaceHolder(str, entry.getKey(), entry.getValue()); - } + if (placeholders == null) { + return str; } - return str; + return transformJavascriptMarkers(str, value -> replacePlaceHolderMapRaw(value, placeholders, true), + value -> replacePlaceHolderMapEncoded(value, placeholders, true)); } public static String replacePlaceHolder(String str, HashMap placeholders, boolean ignoreCase) { - if (placeholders != null) { - for (Entry entry : placeholders.entrySet()) { - str = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase); - } + if (placeholders == null) { + return str; } - return str; + return transformJavascriptMarkers(str, value -> replacePlaceHolderMapRaw(value, placeholders, ignoreCase), + value -> replacePlaceHolderMapEncoded(value, placeholders, ignoreCase)); } /** @@ -322,6 +317,30 @@ public static String replacePlaceHolder(String str, String toReplace, String rep } public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) { + return transformJavascriptMarkers(str, + value -> replacePlaceHolderRaw(value, toReplace, replaceWith, ignoreCase), + value -> replacePlaceHolderRaw(value, toReplace, JavascriptPlaceholderValue.encode(replaceWith), ignoreCase)); + } + + private static String replacePlaceHolderMapRaw(String str, HashMap placeholders, boolean ignoreCase) { + String result = str; + for (Entry entry : placeholders.entrySet()) { + result = replacePlaceHolderRaw(result, entry.getKey(), entry.getValue(), ignoreCase); + } + return result; + } + + private static String replacePlaceHolderMapEncoded(String str, HashMap placeholders, + boolean ignoreCase) { + String result = str; + for (Entry entry : placeholders.entrySet()) { + result = replacePlaceHolderRaw(result, entry.getKey(), JavascriptPlaceholderValue.encode(entry.getValue()), + ignoreCase); + } + return result; + } + + private static String replacePlaceHolderRaw(String str, String toReplace, String replaceWith, boolean ignoreCase) { if (ignoreCase) { return MessageAPI.replaceIgnoreCase(MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", replaceWith), "\\{" + toReplace + "\\}", replaceWith); @@ -353,7 +372,8 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { return text; } if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); + return transformJavascriptMarkers(text, value -> PlaceholderAPI.setPlaceholders(player, value), + Function.identity()); } return text; } @@ -366,13 +386,60 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { * @return the string */ public static String replacePlaceHolders(Player player, String text) { - if (player == null) { + return replacePlaceHolders((OfflinePlayer) player, text); + } + + private static String transformJavascriptMarkers(String text, Function outsideTransform, + Function insideTransform) { + if (text == null || text.isEmpty()) { return text; } - if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); + StringBuilder result = new StringBuilder(text.length()); + int cursor = 0; + while (cursor < text.length()) { + int start = indexOfIgnoreCase(text, "[Javascript=", cursor); + if (start < 0) { + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor)))); + break; + } + int end = text.indexOf(']', start); + if (end < 0) { + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor)))); + break; + } + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor, start)))); + int bodyStart = start + "[Javascript=".length(); + result.append(text, start, bodyStart); + result.append(insideTransform.apply(text.substring(bodyStart, end))); + result.append(']'); + cursor = end + 1; } - return text; + return result.toString(); + } + + private static String neutralizeJavascriptMarkers(String text) { + StringBuilder result = new StringBuilder(text.length()); + int cursor = 0; + while (cursor < text.length()) { + int start = indexOfIgnoreCase(text, "[Javascript=", cursor); + if (start < 0) { + result.append(text.substring(cursor)); + break; + } + result.append(text, cursor, start).append("[Javascript ="); + cursor = start + "[Javascript=".length(); + } + return result.toString(); + } + + private static int indexOfIgnoreCase(String text, String target, int fromIndex) { + int max = text.length() - target.length(); + for (int i = Math.max(0, fromIndex); i <= max; i++) { + if (text.regionMatches(true, i, target, 0, target.length())) { + return i; + } + } + return -1; } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java index 3af8e73a5..a58baa1a8 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java @@ -14,7 +14,6 @@ import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueList; import com.bencodez.advancedcore.api.item.ItemBuilder; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; import com.bencodez.advancedcore.api.rewards.DefinedReward; import com.bencodez.advancedcore.api.rewards.Reward; import com.bencodez.advancedcore.api.rewards.RewardBuilder; @@ -37,10 +36,10 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { public String onRewardRequest(Reward reward, AdvancedCoreUser user, ArrayList list, HashMap placeholders) { if (!list.isEmpty()) { - JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()); + JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .addPlaceholders(placeholders); for (String script : list) { - String expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), script); - engine.execute(PlaceholderUtils.replacePlaceHolder(expression, placeholders)); + engine.execute(script); } } return null; @@ -61,9 +60,9 @@ public String onRewardRequested(Reward reward, AdvancedCoreUser user, Configurat HashMap placeholders) { if (section.getBoolean("Enabled")) { String expression = section.getString("Expression"); - expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), expression); - if (new JavascriptEngine().addPlayer(user.getOfflinePlayer()) - .getBooleanValue(PlaceholderUtils.replacePlaceHolder(expression, placeholders))) { + JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .addPlaceholders(placeholders); + if (engine.getBooleanValue(expression)) { new RewardBuilder(section, "TrueRewards").withPrefix(reward.getName() + ".Javascript").send(user); } else { new RewardBuilder(section, "FalseRewards").withPrefix(reward.getName() + ".Javascript").send(user); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java index 22be9c421..113055e75 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java @@ -8,7 +8,6 @@ import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueString; import com.bencodez.advancedcore.api.item.ItemBuilder; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; import com.bencodez.advancedcore.api.rewards.Reward; import com.bencodez.advancedcore.api.rewards.RewardEditData; import com.bencodez.advancedcore.api.rewards.RewardHandler; @@ -28,9 +27,11 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { @Override public boolean onRequirementsRequest(Reward reward, AdvancedCoreUser user, String expression, RewardOptions rewardOptions) { - return expression.equals("") || new JavascriptEngine().addPlayer(user.getOfflinePlayer()) - .getBooleanValue(PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), - PlaceholderUtils.replacePlaceHolder(expression, rewardOptions.getPlaceholders()))); + if (expression.equals("")) { + return true; + } + return new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .addPlaceholders(rewardOptions.getPlaceholders()).getBooleanValue(expression); } }.priority(90).addEditButton(new EditGUIButton(new ItemBuilder("DETECTOR_RAIL"), new EditGUIValueString("JavascriptExpression", null) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java new file mode 100644 index 000000000..323ac0b98 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -0,0 +1,142 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderBinderTest { + + @Test + void ordinaryJavascriptWithoutPlaceholdersIsUntouched() { + HashMap bindings = new HashMap<>(); + String script = "Player.hasPermission(\"someper\") == true"; + + String prepared = JavascriptPlaceholderBinder.bind(script, token -> token, bindings::put); + + assertEquals(script, prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderExpressionIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%permission_result% == true", ignored -> "true", + bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true", prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void placeholderOutputIsBoundInsteadOfCopiedIntoSource() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("%name% == 'safe'", ignored -> injection, bindings::put); + + assertEquals("__advancedCorePlaceholder0 == 'safe'", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + + @Test + void existingQuotedPlaceholderSyntaxIsPreserved() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%name%' == 'Ben'", ignored -> "Ben", bindings::put); + + assertEquals("'Ben' == 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideExistingStringNeedsNoMigration() { + HashMap bindings = new HashMap<>(); + String value = "Ben's \\ server"; + + String prepared = JavascriptPlaceholderBinder.bind("'Hello %name%!'", ignored -> value, bindings::put); + + assertEquals("'Hello Ben\\'s \\\\ server!'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideTemplateTextIsEscapedAutomatically() { + HashMap bindings = new HashMap<>(); + String value = "${attack}`"; + + String prepared = JavascriptPlaceholderBinder.bind("`Hello %name%`", ignored -> value, bindings::put); + + assertEquals("`Hello \\${attack}\\``", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideTemplateExpressionIsBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("`${%count% + 1}`", ignored -> "5", bindings::put); + + assertEquals("`${__advancedCorePlaceholder0 + 1}`", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void placeholderInsideRegexKeepsExistingSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("/^%name%$/.test(value)", ignored -> "Ben.* /admin", + bindings::put); + + assertEquals("/^Ben\\.\\* \\/admin$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void encodedCustomPlaceholderInsideStringCannotBreakOut() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, 'op attacker'); '"; + String encoded = JavascriptPlaceholderValue.encode(injection); + + String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "'", token -> token, bindings::put); + + assertEquals("'\\'; Bukkit.dispatchCommand(Console, \\'op attacker\\'); \\''", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void preservesPrimitiveTypesForExpressionPlaceholders() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%allowed% && %count% > 2 && %ratio% < 2.0", token -> { + if (token.equals("%allowed%")) { + return "true"; + } + if (token.equals("%count%")) { + return "5"; + } + return "1.5"; + }, bindings::put); + + assertEquals("__advancedCorePlaceholder0 && __advancedCorePlaceholder1 > 2 && __advancedCorePlaceholder2 < 2.0", + prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder1")); + assertEquals(Double.valueOf(1.5), bindings.get("__advancedCorePlaceholder2")); + } + + @Test + void unresolvedTokensRemainUntouched() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%unknown% == true", token -> token, bindings::put); + + assertEquals("%unknown% == true", prepared); + assertTrue(bindings.isEmpty()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java new file mode 100644 index 000000000..adc86acbc --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java @@ -0,0 +1,47 @@ +package com.bencodez.advancedcore.api.messages; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +class PlaceholderUtilsJavascriptBoundaryTest { + + @Test + void customPlaceholderCannotCreateJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("value", "[Javascript=Bukkit.dispatchCommand(Console,'op attacker')]"); + + String result = PlaceholderUtils.replacePlaceHolder("prefix %value%", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertEquals("prefix [Javascript =Bukkit.dispatchCommand(Console,'op attacker')]", result); + } + + @Test + void multipleSubstitutionsCannotAssembleJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("part1", "Java"); + placeholders.put("part2", "script"); + + String result = PlaceholderUtils.replacePlaceHolder("[%part1%%part2%=danger]", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertEquals("[Javascript =danger]", result); + } + + @Test + void customValuesInsideAuthoredMarkerAreEncodedAsData() { + HashMap placeholders = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console,'op attacker'); '"; + placeholders.put("value", injection); + + String result = PlaceholderUtils.replacePlaceHolder("[Javascript='%value%']", placeholders); + + assertTrue(result.startsWith("[Javascript='%__advancedcore_bound_")); + assertFalse(result.contains(injection)); + } +} From fed11f9f39855877abe892f2fc56fcdbbfa66159 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 18:59:15 -0600 Subject: [PATCH 02/63] Isolate JavaScript parser compatibility tests --- .../api/javascript/JavascriptPlaceholderBinderTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java index 323ac0b98..79a70ed70 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -6,10 +6,17 @@ import java.util.HashMap; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class JavascriptPlaceholderBinderTest { + @BeforeEach + void useTestClasspathNashornParser() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + @Test void ordinaryJavascriptWithoutPlaceholdersIsUntouched() { HashMap bindings = new HashMap<>(); From ee31bd8f2ee09727aabb9adb736e3d7f0522c15b Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:26:23 -0600 Subject: [PATCH 03/63] Prepare JavaScript compatibility fixes --- .../workflows/fix-js-placeholder-compat.yml | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 .github/workflows/fix-js-placeholder-compat.yml diff --git a/.github/workflows/fix-js-placeholder-compat.yml b/.github/workflows/fix-js-placeholder-compat.yml new file mode 100644 index 000000000..7a18623ef --- /dev/null +++ b/.github/workflows/fix-js-placeholder-compat.yml @@ -0,0 +1,256 @@ +name: Apply JavaScript placeholder compatibility fixes +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply fixes and tests + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + binder = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = binder.read_text() + + old = ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%");' + new = ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%|(? placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); +''' + new = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens + // are AdvancedCore's legacy custom placeholder form and are resolved below. + if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); +''' + if old not in text: + raise SystemExit('resolve block not found') + text = text.replace(old, new, 1) + + marker = ''' private static String escapeString(String value, char quote) { +''' + helper = ''' private static char literalDelimiter(String expression, Range range) { + int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; + for (int candidate : candidates) { + if (candidate < 0 || candidate >= expression.length()) { + continue; + } + char value = expression.charAt(candidate); + if (value == '\\'' || value == '"' || value == '`') { + return value; + } + } + // A parsed STRING_LITERAL should always have a source delimiter nearby. If + // an engine reports an unusual range, use the enclosing source text as a + // final local check rather than treating placeholder data as executable code. + for (int i = Math.max(0, range.start - 2); i <= Math.min(expression.length() - 1, range.start + 1); i++) { + char value = expression.charAt(i); + if (value == '\\'' || value == '"' || value == '`') { + return value; + } + } + return '\\''; + } + +''' + if marker not in text: + raise SystemExit('escapeString marker not found') + text = text.replace(marker, helper + marker, 1) + + old = ''' private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + if (handler.getNashornClassLoader() != null) { + return handler.getNashornClassLoader(); + } + ScriptEngine cached = handler.getCachedEngine(); + if (cached != null && cached.getClass().getClassLoader() != null) { + return cached.getClass().getClassLoader(); + } + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + try { + Class.forName(PARSER_CLASS, false, own); + return own; + } catch (ClassNotFoundException ignored) { + return null; + } + } +''' + new = ''' private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + ClassLoader downloaded = handler.getNashornClassLoader(); + if (canLoadParser(downloaded)) { + return downloaded; + } + + // nashorn-core is packaged with AdvancedCore so parser support remains + // available even when the active ScriptEngine is Rhino/GraalJS. + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + if (canLoadParser(own)) { + return own; + } + + ScriptEngine cached = handler.getCachedEngine(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + return canLoadParser(cachedLoader) ? cachedLoader : null; + } + + private static boolean canLoadParser(ClassLoader loader) { + if (loader == null) { + return false; + } + try { + Class.forName(PARSER_CLASS, false, loader); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + return false; + } + } +''' + if old not in text: + raise SystemExit('parserClassLoader block not found') + text = text.replace(old, new, 1) + binder.write_text(text) + + pom = Path('AdvancedCore/pom.xml') + text = pom.read_text() + old = ''' + org.openjdk.nashorn + nashorn-core + 15.7 + provided + ''' + new = ''' + org.openjdk.nashorn + nashorn-core + 15.7 + compile + ''' + if old not in text: + raise SystemExit('nashorn dependency block not found') + pom.write_text(text.replace(old, new, 1)) + + test = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java') + text = test.read_text() + insert = ''' + @Test + void exactQuotedNumericLookingPlaceholderRemainsAString() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); + + assertEquals("'001' === '001'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", + token -> token.equals("{count}") ? "5" : token, bindings::put); + + assertEquals("__advancedCorePlaceholder0 > 0", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", + token -> token.equals("%name%") ? "Ben" : token, bindings::put); + + assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } +''' + marker = '\n @Test\n void unresolvedTokensRemainUntouched() {' + if marker not in text: + raise SystemExit('test insertion marker not found') + text = text.replace(marker, insert + marker, 1) + test.write_text(text) + PY + rm -f .github/workflows/fix-js-placeholder-compat.yml + git config user.name "Ben" + git config user.email "benbergen12@gmail.com" + git add -A + git commit -m "Fix automatic JavaScript placeholder compatibility" + git push origin HEAD:security/javascript-placeholder-bindings From 52e406eaa3a364b3e58d3f80a1f34133448fc681 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:29:36 -0600 Subject: [PATCH 04/63] Fix JavaScript compatibility workflow --- .../workflows/fix-js-placeholder-compat.yml | 241 +----------------- 1 file changed, 5 insertions(+), 236 deletions(-) diff --git a/.github/workflows/fix-js-placeholder-compat.yml b/.github/workflows/fix-js-placeholder-compat.yml index 7a18623ef..7525770bf 100644 --- a/.github/workflows/fix-js-placeholder-compat.yml +++ b/.github/workflows/fix-js-placeholder-compat.yml @@ -15,242 +15,11 @@ jobs: - name: Apply fixes and tests shell: bash run: | - python3 <<'PY' - from pathlib import Path - - binder = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = binder.read_text() - - old = ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%");' - new = ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%|(? placeholders) { - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } - - if (placeholders != null) { - String name = token.substring(1, token.length() - 1); -''' - new = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens - // are AdvancedCore's legacy custom placeholder form and are resolved below. - if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } - - if (placeholders != null) { - String name = token.substring(1, token.length() - 1); -''' - if old not in text: - raise SystemExit('resolve block not found') - text = text.replace(old, new, 1) - - marker = ''' private static String escapeString(String value, char quote) { -''' - helper = ''' private static char literalDelimiter(String expression, Range range) { - int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; - for (int candidate : candidates) { - if (candidate < 0 || candidate >= expression.length()) { - continue; - } - char value = expression.charAt(candidate); - if (value == '\\'' || value == '"' || value == '`') { - return value; - } - } - // A parsed STRING_LITERAL should always have a source delimiter nearby. If - // an engine reports an unusual range, use the enclosing source text as a - // final local check rather than treating placeholder data as executable code. - for (int i = Math.max(0, range.start - 2); i <= Math.min(expression.length() - 1, range.start + 1); i++) { - char value = expression.charAt(i); - if (value == '\\'' || value == '"' || value == '`') { - return value; - } - } - return '\\''; - } - -''' - if marker not in text: - raise SystemExit('escapeString marker not found') - text = text.replace(marker, helper + marker, 1) - - old = ''' private static ClassLoader parserClassLoader() { - JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); - if (handler.getNashornClassLoader() != null) { - return handler.getNashornClassLoader(); - } - ScriptEngine cached = handler.getCachedEngine(); - if (cached != null && cached.getClass().getClassLoader() != null) { - return cached.getClass().getClassLoader(); - } - ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); - try { - Class.forName(PARSER_CLASS, false, own); - return own; - } catch (ClassNotFoundException ignored) { - return null; - } - } -''' - new = ''' private static ClassLoader parserClassLoader() { - JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); - ClassLoader downloaded = handler.getNashornClassLoader(); - if (canLoadParser(downloaded)) { - return downloaded; - } - - // nashorn-core is packaged with AdvancedCore so parser support remains - // available even when the active ScriptEngine is Rhino/GraalJS. - ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); - if (canLoadParser(own)) { - return own; - } - - ScriptEngine cached = handler.getCachedEngine(); - ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); - return canLoadParser(cachedLoader) ? cachedLoader : null; - } - - private static boolean canLoadParser(ClassLoader loader) { - if (loader == null) { - return false; - } - try { - Class.forName(PARSER_CLASS, false, loader); - return true; - } catch (ClassNotFoundException | LinkageError ignored) { - return false; - } - } -''' - if old not in text: - raise SystemExit('parserClassLoader block not found') - text = text.replace(old, new, 1) - binder.write_text(text) - - pom = Path('AdvancedCore/pom.xml') - text = pom.read_text() - old = ''' - org.openjdk.nashorn - nashorn-core - 15.7 - provided - ''' - new = ''' - org.openjdk.nashorn - nashorn-core - 15.7 - compile - ''' - if old not in text: - raise SystemExit('nashorn dependency block not found') - pom.write_text(text.replace(old, new, 1)) - - test = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java') - text = test.read_text() - insert = ''' - @Test - void exactQuotedNumericLookingPlaceholderRemainsAString() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); - - assertEquals("'001' === '001'", prepared); - assertTrue(bindings.isEmpty()); - } - - @Test - void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", - token -> token.equals("{count}") ? "5" : token, bindings::put); - - assertEquals("__advancedCorePlaceholder0 > 0", prepared); - assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); - } - - @Test - void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", - token -> token.equals("%name%") ? "Ben" : token, bindings::put); - - assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); - assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); - } -''' - marker = '\n @Test\n void unresolvedTokensRemainUntouched() {' - if marker not in text: - raise SystemExit('test insertion marker not found') - text = text.replace(marker, insert + marker, 1) - test.write_text(text) - PY + echo 'CmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKZGVmIHJlcGxhY2Vfb25jZSh0ZXh0LCBvbGQsIG5ldywgbGFiZWwpOgogICAgY291bnQgPSB0ZXh0LmNvdW50KG9sZCkKICAgIGlmIGNvdW50ICE9IDE6CiAgICAgICAgcmFpc2UgU3lzdGVtRXhpdChmIntsYWJlbH06IGV4cGVjdGVkIDEgbWF0Y2gsIGdvdCB7Y291bnR9IikKICAgIHJldHVybiB0ZXh0LnJlcGxhY2Uob2xkLCBuZXcsIDEpCgpiYXNlID0gUGF0aCgiQWR2YW5jZWRDb3JlL3NyYy9tYWluL2phdmEvY29tL2JlbmNvZGV6L2FkdmFuY2VkY29yZS9hcGkvamF2YXNjcmlwdC9KYXZhc2NyaXB0UGxhY2Vob2xkZXJCaW5kZXIuamF2YSIpCnRleHQgPSBiYXNlLnJlYWRfdGV4dCgpCgp0ZXh0ID0gcmVwbGFjZV9vbmNlKAogICAgdGV4dCwKICAgICcgICAgcHJpdmF0ZSBzdGF0aWMgZmluYWwgUGF0dGVybiBQTEFDRUhPTERFUiA9IFBhdHRlcm4uY29tcGlsZSgiJShbXiVcXFxcc10rKSU iKTsnLnJlcGxhY2UoIiAiLCAiIiksCiAgICAnICAgIHByaXZhdGUgc3RhdGljIGZpbmFsIFBhdHRlcm4gUExBQ0VIT0xERVIgPSBQYXR0ZXJuLmNvbXBpbGUoIiUoW14lXFxcXHNdKyklfCg/PCFcXFwkKVxcXHsoW157fSVcXFxc c10rKVxcXH0iKTsnLnJlcGxhY2UoIiAiLCAiIiksCiAgICAicGxhY2Vob2xkZXIgcGF0dGVybiIsCikKCm9sZCA9ICcnJyAgICAgICAgICAgIG1hdGNoZXMuYWRkKG5ldyBQbGFjZWhvbGRlck1hdGNoKG1hdGNoZXIuc3RhcnQoKSwgbWF0Y2hlci5lbmQoKSwgdG9rZW4sIHZhbHVlKSk7CiAgICAgICAgICAgIC8vIEtlZXAgYWxsIHNvdXJjZSBvZmZzZXRzIHVuY2hhbmdlZCB3aGlsZSBtYWtpbmcgYSBiYXJlICVwbGFjZWhvbGRlciUKICAgICAgICAgICAgLy8gcGFyc2UgYXMgYW4gb3JkaW5hcnkgaWRlbnRpZmllci4KICAgICAgICAgICAgZm9yIChpbnQgaSA9IG1hdGNoZXIuc3RhcnQoKTsgaSA8IG1hdGNoZXIuZW5kKCk7IGkrKykgewogICAgICAgICAgICAgICAgc2FuaXRpemVkLnNldENoYXJBdChpLCAncCcpOwogICAgICAgICAgICB9CicnJwpuZXcgPSAnJycgICAgICAgICAgICBtYXRjaGVzLmFkZChuZXcgUGxhY2Vob2xkZXJNYXRjaChtYXRjaGVyLnN0YXJ0KCksIG1hdGNoZXIuZW5kKCksIHRva2VuLCB2YWx1ZSkpOwogICAgICAgICAgICAvLyBLZWVwIGFsbCBzb3VyY2Ugb2Zmc2V0cyB1bmNoYW5nZWQgd2hpbGUgbWFraW5nIHJlc29sdmVkIHBsYWNlaG9sZGVycyBwYXJzZQogICAgICAgICAgICAvLyBhcyBhbiBvcmRpbmFyeSBpZGVudGlmaWVyLiBVbnJlc29sdmVkIGJyYWNlIHN5bnRheCBtYXkgYmUgdmFsaWQgSmF2YVNjcmlwdAogICAgICAgICAgICAvLyAoZm9yIGV4YW1wbGUgYW4gb2JqZWN0L2Jsb2NrKSwgc28gb25seSBzYW5pdGl6ZSBicmFjZSBwbGFjZWhvbGRlcnMgd2hlbgogICAgICAgICAgICAvLyB0aGV5IGFjdHVhbGx5IHJlc29sdmUgYXMgQWR2YW5jZWRDb3JlIGN1c3RvbSBkYXRhLgogICAgICAgICAgICBib29sZWFuIGJyYWNlUGxhY2Vob2xkZXIgPSB0b2tlbi5jaGFyQXQoMCkgPT0gJ3snOwogICAgICAgICAgICBpZiAoIWJyYWNlUGxhY2Vob2xkZXIgfHwgKHZhbHVlICE9IG51bGwgJiYgIXZhbHVlLmVxdWFscyh0b2tlbikpKSB7CiAgICAgICAgICAgICAgICBmb3IgKGludCBpID0gbWF0Y2hlci5zdGFydCgpOyBpIDwgbWF0Y2hlci5lbmQoKTsgaSsrKSB7CiAgICAgICAgICAgICAgICAgICAgc2FuaXRpemVkLnNldENoYXJBdChpLCAncCcpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAic2FuaXRpemF0aW9uIikKCm9sZCA9ICcnJyAgICAgICAgICAgIGlmIChyZWdleCAhPSBudWxsKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVSZWdleChtYXRjaC52YWx1ZSwgZXhwcmVzc2lvbiwgcmVnZXgsIG1hdGNoLnN0YXJ0KTsKICAgICAgICAgICAgfSBlbHNlIGlmIChzdHJpbmcgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmVwbGFjZW1lbnRzW2ldID0gZXNjYXBlU3RyaW5nKG1hdGNoLnZhbHVlLCBleHByZXNzaW9uLmNoYXJBdChzdHJpbmcuc3RhcnQpKTsKICAgICAgICAgICAgfSBlbHNlIGlmICh0ZW1wbGF0ZSAhPSBudWxsICYmICFjb250ZXh0cy5pbnNpZGVUZW1wbGF0ZUV4cHJlc3Npb24obWF0Y2guc3RhcnQpKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgIH0gZWxzZSB7CicnJwpuZXcgPSAnJycgICAgICAgICAgICBpZiAocmVnZXggIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmVwbGFjZW1lbnRzW2ldID0gZXNjYXBlUmVnZXgobWF0Y2gudmFsdWUsIGV4cHJlc3Npb24sIHJlZ2V4LCBtYXRjaC5zdGFydCk7CiAgICAgICAgICAgIH0gZWxzZSBpZiAoc3RyaW5nICE9IG51bGwpIHsKICAgICAgICAgICAgICAgIGNoYXIgZGVsaW1pdGVyID0gbGl0ZXJhbERlbGltaXRlcihleHByZXNzaW9uLCBzdHJpbmcpOwogICAgICAgICAgICAgICAgaWYgKGRlbGltaXRlciA9PSAnYCcgJiYgIWNvbnRleHRzLmluc2lkZVRlbXBsYXRlRXhwcmVzc2lvbihtYXRjaC5zdGFydCkpIHsKICAgICAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIHJlcGxhY2VtZW50c1tpXSA9IGVzY2FwZVN0cmluZyhtYXRjaC52YWx1ZSwgZGVsaW1pdGVyKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfSBlbHNlIGlmICh0ZW1wbGF0ZSAhPSBudWxsICYmICFjb250ZXh0cy5pbnNpZGVUZW1wbGF0ZUV4cHJlc3Npb24obWF0Y2guc3RhcnQpKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgIH0gZWxzZSB7CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAiY29udGV4dCBibG9jayIpCgpvbGQgPSAnJycgICAgcHJpdmF0ZSBzdGF0aWMgU3RyaW5nIHJlc29sdmUoU3RyaW5nIHRva2VuLCBPZmZsaW5lUGxheWVyIHBsYXllciwgTWFwPFN0cmluZywgU3RyaW5nPiBwbGFjZWhvbGRlcnMpIHsKICAgICAgICBBZHZhbmNlZENvcmVQbHVnaW4gcGx1Z2luID0gQWR2YW5jZWRDb3JlUGx1Z2luLmdldEluc3RhbmNlKCk7CiAgICAgICAgaWYgKHBsYXllciAhPSBudWxsICYmIHBsdWdpbiAhPSBudWxsICYmIHBsdWdpbi5pc1BsYWNlSG9sZGVyQVBJRW5hYmxlZCgpKSB7CiAgICAgICAgICAgIFN0cmluZyByZXNvbHZlZCA9IFBsYWNlaG9sZGVyQVBJLnNldFBsYWNlaG9sZGVycyhwbGF5ZXIsIHRva2VuKTsKICAgICAgICAgICAgaWYgKHJlc29sdmVkICE9IG51bGwgJiYgIXJlc29sdmVkLmVxdWFscyh0b2tlbikpIHsKICAgICAgICAgICAgICAgIHJldHVybiByZXNvbHZlZDsKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgaWYgKHBsYWNlaG9sZGVycyAhPSBudWxsKSB7CiAgICAgICAgICAgIFN0cmluZyBuYW1lID0gdG9rZW4uc3Vic3RyaW5nKDEsIHRva2VuLmxlbmd0aCgpIC0gMSk7CicnJwpuZXcgPSAnJycgICAgcHJpdmF0ZSBzdGF0aWMgU3RyaW5nIHJlc29sdmUoU3RyaW5nIHRva2VuLCBPZmZsaW5lUGxheWVyIHBsYXllciwgTWFwPFN0cmluZywgU3RyaW5nPiBwbGFjZWhvbGRlcnMpIHsKICAgICAgICBBZHZhbmNlZENvcmVQbHVnaW4gcGx1Z2luID0gQWR2YW5jZWRDb3JlUGx1Z2luLmdldEluc3RhbmNlKCk7CiAgICAgICAgLy8gUGxhY2Vob2xkZXJBUGkgdXNlcyBwZXJjZW50LWRlbGltaXRlZCBwbGFjZWhvbGRlcnMuIEJyYWNlLWRlbGltaXRlZCB0b2tlbnMKICAgICAgICAvLyBhcmUgQWR2YW5jZWRDb3JlJ3MgbGVnYWN5IGN1c3RvbSBwbGFjZWhvbGRlciBmb3JtIGFuZCBhcmUgcmVzb2x2ZWQgYmVsb3cuCiAgICAgICAgaWYgKHRva2VuLnN0YXJ0c1dpdGgoIiUiKSAmJiBwbGF5ZXIgIT0gbnVsbCAmJiBwbHVnaW4gIT0gbnVsbCAmJiBwbHVnaW4uaXNQbGFjZUhvbGRlckFQSUVuYWJsZWQoKSkgewogICAgICAgICAgICBTdHJpbmcgcmVzb2x2ZWQgPSBQbGFjZWhvbGRlckFQSS5zZXRQbGFjZWhvbGRlcnMocGxheWVyLCB0b2tlbik7CiAgICAgICAgICAgIGlmIChyZXNvbHZlZCAhPSBudWxsICYmICFyZXNvbHZlZC5lcXVhbHModG9rZW4pKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gcmVzb2x2ZWQ7CiAgICAgICAgICAgIH0KICAgICAgICB9CgogICAgICAgIGlmIChwbGFjZWhvbGRlcnMgIT0gbnVsbCkgewogICAgICAgICAgICBTdHJpbmcgbmFtZSA9IHRva2VuLnN1YnN0cmluZygxLCB0b2tlbi5sZW5ndGgoKSAtIDEpOwonJycKdGV4dCA9IHJlcGxhY2Vfb25jZSh0ZXh0LCBvbGQsIG5ldywgInJlc29sdmUgYmxvY2siKQoKbWFya2VyID0gJycnICAgIHByaXZhdGUgc3RhdGljIFN0cmluZyBlc2NhcGVTdHJpbmcoU3RyaW5nIHZhbHVlLCBjaGFyIHF1b3RlKSB7CicnJwpoZWxwZXIgPSByJycnICAgIHByaXZhdGUgc3RhdGljIGNoYXIgbGl0ZXJhbERlbGltaXRlcihTdHJpbmcgZXhwcmVzc2lvbiwgUmFuZ2UgcmFuZ2UpIHsKICAgICAgICBpbnRbXSBjYW5kaWRhdGVzID0geyByYW5nZS5zdGFydCAtIDEsIHJhbmdlLnN0YXJ0LCByYW5nZS5lbmQsIHJhbmdlLmVuZCAtIDEgfTsKICAgICAgICBmb3IgKGludCBjYW5kaWRhdGUgOiBjYW5kaWRhdGVzKSB7CiAgICAgICAgICAgIGlmIChjYW5kaWRhdGUgPCAwIHx8IGNhbmRpZGF0ZSA+PSBleHByZXNzaW9uLmxlbmd0aCgpKSB7CiAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjaGFyIHZhbHVlID0gZXhwcmVzc2lvbi5jaGFyQXQoY2FuZGlkYXRlKTsKICAgICAgICAgICAgaWYgKHZhbHVlID09ICdcJycgfHwgdmFsdWUgPT0gJyInIHx8IHZhbHVlID09ICdgJykgewogICAgICAgICAgICAgICAgcmV0dXJuIHZhbHVlOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGZvciAoaW50IGkgPSBNYXRoLm1heCgwLCByYW5nZS5zdGFydCAtIDIpOwogICAgICAgICAgICAgICAgaSA8PSBNYXRoLm1pbihleHByZXNzaW9uLmxlbmd0aCgpIC0gMSwgcmFuZ2Uuc3RhcnQgKyAxKTsgaSsrKSB7CiAgICAgICAgICAgIGNoYXIgdmFsdWUgPSBleHByZXNzaW9uLmNoYXJBdChpKTsKICAgICAgICAgICAgaWYgKHZhbHVlID09ICdcJycgfHwgdmFsdWUgPT0gJyInIHx8IHZhbHVlID09ICdgJykgewogICAgICAgICAgICAgICAgcmV0dXJuIHZhbHVlOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIHJldHVybiAnXCcnOwogICAgfQoKJycnCmlmIG1hcmtlciBub3QgaW4gdGV4dDoKICAgIHJhaXNlIFN5c3RlbUV4aXQoImVzY2FwZSBoZWxwZXIgbWFya2VyIG5vdCBmb3VuZCIpCnRleHQgPSB0ZXh0LnJlcGxhY2UobWFya2VyLCBoZWxwZXIgKyBtYXJrZXIsIDEpCgpvbGQgPSAnJycgICAgICAgIHByaXZhdGUgc3RhdGljIENsYXNzTG9hZGVyIHBhcnNlckNsYXNzTG9hZGVyKCkgewogICAgICAgICAgICBKYXZhc2NyaXB0RW5naW5lSGFuZGxlciBoYW5kbGVyID0gSmF2YXNjcmlwdEVuZ2luZUhhbmRsZXIuZ2V0SW5zdGFuY2UoKTsKICAgICAgICAgICAgaWYgKGhhbmRsZXIuZ2V0TmFzaG9ybkNsYXNzTG9hZGVyKCkgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmV0dXJuIGhhbmRsZXIuZ2V0TmFzaG9ybkNsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgU2NyaXB0RW5naW5lIGNhY2hlZCA9IGhhbmRsZXIuZ2V0Q2FjaGVkRW5naW5lKCk7CiAgICAgICAgICAgIGlmIChjYWNoZWQgIT0gbnVsbICYmIGNhY2hlZC5nZXRDbGFzcygpLmdldENsYXNzTG9hZGVyKCkgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmV0dXJuIGNhY2hlZC5nZXRDbGFzcygpLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQ2xhc3NMb2FkZXIgb3duID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmNsYXNzLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBDbGFzcy5mb3JOYW1lKFBBUlNFUl9DTEFTUywgZmFsc2UsIG93bik7CiAgICAgICAgICAgICAgICByZXR1cm4gb3duOwogICAgICAgICAgICB9IGNhdGNoIChDbGFzc05vdEZvdW5kRXhjZXB0aW9uIGlnbm9yZWQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBudWxsOwogICAgICAgICAgICB9CiAgICAgICAgfQonJycKbmV3ID0gJycnICAgICAgICBwcml2YXRlIHN0YXRpYyBDbGFzc0xvYWRlciBwYXJzZXJDbGFzc0xvYWRlcigpIHsKICAgICAgICAgICAgSmF2YXNjcmlwdEVuZ2luZUhhbmRsZXIgaGFuZGxlciA9IEphdmFzY3JpcHRFbmdpbmVIYW5kbGVyLmdldEluc3RhbmNlKCk7CiAgICAgICAgICAgIENsYXNzTG9hZGVyIGRvd25sb2FkZWQgPSBoYW5kbGVyLmdldE5hc2hvcm5DbGFzc0xvYWRlcigpOwogICAgICAgICAgICBpZiAoY2FuTG9hZFBhcnNlcihkb3dubG9hZGVkKSkgewogICAgICAgICAgICAgICAgcmV0dXJuIGRvd25sb2FkZWQ7CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIC8vIG5hc2hvcm4tY29yZSBpcyBwYWNrYWdlZCB3aXRoIEFkdmFuY2VkQ29yZSBzbyBwYXJzZXIgc3VwcG9ydCByZW1haW5zCiAgICAgICAgICAgIC8vIGF2YWlsYWJsZSBldmVuIHdoZW4gdGhlIGFjdGl2ZSBTY3JpcHRFbmdpbmUgaXMgUmhpbm8vR3JhYWxKUy4KICAgICAgICAgICAgQ2xhc3NMb2FkZXIgb3duID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmNsYXNzLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIGlmIChjYW5Mb2FkUGFyc2VyKG93bikpIHsKICAgICAgICAgICAgICAgIHJldHVybiBvd247CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIFNjcmlwdEVuZ2luZSBjYWNoZWQgPSBoYW5kbGVyLmdldENhY2hlZEVuZ2luZSgpOwogICAgICAgICAgICBDbGFzc0xvYWRlciBjYWNoZWRMb2FkZXIgPSBjYWNoZWQgPT0gbnVsbCA/I G51bGwgOiBjYWNoZWQuZ2V0Q2xhc3MoKS5nZXRDbGFzc0xvYWRlcigpOwogICAgICAgICAgICByZXR1cm4gY2FuTG9hZFBhcnNlcihjYWNoZWRMb2FkZXIpID8gY2FjaGVkTG9hZGVyIDogbnVsbDsKICAgICAgICB9CgogICAgICAgIHByaXZhdGUgc3RhdGljIGJvb2xlYW4gY2FuTG9hZFBhcnNlcihDbGFzc0xvYWRlciBsb2FkZXIpIHsKICAgICAgICAgICAgaWYgKGxvYWRlciA9PSBudWxsKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgICAgIENsYXNzLmZvck5hbWUoUEFSU0VSX0NMQVNTLCBmYWxzZSwgbG9hZGVyKTsKICAgICAgICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgICAgICB9IGNhdGNoIChDbGFzc05vdEZvdW5kRXhjZXB0aW9uIHwgTGlua2FnZUVycm9yIGlnbm9yZWQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBmYWxzZTsKICAgICAgICAgICAgfQogICAgICAgIH0KJycnCmlmIG9sZCBub3QgaW4gdGV4dDoKICAgIHJhaXNlIFN5c3RlbUV4aXQoInBhcnNlciBsb2FkZXIgYmxvY2sgbm90IGZvdW5kIikKdGV4dCA9IHRleHQucmVwbGFjZShvbGQsIG5ldywgMSkKYmFzZS53cml0ZV90ZXh0KHRleHQpCgpwb20gPSBQYXRoKCJBZHZhbmNlZENvcmUvcG9tLnhtbCIpCnRleHQgPSBwb20ucmVhZF90ZXh0KCkKb2xkID0gJycnICAgICAgICA8ZGVwZW5kZW5jeT4KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLm9wZW5qZGs ubmFzaG9ybjwvZ3JvdXBJZD4KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bmFzaG9ybi1jb3JlPC9hcnRpZmFjdElkPgogICAgICAgICAgICA8dmVyc2lvbj4xNS43PC92ZXJzaW9uPgogICAgICAgICAgICA8c2NvcGU+cHJvdmlkZWQ8L3Njb3BlPgogICAgICAgIDwvZGVwZW5kZW5jeT4nJycKbmV3ID0gJycnICAgICAgICA8ZGVwZW5kZW5jeT4KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLm9wZW5qZGs ubmFzaG9ybjwvZ3JvdXBJZD4KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bmFzaG9ybi1jb3JlPC9hcnRpZmFjdElkPgogICAgICAgICAgICA8dmVyc2lvbj4xNS43PC92ZXJzaW9uPgogICAgICAgICAgICA8c2NvcGU+Y29tcGlsZTwvc2NvcGU+CiAgICAgICAgPC9kZXBlbmRlbmN5PicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAibmFzaG9ybiBkZXBlbmRlbmN5IikKcG9tLndyaXRlX3RleHQodGV4dCkKCnRlc3QgPSBQYXRoKCJBZHZhbmNlZENvcmUvc3JjL3Rlc3QvamF2YS9jb20vYmVuY29kZXovYWR2YW5jZWRjb3JlL2FwaS9qYXZhc2NyaXB0L0phdmFzY3JpcHRQbGFjZWhvbGRlckJpbmRlclRlc3QuamF2YSIpCnRleHQgPSB0ZXN0LnJlYWRfdGV4dCgpCmluc2VydCA9IHInJycKICAgIEBUZXN0CiAgICB2b2lkIGV4YWN0UXVvdGVkTnVtZXJpY0xvb2tpbmdQbGFjZWhvbGRlclJlbWFpbnNBU3RyaW5nKCkgewogICAgICAgIEhhc2hNYXA8U3RyaW5nLCBPYmplY3Q+IGJpbmRpbmdzID0gbmV3IEhhc2hNYXAoKTsgCiAgICAgICAgU3RyaW5nIHByZXBhcmVkID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmJpbmQoIiclY29kZSU nID09PSAnMDAxJyIsIGlnbm9yZWQgLT4gIjAwMSIsIGJpbmRpbmdzOjpwdXQpOwogICAgICAgIGFzc2VydEVxdWFscygiJzAwMScgPT09ICcwMDEnIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydFRydWUoYmluZGluZ3MuaXNFbXB0eSgpKTsKICAgIH0KCiAgICBAVGVzdAogICAgdm9pZCBicmFjZURlbGltaXRlZEN1c3RvbVBsYWNlaG9sZGVySXNBdXRvbWF0aWNhbGx5Qm91bmQoKSB7CiAgICAgICAgSGFzaE1hcDxTdHJpbmcsIE9iamVjdD4gYmluZGluZ3MgPSBuZXcgSGFzaE1hcDw+KCk7CiAgICAgICAgU3RyaW5nIHByZXBhcmVkID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmJpbmQoIntjb3VudH0gPiAwIiwKICAgICAgICAgICAgICAgIHRva2VuIC0+IHRva2VuLmVxdWFscygie2NvdW50fSIpID8gIjUiIDogdG9rZW4sIGJpbmRpbmdzOjpwdXQpOwogICAgICAgIGFzc2VydEVxdWFscygiX19hZHZhbmNlZENvcmVQbGFjZWhvbGRlcjAgPiAwIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydEVxdWFscyhMb25nLnZhbHVlT2YoNSksIGJpbmRpbmdzLmdldCgiX19hZHZhbmNlZENvcmVQbGFjZWhvbGRlcjAiKSk7CiAgICB9CgogICAgQFRlc3QKICAgIHZvaWQgdW5yZXNvbHZlZEJyYWNlU3ludGF4UmVtYWluc09yZGluYXJ5SmF2YXNjcmlwdCgpIHsKICAgICAgICBIYXNoTWFwPFN0cmluZywgT2JqZWN0PiBiaW5kaW5ncyA9IG5ldyBIYXNoTWFwPD4oKTsKICAgICAgICBTdHJpbmcgcHJlcGFyZWQgPSBKYXZhc2NyaXB0UGxhY2Vob2xkZXJCaW5kZXIuYmluZCgidmFyIHZhbHVlID0ge2NvdW50OiAxfTsgJW5hbWUlIiwKICAgICAgICAgICAgICAgIHRva2VuIC0+IHRva2VuLmVxdWFscygiJW5hbWUlIikgPyAiQmVuIiA6IHRva2VuLCBiaW5kaW5nczo6cHV0KTsKICAgICAgICBhc3NlcnRFcXVhbHMoInZhciB2YWx1ZSA9IHtjb3VudDogMX07IF9fYWR2YW5jZWRDb3JlUGxhY2Vob2xkZXIwIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydEVxdWFscygiQmVuIiwgYmluZGluZ3MuZ2V0KCJfX2FkdmFuY2VkQ29yZVBsYWNlaG9sZGVyMCIpKTsKICAgIH0KCicnJwptYXJrZXIgPSAnJycKICAgIEBUZXN0CiAgICB2b2lkIHVucmVzb2x2ZWRUb2tlbnNSZW1haW5VbnRvdWNoZWQoKSB7CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG1hcmtlciwgIlxuIiArIGluc2VydCArIG1hcmtlci5sc3RyaXAoIlxuIiksICJ0ZXN0IG1hcmtlciIpCnRlc3Qud3JpdGVfdGV4dCh0ZXh0KQo=' | base64 -d > /tmp/fix.py + python3 /tmp/fix.py rm -f .github/workflows/fix-js-placeholder-compat.yml - git config user.name "Ben" - git config user.email "benbergen12@gmail.com" + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' git add -A - git commit -m "Fix automatic JavaScript placeholder compatibility" + git commit -m 'Fix automatic JavaScript placeholder compatibility' git push origin HEAD:security/javascript-placeholder-bindings From c8a958bee58c8b25e80e27206e7e6e6df54b78e6 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:30:34 -0600 Subject: [PATCH 05/63] Add JavaScript compatibility patch script --- .github/fix_js_placeholder_compat.py | 229 +++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 .github/fix_js_placeholder_compat.py diff --git a/.github/fix_js_placeholder_compat.py b/.github/fix_js_placeholder_compat.py new file mode 100644 index 000000000..b90ce1f74 --- /dev/null +++ b/.github/fix_js_placeholder_compat.py @@ -0,0 +1,229 @@ +from pathlib import Path + + +def replace_once(text, old, new, label): + count = text.count(old) + if count != 1: + raise SystemExit(f"{label}: expected 1 match, got {count}") + return text.replace(old, new, 1) + + +binder = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") +text = binder.read_text() + +text = replace_once( + text, + ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%");', + ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%|(? placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); +''' +new = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens + // are AdvancedCore's legacy custom placeholder form and are resolved below. + if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); +''' +text = replace_once(text, old, new, "resolve block") + +marker = ''' private static String escapeString(String value, char quote) { +''' +helper = ''' private static char literalDelimiter(String expression, Range range) { + int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; + for (int candidate : candidates) { + if (candidate < 0 || candidate >= expression.length()) { + continue; + } + char value = expression.charAt(candidate); + if (value == '\\'' || value == '"' || value == '`') { + return value; + } + } + for (int i = Math.max(0, range.start - 2); + i <= Math.min(expression.length() - 1, range.start + 1); i++) { + char value = expression.charAt(i); + if (value == '\\'' || value == '"' || value == '`') { + return value; + } + } + return '\\''; + } + +''' +text = replace_once(text, marker, helper + marker, "escape helper marker") + +old = ''' private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + if (handler.getNashornClassLoader() != null) { + return handler.getNashornClassLoader(); + } + ScriptEngine cached = handler.getCachedEngine(); + if (cached != null && cached.getClass().getClassLoader() != null) { + return cached.getClass().getClassLoader(); + } + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + try { + Class.forName(PARSER_CLASS, false, own); + return own; + } catch (ClassNotFoundException ignored) { + return null; + } + } +''' +new = ''' private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + ClassLoader downloaded = handler.getNashornClassLoader(); + if (canLoadParser(downloaded)) { + return downloaded; + } + + // nashorn-core is packaged with AdvancedCore so parser support remains + // available even when the active ScriptEngine is Rhino/GraalJS. + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + if (canLoadParser(own)) { + return own; + } + + ScriptEngine cached = handler.getCachedEngine(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + return canLoadParser(cachedLoader) ? cachedLoader : null; + } + + private static boolean canLoadParser(ClassLoader loader) { + if (loader == null) { + return false; + } + try { + Class.forName(PARSER_CLASS, false, loader); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + return false; + } + } +''' +text = replace_once(text, old, new, "parser loader") +binder.write_text(text) + +pom = Path("AdvancedCore/pom.xml") +text = pom.read_text() +old = ''' + org.openjdk.nashorn + nashorn-core + 15.7 + provided + ''' +new = ''' + org.openjdk.nashorn + nashorn-core + 15.7 + compile + ''' +text = replace_once(text, old, new, "nashorn dependency") +pom.write_text(text) + +test = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java") +text = test.read_text() +insert = ''' + @Test + void exactQuotedNumericLookingPlaceholderRemainsAString() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); + + assertEquals("'001' === '001'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", + token -> token.equals("{count}") ? "5" : token, bindings::put); + + assertEquals("__advancedCorePlaceholder0 > 0", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", + token -> token.equals("%name%") ? "Ben" : token, bindings::put); + + assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } + +''' +marker = ''' + @Test + void unresolvedTokensRemainUntouched() { +''' +text = replace_once(text, marker, "\n" + insert + marker.lstrip("\n"), "test marker") +test.write_text(text) From aaf9abd961283cea08ef30cc954b5413dec38159 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:30:51 -0600 Subject: [PATCH 06/63] Run JavaScript compatibility patch --- .github/workflows/fix-js-placeholder-compat.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fix-js-placeholder-compat.yml b/.github/workflows/fix-js-placeholder-compat.yml index 7525770bf..db88705a7 100644 --- a/.github/workflows/fix-js-placeholder-compat.yml +++ b/.github/workflows/fix-js-placeholder-compat.yml @@ -12,12 +12,10 @@ jobs: - uses: actions/checkout@v4 with: ref: security/javascript-placeholder-bindings - - name: Apply fixes and tests - shell: bash + - name: Apply fixes run: | - echo 'CmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKZGVmIHJlcGxhY2Vfb25jZSh0ZXh0LCBvbGQsIG5ldywgbGFiZWwpOgogICAgY291bnQgPSB0ZXh0LmNvdW50KG9sZCkKICAgIGlmIGNvdW50ICE9IDE6CiAgICAgICAgcmFpc2UgU3lzdGVtRXhpdChmIntsYWJlbH06IGV4cGVjdGVkIDEgbWF0Y2gsIGdvdCB7Y291bnR9IikKICAgIHJldHVybiB0ZXh0LnJlcGxhY2Uob2xkLCBuZXcsIDEpCgpiYXNlID0gUGF0aCgiQWR2YW5jZWRDb3JlL3NyYy9tYWluL2phdmEvY29tL2JlbmNvZGV6L2FkdmFuY2VkY29yZS9hcGkvamF2YXNjcmlwdC9KYXZhc2NyaXB0UGxhY2Vob2xkZXJCaW5kZXIuamF2YSIpCnRleHQgPSBiYXNlLnJlYWRfdGV4dCgpCgp0ZXh0ID0gcmVwbGFjZV9vbmNlKAogICAgdGV4dCwKICAgICcgICAgcHJpdmF0ZSBzdGF0aWMgZmluYWwgUGF0dGVybiBQTEFDRUhPTERFUiA9IFBhdHRlcm4uY29tcGlsZSgiJShbXiVcXFxcc10rKSU iKTsnLnJlcGxhY2UoIiAiLCAiIiksCiAgICAnICAgIHByaXZhdGUgc3RhdGljIGZpbmFsIFBhdHRlcm4gUExBQ0VIT0xERVIgPSBQYXR0ZXJuLmNvbXBpbGUoIiUoW14lXFxcXHNdKyklfCg/PCFcXFwkKVxcXHsoW157fSVcXFxc c10rKVxcXH0iKTsnLnJlcGxhY2UoIiAiLCAiIiksCiAgICAicGxhY2Vob2xkZXIgcGF0dGVybiIsCikKCm9sZCA9ICcnJyAgICAgICAgICAgIG1hdGNoZXMuYWRkKG5ldyBQbGFjZWhvbGRlck1hdGNoKG1hdGNoZXIuc3RhcnQoKSwgbWF0Y2hlci5lbmQoKSwgdG9rZW4sIHZhbHVlKSk7CiAgICAgICAgICAgIC8vIEtlZXAgYWxsIHNvdXJjZSBvZmZzZXRzIHVuY2hhbmdlZCB3aGlsZSBtYWtpbmcgYSBiYXJlICVwbGFjZWhvbGRlciUKICAgICAgICAgICAgLy8gcGFyc2UgYXMgYW4gb3JkaW5hcnkgaWRlbnRpZmllci4KICAgICAgICAgICAgZm9yIChpbnQgaSA9IG1hdGNoZXIuc3RhcnQoKTsgaSA8IG1hdGNoZXIuZW5kKCk7IGkrKykgewogICAgICAgICAgICAgICAgc2FuaXRpemVkLnNldENoYXJBdChpLCAncCcpOwogICAgICAgICAgICB9CicnJwpuZXcgPSAnJycgICAgICAgICAgICBtYXRjaGVzLmFkZChuZXcgUGxhY2Vob2xkZXJNYXRjaChtYXRjaGVyLnN0YXJ0KCksIG1hdGNoZXIuZW5kKCksIHRva2VuLCB2YWx1ZSkpOwogICAgICAgICAgICAvLyBLZWVwIGFsbCBzb3VyY2Ugb2Zmc2V0cyB1bmNoYW5nZWQgd2hpbGUgbWFraW5nIHJlc29sdmVkIHBsYWNlaG9sZGVycyBwYXJzZQogICAgICAgICAgICAvLyBhcyBhbiBvcmRpbmFyeSBpZGVudGlmaWVyLiBVbnJlc29sdmVkIGJyYWNlIHN5bnRheCBtYXkgYmUgdmFsaWQgSmF2YVNjcmlwdAogICAgICAgICAgICAvLyAoZm9yIGV4YW1wbGUgYW4gb2JqZWN0L2Jsb2NrKSwgc28gb25seSBzYW5pdGl6ZSBicmFjZSBwbGFjZWhvbGRlcnMgd2hlbgogICAgICAgICAgICAvLyB0aGV5IGFjdHVhbGx5IHJlc29sdmUgYXMgQWR2YW5jZWRDb3JlIGN1c3RvbSBkYXRhLgogICAgICAgICAgICBib29sZWFuIGJyYWNlUGxhY2Vob2xkZXIgPSB0b2tlbi5jaGFyQXQoMCkgPT0gJ3snOwogICAgICAgICAgICBpZiAoIWJyYWNlUGxhY2Vob2xkZXIgfHwgKHZhbHVlICE9IG51bGwgJiYgIXZhbHVlLmVxdWFscyh0b2tlbikpKSB7CiAgICAgICAgICAgICAgICBmb3IgKGludCBpID0gbWF0Y2hlci5zdGFydCgpOyBpIDwgbWF0Y2hlci5lbmQoKTsgaSsrKSB7CiAgICAgICAgICAgICAgICAgICAgc2FuaXRpemVkLnNldENoYXJBdChpLCAncCcpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAic2FuaXRpemF0aW9uIikKCm9sZCA9ICcnJyAgICAgICAgICAgIGlmIChyZWdleCAhPSBudWxsKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVSZWdleChtYXRjaC52YWx1ZSwgZXhwcmVzc2lvbiwgcmVnZXgsIG1hdGNoLnN0YXJ0KTsKICAgICAgICAgICAgfSBlbHNlIGlmIChzdHJpbmcgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmVwbGFjZW1lbnRzW2ldID0gZXNjYXBlU3RyaW5nKG1hdGNoLnZhbHVlLCBleHByZXNzaW9uLmNoYXJBdChzdHJpbmcuc3RhcnQpKTsKICAgICAgICAgICAgfSBlbHNlIGlmICh0ZW1wbGF0ZSAhPSBudWxsICYmICFjb250ZXh0cy5pbnNpZGVUZW1wbGF0ZUV4cHJlc3Npb24obWF0Y2guc3RhcnQpKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgIH0gZWxzZSB7CicnJwpuZXcgPSAnJycgICAgICAgICAgICBpZiAocmVnZXggIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmVwbGFjZW1lbnRzW2ldID0gZXNjYXBlUmVnZXgobWF0Y2gudmFsdWUsIGV4cHJlc3Npb24sIHJlZ2V4LCBtYXRjaC5zdGFydCk7CiAgICAgICAgICAgIH0gZWxzZSBpZiAoc3RyaW5nICE9IG51bGwpIHsKICAgICAgICAgICAgICAgIGNoYXIgZGVsaW1pdGVyID0gbGl0ZXJhbERlbGltaXRlcihleHByZXNzaW9uLCBzdHJpbmcpOwogICAgICAgICAgICAgICAgaWYgKGRlbGltaXRlciA9PSAnYCcgJiYgIWNvbnRleHRzLmluc2lkZVRlbXBsYXRlRXhwcmVzc2lvbihtYXRjaC5zdGFydCkpIHsKICAgICAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIHJlcGxhY2VtZW50c1tpXSA9IGVzY2FwZVN0cmluZyhtYXRjaC52YWx1ZSwgZGVsaW1pdGVyKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfSBlbHNlIGlmICh0ZW1wbGF0ZSAhPSBudWxsICYmICFjb250ZXh0cy5pbnNpZGVUZW1wbGF0ZUV4cHJlc3Npb24obWF0Y2guc3RhcnQpKSB7CiAgICAgICAgICAgICAgICByZXBsYWNlbWVudHNbaV0gPSBlc2NhcGVUZW1wbGF0ZShtYXRjaC52YWx1ZSk7CiAgICAgICAgICAgIH0gZWxzZSB7CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAiY29udGV4dCBibG9jayIpCgpvbGQgPSAnJycgICAgcHJpdmF0ZSBzdGF0aWMgU3RyaW5nIHJlc29sdmUoU3RyaW5nIHRva2VuLCBPZmZsaW5lUGxheWVyIHBsYXllciwgTWFwPFN0cmluZywgU3RyaW5nPiBwbGFjZWhvbGRlcnMpIHsKICAgICAgICBBZHZhbmNlZENvcmVQbHVnaW4gcGx1Z2luID0gQWR2YW5jZWRDb3JlUGx1Z2luLmdldEluc3RhbmNlKCk7CiAgICAgICAgaWYgKHBsYXllciAhPSBudWxsICYmIHBsdWdpbiAhPSBudWxsICYmIHBsdWdpbi5pc1BsYWNlSG9sZGVyQVBJRW5hYmxlZCgpKSB7CiAgICAgICAgICAgIFN0cmluZyByZXNvbHZlZCA9IFBsYWNlaG9sZGVyQVBJLnNldFBsYWNlaG9sZGVycyhwbGF5ZXIsIHRva2VuKTsKICAgICAgICAgICAgaWYgKHJlc29sdmVkICE9IG51bGwgJiYgIXJlc29sdmVkLmVxdWFscyh0b2tlbikpIHsKICAgICAgICAgICAgICAgIHJldHVybiByZXNvbHZlZDsKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgaWYgKHBsYWNlaG9sZGVycyAhPSBudWxsKSB7CiAgICAgICAgICAgIFN0cmluZyBuYW1lID0gdG9rZW4uc3Vic3RyaW5nKDEsIHRva2VuLmxlbmd0aCgpIC0gMSk7CicnJwpuZXcgPSAnJycgICAgcHJpdmF0ZSBzdGF0aWMgU3RyaW5nIHJlc29sdmUoU3RyaW5nIHRva2VuLCBPZmZsaW5lUGxheWVyIHBsYXllciwgTWFwPFN0cmluZywgU3RyaW5nPiBwbGFjZWhvbGRlcnMpIHsKICAgICAgICBBZHZhbmNlZENvcmVQbHVnaW4gcGx1Z2luID0gQWR2YW5jZWRDb3JlUGx1Z2luLmdldEluc3RhbmNlKCk7CiAgICAgICAgLy8gUGxhY2Vob2xkZXJBUGkgdXNlcyBwZXJjZW50LWRlbGltaXRlZCBwbGFjZWhvbGRlcnMuIEJyYWNlLWRlbGltaXRlZCB0b2tlbnMKICAgICAgICAvLyBhcmUgQWR2YW5jZWRDb3JlJ3MgbGVnYWN5IGN1c3RvbSBwbGFjZWhvbGRlciBmb3JtIGFuZCBhcmUgcmVzb2x2ZWQgYmVsb3cuCiAgICAgICAgaWYgKHRva2VuLnN0YXJ0c1dpdGgoIiUiKSAmJiBwbGF5ZXIgIT0gbnVsbCAmJiBwbHVnaW4gIT0gbnVsbCAmJiBwbHVnaW4uaXNQbGFjZUhvbGRlckFQSUVuYWJsZWQoKSkgewogICAgICAgICAgICBTdHJpbmcgcmVzb2x2ZWQgPSBQbGFjZWhvbGRlckFQSS5zZXRQbGFjZWhvbGRlcnMocGxheWVyLCB0b2tlbik7CiAgICAgICAgICAgIGlmIChyZXNvbHZlZCAhPSBudWxsICYmICFyZXNvbHZlZC5lcXVhbHModG9rZW4pKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gcmVzb2x2ZWQ7CiAgICAgICAgICAgIH0KICAgICAgICB9CgogICAgICAgIGlmIChwbGFjZWhvbGRlcnMgIT0gbnVsbCkgewogICAgICAgICAgICBTdHJpbmcgbmFtZSA9IHRva2VuLnN1YnN0cmluZygxLCB0b2tlbi5sZW5ndGgoKSAtIDEpOwonJycKdGV4dCA9IHJlcGxhY2Vfb25jZSh0ZXh0LCBvbGQsIG5ldywgInJlc29sdmUgYmxvY2siKQoKbWFya2VyID0gJycnICAgIHByaXZhdGUgc3RhdGljIFN0cmluZyBlc2NhcGVTdHJpbmcoU3RyaW5nIHZhbHVlLCBjaGFyIHF1b3RlKSB7CicnJwpoZWxwZXIgPSByJycnICAgIHByaXZhdGUgc3RhdGljIGNoYXIgbGl0ZXJhbERlbGltaXRlcihTdHJpbmcgZXhwcmVzc2lvbiwgUmFuZ2UgcmFuZ2UpIHsKICAgICAgICBpbnRbXSBjYW5kaWRhdGVzID0geyByYW5nZS5zdGFydCAtIDEsIHJhbmdlLnN0YXJ0LCByYW5nZS5lbmQsIHJhbmdlLmVuZCAtIDEgfTsKICAgICAgICBmb3IgKGludCBjYW5kaWRhdGUgOiBjYW5kaWRhdGVzKSB7CiAgICAgICAgICAgIGlmIChjYW5kaWRhdGUgPCAwIHx8IGNhbmRpZGF0ZSA+PSBleHByZXNzaW9uLmxlbmd0aCgpKSB7CiAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjaGFyIHZhbHVlID0gZXhwcmVzc2lvbi5jaGFyQXQoY2FuZGlkYXRlKTsKICAgICAgICAgICAgaWYgKHZhbHVlID09ICdcJycgfHwgdmFsdWUgPT0gJyInIHx8IHZhbHVlID09ICdgJykgewogICAgICAgICAgICAgICAgcmV0dXJuIHZhbHVlOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGZvciAoaW50IGkgPSBNYXRoLm1heCgwLCByYW5nZS5zdGFydCAtIDIpOwogICAgICAgICAgICAgICAgaSA8PSBNYXRoLm1pbihleHByZXNzaW9uLmxlbmd0aCgpIC0gMSwgcmFuZ2Uuc3RhcnQgKyAxKTsgaSsrKSB7CiAgICAgICAgICAgIGNoYXIgdmFsdWUgPSBleHByZXNzaW9uLmNoYXJBdChpKTsKICAgICAgICAgICAgaWYgKHZhbHVlID09ICdcJycgfHwgdmFsdWUgPT0gJyInIHx8IHZhbHVlID09ICdgJykgewogICAgICAgICAgICAgICAgcmV0dXJuIHZhbHVlOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIHJldHVybiAnXCcnOwogICAgfQoKJycnCmlmIG1hcmtlciBub3QgaW4gdGV4dDoKICAgIHJhaXNlIFN5c3RlbUV4aXQoImVzY2FwZSBoZWxwZXIgbWFya2VyIG5vdCBmb3VuZCIpCnRleHQgPSB0ZXh0LnJlcGxhY2UobWFya2VyLCBoZWxwZXIgKyBtYXJrZXIsIDEpCgpvbGQgPSAnJycgICAgICAgIHByaXZhdGUgc3RhdGljIENsYXNzTG9hZGVyIHBhcnNlckNsYXNzTG9hZGVyKCkgewogICAgICAgICAgICBKYXZhc2NyaXB0RW5naW5lSGFuZGxlciBoYW5kbGVyID0gSmF2YXNjcmlwdEVuZ2luZUhhbmRsZXIuZ2V0SW5zdGFuY2UoKTsKICAgICAgICAgICAgaWYgKGhhbmRsZXIuZ2V0TmFzaG9ybkNsYXNzTG9hZGVyKCkgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmV0dXJuIGhhbmRsZXIuZ2V0TmFzaG9ybkNsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgU2NyaXB0RW5naW5lIGNhY2hlZCA9IGhhbmRsZXIuZ2V0Q2FjaGVkRW5naW5lKCk7CiAgICAgICAgICAgIGlmIChjYWNoZWQgIT0gbnVsbICYmIGNhY2hlZC5nZXRDbGFzcygpLmdldENsYXNzTG9hZGVyKCkgIT0gbnVsbCkgewogICAgICAgICAgICAgICAgcmV0dXJuIGNhY2hlZC5nZXRDbGFzcygpLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQ2xhc3NMb2FkZXIgb3duID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmNsYXNzLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBDbGFzcy5mb3JOYW1lKFBBUlNFUl9DTEFTUywgZmFsc2UsIG93bik7CiAgICAgICAgICAgICAgICByZXR1cm4gb3duOwogICAgICAgICAgICB9IGNhdGNoIChDbGFzc05vdEZvdW5kRXhjZXB0aW9uIGlnbm9yZWQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBudWxsOwogICAgICAgICAgICB9CiAgICAgICAgfQonJycKbmV3ID0gJycnICAgICAgICBwcml2YXRlIHN0YXRpYyBDbGFzc0xvYWRlciBwYXJzZXJDbGFzc0xvYWRlcigpIHsKICAgICAgICAgICAgSmF2YXNjcmlwdEVuZ2luZUhhbmRsZXIgaGFuZGxlciA9IEphdmFzY3JpcHRFbmdpbmVIYW5kbGVyLmdldEluc3RhbmNlKCk7CiAgICAgICAgICAgIENsYXNzTG9hZGVyIGRvd25sb2FkZWQgPSBoYW5kbGVyLmdldE5hc2hvcm5DbGFzc0xvYWRlcigpOwogICAgICAgICAgICBpZiAoY2FuTG9hZFBhcnNlcihkb3dubG9hZGVkKSkgewogICAgICAgICAgICAgICAgcmV0dXJuIGRvd25sb2FkZWQ7CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIC8vIG5hc2hvcm4tY29yZSBpcyBwYWNrYWdlZCB3aXRoIEFkdmFuY2VkQ29yZSBzbyBwYXJzZXIgc3VwcG9ydCByZW1haW5zCiAgICAgICAgICAgIC8vIGF2YWlsYWJsZSBldmVuIHdoZW4gdGhlIGFjdGl2ZSBTY3JpcHRFbmdpbmUgaXMgUmhpbm8vR3JhYWxKUy4KICAgICAgICAgICAgQ2xhc3NMb2FkZXIgb3duID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmNsYXNzLmdldENsYXNzTG9hZGVyKCk7CiAgICAgICAgICAgIGlmIChjYW5Mb2FkUGFyc2VyKG93bikpIHsKICAgICAgICAgICAgICAgIHJldHVybiBvd247CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIFNjcmlwdEVuZ2luZSBjYWNoZWQgPSBoYW5kbGVyLmdldENhY2hlZEVuZ2luZSgpOwogICAgICAgICAgICBDbGFzc0xvYWRlciBjYWNoZWRMb2FkZXIgPSBjYWNoZWQgPT0gbnVsbCA/I G51bGwgOiBjYWNoZWQuZ2V0Q2xhc3MoKS5nZXRDbGFzc0xvYWRlcigpOwogICAgICAgICAgICByZXR1cm4gY2FuTG9hZFBhcnNlcihjYWNoZWRMb2FkZXIpID8gY2FjaGVkTG9hZGVyIDogbnVsbDsKICAgICAgICB9CgogICAgICAgIHByaXZhdGUgc3RhdGljIGJvb2xlYW4gY2FuTG9hZFBhcnNlcihDbGFzc0xvYWRlciBsb2FkZXIpIHsKICAgICAgICAgICAgaWYgKGxvYWRlciA9PSBudWxsKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgICAgIENsYXNzLmZvck5hbWUoUEFSU0VSX0NMQVNTLCBmYWxzZSwgbG9hZGVyKTsKICAgICAgICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgICAgICB9IGNhdGNoIChDbGFzc05vdEZvdW5kRXhjZXB0aW9uIHwgTGlua2FnZUVycm9yIGlnbm9yZWQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBmYWxzZTsKICAgICAgICAgICAgfQogICAgICAgIH0KJycnCmlmIG9sZCBub3QgaW4gdGV4dDoKICAgIHJhaXNlIFN5c3RlbUV4aXQoInBhcnNlciBsb2FkZXIgYmxvY2sgbm90IGZvdW5kIikKdGV4dCA9IHRleHQucmVwbGFjZShvbGQsIG5ldywgMSkKYmFzZS53cml0ZV90ZXh0KHRleHQpCgpwb20gPSBQYXRoKCJBZHZhbmNlZENvcmUvcG9tLnhtbCIpCnRleHQgPSBwb20ucmVhZF90ZXh0KCkKb2xkID0gJycnICAgICAgICA8ZGVwZW5kZW5jeT4KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLm9wZW5qZGs ubmFzaG9ybjwvZ3JvdXBJZD4KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bmFzaG9ybi1jb3JlPC9hcnRpZmFjdElkPgogICAgICAgICAgICA8dmVyc2lvbj4xNS43PC92ZXJzaW9uPgogICAgICAgICAgICA8c2NvcGU+cHJvdmlkZWQ8L3Njb3BlPgogICAgICAgIDwvZGVwZW5kZW5jeT4nJycKbmV3ID0gJycnICAgICAgICA8ZGVwZW5kZW5jeT4KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLm9wZW5qZGs ubmFzaG9ybjwvZ3JvdXBJZD4KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bmFzaG9ybi1jb3JlPC9hcnRpZmFjdElkPgogICAgICAgICAgICA8dmVyc2lvbj4xNS43PC92ZXJzaW9uPgogICAgICAgICAgICA8c2NvcGU+Y29tcGlsZTwvc2NvcGU+CiAgICAgICAgPC9kZXBlbmRlbmN5PicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG9sZCwgbmV3LCAibmFzaG9ybiBkZXBlbmRlbmN5IikKcG9tLndyaXRlX3RleHQodGV4dCkKCnRlc3QgPSBQYXRoKCJBZHZhbmNlZENvcmUvc3JjL3Rlc3QvamF2YS9jb20vYmVuY29kZXovYWR2YW5jZWRjb3JlL2FwaS9qYXZhc2NyaXB0L0phdmFzY3JpcHRQbGFjZWhvbGRlckJpbmRlclRlc3QuamF2YSIpCnRleHQgPSB0ZXN0LnJlYWRfdGV4dCgpCmluc2VydCA9IHInJycKICAgIEBUZXN0CiAgICB2b2lkIGV4YWN0UXVvdGVkTnVtZXJpY0xvb2tpbmdQbGFjZWhvbGRlclJlbWFpbnNBU3RyaW5nKCkgewogICAgICAgIEhhc2hNYXA8U3RyaW5nLCBPYmplY3Q+IGJpbmRpbmdzID0gbmV3IEhhc2hNYXAoKTsgCiAgICAgICAgU3RyaW5nIHByZXBhcmVkID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmJpbmQoIiclY29kZSU nID09PSAnMDAxJyIsIGlnbm9yZWQgLT4gIjAwMSIsIGJpbmRpbmdzOjpwdXQpOwogICAgICAgIGFzc2VydEVxdWFscygiJzAwMScgPT09ICcwMDEnIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydFRydWUoYmluZGluZ3MuaXNFbXB0eSgpKTsKICAgIH0KCiAgICBAVGVzdAogICAgdm9pZCBicmFjZURlbGltaXRlZEN1c3RvbVBsYWNlaG9sZGVySXNBdXRvbWF0aWNhbGx5Qm91bmQoKSB7CiAgICAgICAgSGFzaE1hcDxTdHJpbmcsIE9iamVjdD4gYmluZGluZ3MgPSBuZXcgSGFzaE1hcDw+KCk7CiAgICAgICAgU3RyaW5nIHByZXBhcmVkID0gSmF2YXNjcmlwdFBsYWNlaG9sZGVyQmluZGVyLmJpbmQoIntjb3VudH0gPiAwIiwKICAgICAgICAgICAgICAgIHRva2VuIC0+IHRva2VuLmVxdWFscygie2NvdW50fSIpID8gIjUiIDogdG9rZW4sIGJpbmRpbmdzOjpwdXQpOwogICAgICAgIGFzc2VydEVxdWFscygiX19hZHZhbmNlZENvcmVQbGFjZWhvbGRlcjAgPiAwIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydEVxdWFscyhMb25nLnZhbHVlT2YoNSksIGJpbmRpbmdzLmdldCgiX19hZHZhbmNlZENvcmVQbGFjZWhvbGRlcjAiKSk7CiAgICB9CgogICAgQFRlc3QKICAgIHZvaWQgdW5yZXNvbHZlZEJyYWNlU3ludGF4UmVtYWluc09yZGluYXJ5SmF2YXNjcmlwdCgpIHsKICAgICAgICBIYXNoTWFwPFN0cmluZywgT2JqZWN0PiBiaW5kaW5ncyA9IG5ldyBIYXNoTWFwPD4oKTsKICAgICAgICBTdHJpbmcgcHJlcGFyZWQgPSBKYXZhc2NyaXB0UGxhY2Vob2xkZXJCaW5kZXIuYmluZCgidmFyIHZhbHVlID0ge2NvdW50OiAxfTsgJW5hbWUlIiwKICAgICAgICAgICAgICAgIHRva2VuIC0+IHRva2VuLmVxdWFscygiJW5hbWUlIikgPyAiQmVuIiA6IHRva2VuLCBiaW5kaW5nczo6cHV0KTsKICAgICAgICBhc3NlcnRFcXVhbHMoInZhciB2YWx1ZSA9IHtjb3VudDogMX07IF9fYWR2YW5jZWRDb3JlUGxhY2Vob2xkZXIwIiwgcHJlcGFyZWQpOwogICAgICAgIGFzc2VydEVxdWFscygiQmVuIiwgYmluZGluZ3MuZ2V0KCJfX2FkdmFuY2VkQ29yZVBsYWNlaG9sZGVyMCIpKTsKICAgIH0KCicnJwptYXJrZXIgPSAnJycKICAgIEBUZXN0CiAgICB2b2lkIHVucmVzb2x2ZWRUb2tlbnNSZW1haW5VbnRvdWNoZWQoKSB7CicnJwp0ZXh0ID0gcmVwbGFjZV9vbmNlKHRleHQsIG1hcmtlciwgIlxuIiArIGluc2VydCArIG1hcmtlci5sc3RyaXAoIlxuIiksICJ0ZXN0IG1hcmtlciIpCnRlc3Qud3JpdGVfdGV4dCh0ZXh0KQo=' | base64 -d > /tmp/fix.py - python3 /tmp/fix.py - rm -f .github/workflows/fix-js-placeholder-compat.yml + python3 .github/fix_js_placeholder_compat.py + rm -f .github/fix_js_placeholder_compat.py .github/workflows/fix-js-placeholder-compat.yml git config user.name 'Ben' git config user.email 'benbergen12@gmail.com' git add -A From c3e859781b373924e68fd9ef6174f69c99f075cb Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 01:30:58 +0000 Subject: [PATCH 07/63] Fix automatic JavaScript placeholder compatibility --- .github/fix_js_placeholder_compat.py | 229 ------------------ .../workflows/fix-js-placeholder-compat.yml | 23 -- AdvancedCore/pom.xml | 2 +- .../JavascriptPlaceholderBinder.java | 78 ++++-- .../JavascriptPlaceholderBinderTest.java | 33 +++ 5 files changed, 96 insertions(+), 269 deletions(-) delete mode 100644 .github/fix_js_placeholder_compat.py delete mode 100644 .github/workflows/fix-js-placeholder-compat.yml diff --git a/.github/fix_js_placeholder_compat.py b/.github/fix_js_placeholder_compat.py deleted file mode 100644 index b90ce1f74..000000000 --- a/.github/fix_js_placeholder_compat.py +++ /dev/null @@ -1,229 +0,0 @@ -from pathlib import Path - - -def replace_once(text, old, new, label): - count = text.count(old) - if count != 1: - raise SystemExit(f"{label}: expected 1 match, got {count}") - return text.replace(old, new, 1) - - -binder = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") -text = binder.read_text() - -text = replace_once( - text, - ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%");', - ' private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\\\s]+)%|(? placeholders) { - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } - - if (placeholders != null) { - String name = token.substring(1, token.length() - 1); -''' -new = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens - // are AdvancedCore's legacy custom placeholder form and are resolved below. - if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } - - if (placeholders != null) { - String name = token.substring(1, token.length() - 1); -''' -text = replace_once(text, old, new, "resolve block") - -marker = ''' private static String escapeString(String value, char quote) { -''' -helper = ''' private static char literalDelimiter(String expression, Range range) { - int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; - for (int candidate : candidates) { - if (candidate < 0 || candidate >= expression.length()) { - continue; - } - char value = expression.charAt(candidate); - if (value == '\\'' || value == '"' || value == '`') { - return value; - } - } - for (int i = Math.max(0, range.start - 2); - i <= Math.min(expression.length() - 1, range.start + 1); i++) { - char value = expression.charAt(i); - if (value == '\\'' || value == '"' || value == '`') { - return value; - } - } - return '\\''; - } - -''' -text = replace_once(text, marker, helper + marker, "escape helper marker") - -old = ''' private static ClassLoader parserClassLoader() { - JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); - if (handler.getNashornClassLoader() != null) { - return handler.getNashornClassLoader(); - } - ScriptEngine cached = handler.getCachedEngine(); - if (cached != null && cached.getClass().getClassLoader() != null) { - return cached.getClass().getClassLoader(); - } - ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); - try { - Class.forName(PARSER_CLASS, false, own); - return own; - } catch (ClassNotFoundException ignored) { - return null; - } - } -''' -new = ''' private static ClassLoader parserClassLoader() { - JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); - ClassLoader downloaded = handler.getNashornClassLoader(); - if (canLoadParser(downloaded)) { - return downloaded; - } - - // nashorn-core is packaged with AdvancedCore so parser support remains - // available even when the active ScriptEngine is Rhino/GraalJS. - ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); - if (canLoadParser(own)) { - return own; - } - - ScriptEngine cached = handler.getCachedEngine(); - ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); - return canLoadParser(cachedLoader) ? cachedLoader : null; - } - - private static boolean canLoadParser(ClassLoader loader) { - if (loader == null) { - return false; - } - try { - Class.forName(PARSER_CLASS, false, loader); - return true; - } catch (ClassNotFoundException | LinkageError ignored) { - return false; - } - } -''' -text = replace_once(text, old, new, "parser loader") -binder.write_text(text) - -pom = Path("AdvancedCore/pom.xml") -text = pom.read_text() -old = ''' - org.openjdk.nashorn - nashorn-core - 15.7 - provided - ''' -new = ''' - org.openjdk.nashorn - nashorn-core - 15.7 - compile - ''' -text = replace_once(text, old, new, "nashorn dependency") -pom.write_text(text) - -test = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java") -text = test.read_text() -insert = ''' - @Test - void exactQuotedNumericLookingPlaceholderRemainsAString() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); - - assertEquals("'001' === '001'", prepared); - assertTrue(bindings.isEmpty()); - } - - @Test - void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", - token -> token.equals("{count}") ? "5" : token, bindings::put); - - assertEquals("__advancedCorePlaceholder0 > 0", prepared); - assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); - } - - @Test - void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", - token -> token.equals("%name%") ? "Ben" : token, bindings::put); - - assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); - assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); - } - -''' -marker = ''' - @Test - void unresolvedTokensRemainUntouched() { -''' -text = replace_once(text, marker, "\n" + insert + marker.lstrip("\n"), "test marker") -test.write_text(text) diff --git a/.github/workflows/fix-js-placeholder-compat.yml b/.github/workflows/fix-js-placeholder-compat.yml deleted file mode 100644 index db88705a7..000000000 --- a/.github/workflows/fix-js-placeholder-compat.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Apply JavaScript placeholder compatibility fixes -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply fixes - run: | - python3 .github/fix_js_placeholder_compat.py - rm -f .github/fix_js_placeholder_compat.py .github/workflows/fix-js-placeholder-compat.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Fix automatic JavaScript placeholder compatibility' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index ae2ed4bc6..265dfeea9 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -235,7 +235,7 @@ org.openjdk.nashorn nashorn-core 15.7 - provided + compile org.slf4j diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index ef58cbc34..6713511c1 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -35,7 +35,7 @@ * second JavaScript lexer inside AdvancedCore. */ public final class JavascriptPlaceholderBinder { - private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\s]+)%"); + private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\s]+)%|(? resolver, BiConsu value = resolver.apply(token); } matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); - // Keep all source offsets unchanged while making a bare %placeholder% - // parse as an ordinary identifier. - for (int i = matcher.start(); i < matcher.end(); i++) { - sanitized.setCharAt(i, 'p'); + // Keep all source offsets unchanged while making resolved placeholders parse + // as an ordinary identifier. Unresolved brace syntax may be valid JavaScript + // (for example an object/block), so only sanitize brace placeholders when + // they actually resolve as AdvancedCore custom data. + boolean bracePlaceholder = token.charAt(0) == '{'; + if (!bracePlaceholder || (value != null && !value.equals(token))) { + for (int i = matcher.start(); i < matcher.end(); i++) { + sanitized.setCharAt(i, 'p'); + } } } if (matches.isEmpty()) { @@ -94,7 +99,12 @@ static String bind(String expression, Function resolver, BiConsu if (regex != null) { replacements[i] = escapeRegex(match.value, expression, regex, match.start); } else if (string != null) { - replacements[i] = escapeString(match.value, expression.charAt(string.start)); + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } } else if (template != null && !contexts.insideTemplateExpression(match.start)) { replacements[i] = escapeTemplate(match.value); } else { @@ -115,7 +125,9 @@ static String bind(String expression, Function resolver, BiConsu private static String resolve(String token, OfflinePlayer player, Map placeholders) { AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - if (player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens + // are AdvancedCore's legacy custom placeholder form and are resolved below. + if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { String resolved = PlaceholderAPI.setPlaceholders(player, token); if (resolved != null && !resolved.equals(token)) { return resolved; @@ -152,6 +164,27 @@ private static Object coerce(String value) { return value; } + private static char literalDelimiter(String expression, Range range) { + int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; + for (int candidate : candidates) { + if (candidate < 0 || candidate >= expression.length()) { + continue; + } + char value = expression.charAt(candidate); + if (value == '\'' || value == '"' || value == '`') { + return value; + } + } + for (int i = Math.max(0, range.start - 2); + i <= Math.min(expression.length() - 1, range.start + 1); i++) { + char value = expression.charAt(i); + if (value == '\'' || value == '"' || value == '`') { + return value; + } + } + return '\''; + } + private static String escapeString(String value, char quote) { StringBuilder result = new StringBuilder(value.length()); for (int i = 0; i < value.length(); i++) { @@ -313,19 +346,32 @@ private static Object createParser(Class parserClass) throws ReflectiveOperat private static ClassLoader parserClassLoader() { JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); - if (handler.getNashornClassLoader() != null) { - return handler.getNashornClassLoader(); + ClassLoader downloaded = handler.getNashornClassLoader(); + if (canLoadParser(downloaded)) { + return downloaded; } + + // nashorn-core is packaged with AdvancedCore so parser support remains + // available even when the active ScriptEngine is Rhino/GraalJS. + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + if (canLoadParser(own)) { + return own; + } + ScriptEngine cached = handler.getCachedEngine(); - if (cached != null && cached.getClass().getClassLoader() != null) { - return cached.getClass().getClassLoader(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + return canLoadParser(cachedLoader) ? cachedLoader : null; + } + + private static boolean canLoadParser(ClassLoader loader) { + if (loader == null) { + return false; } - ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); try { - Class.forName(PARSER_CLASS, false, own); - return own; - } catch (ClassNotFoundException ignored) { - return null; + Class.forName(PARSER_CLASS, false, loader); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + return false; } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java index 79a70ed70..744f30258 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -137,6 +137,39 @@ void preservesPrimitiveTypesForExpressionPlaceholders() { assertEquals(Double.valueOf(1.5), bindings.get("__advancedCorePlaceholder2")); } + + @Test + void exactQuotedNumericLookingPlaceholderRemainsAString() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); + + assertEquals("'001' === '001'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", + token -> token.equals("{count}") ? "5" : token, bindings::put); + + assertEquals("__advancedCorePlaceholder0 > 0", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", + token -> token.equals("%name%") ? "Ben" : token, bindings::put); + + assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } + @Test void unresolvedTokensRemainUntouched() { HashMap bindings = new HashMap<>(); From c8ab21d78db289be08053ba12dddf466a5a5297c Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:31:51 -0600 Subject: [PATCH 08/63] Test packaged JavaScript parser availability --- ...vascriptPlaceholderRuntimeAvailabilityTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java new file mode 100644 index 000000000..c4b8ba6d4 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java @@ -0,0 +1,14 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderRuntimeAvailabilityTest { + + @Test + void nashornParserIsAvailableToAdvancedCore() { + assertDoesNotThrow(() -> Class.forName("org.openjdk.nashorn.api.tree.Parser", false, + JavascriptPlaceholderBinder.class.getClassLoader())); + } +} From 68c6487d993759f9b735cf293c0dae1c8b992bcb Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:46:37 -0600 Subject: [PATCH 09/63] Prepare Codex parser fallback fixes --- .github/fix_302_codex_round.py | 277 +++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 .github/fix_302_codex_round.py diff --git a/.github/fix_302_codex_round.py b/.github/fix_302_codex_round.py new file mode 100644 index 000000000..c92eaf544 --- /dev/null +++ b/.github/fix_302_codex_round.py @@ -0,0 +1,277 @@ +from pathlib import Path + + +def replace_once(text, old, new, label): + count = text.count(old) + if count != 1: + raise SystemExit(f"{label}: expected 1 match, got {count}") + return text.replace(old, new, 1) + +binder = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") +text = binder.read_text() + +text = replace_once(text, +''' private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; +''', +''' private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; + private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\\\"(?:\\\\.|[^\\\"\\\\])*\\\""); + private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); + private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|\\[(?:\\\\.|[^\\]\\\\])*\\]|[^/\\\\\\r\\n])+/[dgimsuvy]*"); +''', "fallback patterns") + +text = replace_once(text, +''' JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); + String[] replacements = new String[matches.size()]; +''', +''' JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); + if (!contexts.parsed) { + contexts = JavascriptContexts.fallback(expression); + } + String[] replacements = new String[matches.size()]; +''', "fallback selection") + +text = replace_once(text, +''' private final List strings = new ArrayList<>(); + private final List regexes = new ArrayList<>(); + private final List templates = new ArrayList<>(); + private final List templateExpressions = new ArrayList<>(); +''', +''' private final List strings = new ArrayList<>(); + private final List regexes = new ArrayList<>(); + private final List templates = new ArrayList<>(); + private final List templateExpressions = new ArrayList<>(); + private boolean parsed; +''', "parsed flag") + +text = replace_once(text, +''' Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null) { + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''', +''' Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''', "parse success") + +marker = ''' private static Object createParser(Class parserClass) throws ReflectiveOperationException { +''' +fallback = r''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); + contexts.sort(); + return contexts; + } + + private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + Range candidate = new Range(matcher.start(), matcher.end()); + if (existing == null || !existing.overlapsLiteral(candidate)) { + target.add(candidate); + } + } + } + + private static void addFallbackTemplateExpressions(String source, Range template, List target) { + boolean escaped = false; + int expressionStart = -1; + int depth = 0; + for (int i = template.start + 1; i < template.end - 1; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (expressionStart < 0) { + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + expressionStart = i + 2; + depth = 1; + i++; + } + continue; + } + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + target.add(new Range(expressionStart, i)); + expressionStart = -1; + } + } + } + } + + private boolean overlapsLiteral(Range candidate) { + return overlaps(strings, candidate) || overlaps(templates, candidate); + } + + private boolean overlaps(List ranges, Range candidate) { + for (Range range : ranges) { + if (candidate.start < range.end && range.start < candidate.end) { + return true; + } + } + return false; + } + +''' +if marker not in text: + raise SystemExit("createParser marker missing") +text = text.replace(marker, fallback + marker, 1) + +old_loader = ''' ScriptEngine cached = handler.getCachedEngine(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + return canLoadParser(cachedLoader) ? cachedLoader : null; +''' +new_loader = ''' ScriptEngine cached = handler.getCachedEngine(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + if (canLoadParser(cachedLoader)) { + return cachedLoader; + } + + ClassLoader prepared = handler.getOrCreateNashornParserClassLoader(); + return canLoadParser(prepared) ? prepared : null; +''' +text = replace_once(text, old_loader, new_loader, "parser loader fallback") +binder.write_text(text) + +handler = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java") +text = handler.read_text() +marker = '''\t/** +\t * Gets a downloaded Nashorn engine. +''' +method = r''' /** + * Gets or prepares a Nashorn classloader for parser-only use. This is separate + * from the active ScriptEngine so a server-provided Rhino/GraalJS engine can + * still use Nashorn's parser for safe placeholder context detection. + * + * @return a classloader containing Nashorn's parser API, or null if unavailable + */ + public ClassLoader getOrCreateNashornParserClassLoader() { + if (nashornClassLoader != null) { + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, nashornClassLoader); + return nashornClassLoader; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + if (plugin == null) { + return null; + } + + URLClassLoader loader = createParserClassLoader(PRIMARY_NASHORN_VERSION, ASM_VERSION_FOR_PRIMARY); + if (loader == null) { + loader = createParserClassLoader(FALLBACK_NASHORN_VERSION, ASM_VERSION_FOR_FALLBACK); + } + if (loader != null) { + nashornClassLoader = loader; + } + return loader; + } + + private URLClassLoader createParserClassLoader(String nashornVersion, String asmVersion) { + try { + List jars = getOrDownloadJars(nashornVersion, asmVersion); + if (jars.isEmpty()) { + return null; + } + URLClassLoader loader = createClassLoader(jars); + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, loader); + return loader; + } catch (ClassNotFoundException | LinkageError e) { + closeQuietly(loader); + return null; + } + } catch (IOException e) { + logDebug(e); + return null; + } + } + +''' +if marker not in text: + raise SystemExit("handler insertion marker missing") +text = text.replace(marker, method + marker, 1) +handler.write_text(text) + +pom = Path("AdvancedCore/pom.xml") +text = pom.read_text() +text = replace_once(text, +''' + org.openjdk.nashorn + nashorn-core + 15.7 + compile + +''', +''' + org.openjdk.nashorn + nashorn-core + 15.7 + provided + +''', "nashorn scope") +pom.write_text(text) + +test = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java") +text = test.read_text() +marker = ''' @Test + void unresolvedTokensRemainUntouched() { +''' +tests = r''' @Test + void parserFailurePreservesQuotedPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", + ignored -> "Ben", bindings::put); + + assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesTemplateTextUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", + ignored -> "Ben` ${attack}", bindings::put); + + assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesRegexPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + +''' +if marker not in text: + raise SystemExit("test marker missing") +text = text.replace(marker, tests + marker, 1) +test.write_text(text) From ab15043685c215b51fd5920bba18867be9b6094a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:46:47 -0600 Subject: [PATCH 10/63] Run Codex parser fallback fixes --- .github/workflows/run-fix-302-codex-round.yml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/run-fix-302-codex-round.yml diff --git a/.github/workflows/run-fix-302-codex-round.yml b/.github/workflows/run-fix-302-codex-round.yml new file mode 100644 index 000000000..c5ab55b5c --- /dev/null +++ b/.github/workflows/run-fix-302-codex-round.yml @@ -0,0 +1,24 @@ +name: Apply Codex parser fallback fixes +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply fixes + shell: bash + run: | + python3 .github/fix_302_codex_round.py + rm -f .github/fix_302_codex_round.py .github/workflows/run-fix-302-codex-round.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Harden parser fallback compatibility' + git push origin HEAD:security/javascript-placeholder-bindings From 9b45588ef69a0aed12e5a048b1528be2f611d5f0 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 01:46:55 +0000 Subject: [PATCH 11/63] Harden parser fallback compatibility --- .github/fix_302_codex_round.py | 277 ------------------ .github/workflows/run-fix-302-codex-round.yml | 24 -- AdvancedCore/pom.xml | 2 +- .../javascript/JavascriptEngineHandler.java | 49 ++++ .../JavascriptPlaceholderBinder.java | 89 +++++- .../JavascriptPlaceholderBinderTest.java | 33 +++ 6 files changed, 171 insertions(+), 303 deletions(-) delete mode 100644 .github/fix_302_codex_round.py delete mode 100644 .github/workflows/run-fix-302-codex-round.yml diff --git a/.github/fix_302_codex_round.py b/.github/fix_302_codex_round.py deleted file mode 100644 index c92eaf544..000000000 --- a/.github/fix_302_codex_round.py +++ /dev/null @@ -1,277 +0,0 @@ -from pathlib import Path - - -def replace_once(text, old, new, label): - count = text.count(old) - if count != 1: - raise SystemExit(f"{label}: expected 1 match, got {count}") - return text.replace(old, new, 1) - -binder = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") -text = binder.read_text() - -text = replace_once(text, -''' private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; -''', -''' private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; - private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\\\"(?:\\\\.|[^\\\"\\\\])*\\\""); - private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); - private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|\\[(?:\\\\.|[^\\]\\\\])*\\]|[^/\\\\\\r\\n])+/[dgimsuvy]*"); -''', "fallback patterns") - -text = replace_once(text, -''' JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); - String[] replacements = new String[matches.size()]; -''', -''' JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); - if (!contexts.parsed) { - contexts = JavascriptContexts.fallback(expression); - } - String[] replacements = new String[matches.size()]; -''', "fallback selection") - -text = replace_once(text, -''' private final List strings = new ArrayList<>(); - private final List regexes = new ArrayList<>(); - private final List templates = new ArrayList<>(); - private final List templateExpressions = new ArrayList<>(); -''', -''' private final List strings = new ArrayList<>(); - private final List regexes = new ArrayList<>(); - private final List templates = new ArrayList<>(); - private final List templateExpressions = new ArrayList<>(); - private boolean parsed; -''', "parsed flag") - -text = replace_once(text, -''' Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null) { - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''', -''' Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null) { - contexts.parsed = true; - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''', "parse success") - -marker = ''' private static Object createParser(Class parserClass) throws ReflectiveOperationException { -''' -fallback = r''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); - contexts.sort(); - return contexts; - } - - private static void addPatternRanges(String source, Pattern pattern, List target, - JavascriptContexts existing) { - Matcher matcher = pattern.matcher(source); - while (matcher.find()) { - Range candidate = new Range(matcher.start(), matcher.end()); - if (existing == null || !existing.overlapsLiteral(candidate)) { - target.add(candidate); - } - } - } - - private static void addFallbackTemplateExpressions(String source, Range template, List target) { - boolean escaped = false; - int expressionStart = -1; - int depth = 0; - for (int i = template.start + 1; i < template.end - 1; i++) { - char current = source.charAt(i); - if (escaped) { - escaped = false; - continue; - } - if (current == '\\') { - escaped = true; - continue; - } - if (expressionStart < 0) { - if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { - expressionStart = i + 2; - depth = 1; - i++; - } - continue; - } - if (current == '{') { - depth++; - } else if (current == '}') { - depth--; - if (depth == 0) { - target.add(new Range(expressionStart, i)); - expressionStart = -1; - } - } - } - } - - private boolean overlapsLiteral(Range candidate) { - return overlaps(strings, candidate) || overlaps(templates, candidate); - } - - private boolean overlaps(List ranges, Range candidate) { - for (Range range : ranges) { - if (candidate.start < range.end && range.start < candidate.end) { - return true; - } - } - return false; - } - -''' -if marker not in text: - raise SystemExit("createParser marker missing") -text = text.replace(marker, fallback + marker, 1) - -old_loader = ''' ScriptEngine cached = handler.getCachedEngine(); - ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); - return canLoadParser(cachedLoader) ? cachedLoader : null; -''' -new_loader = ''' ScriptEngine cached = handler.getCachedEngine(); - ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); - if (canLoadParser(cachedLoader)) { - return cachedLoader; - } - - ClassLoader prepared = handler.getOrCreateNashornParserClassLoader(); - return canLoadParser(prepared) ? prepared : null; -''' -text = replace_once(text, old_loader, new_loader, "parser loader fallback") -binder.write_text(text) - -handler = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java") -text = handler.read_text() -marker = '''\t/** -\t * Gets a downloaded Nashorn engine. -''' -method = r''' /** - * Gets or prepares a Nashorn classloader for parser-only use. This is separate - * from the active ScriptEngine so a server-provided Rhino/GraalJS engine can - * still use Nashorn's parser for safe placeholder context detection. - * - * @return a classloader containing Nashorn's parser API, or null if unavailable - */ - public ClassLoader getOrCreateNashornParserClassLoader() { - if (nashornClassLoader != null) { - try { - Class.forName("org.openjdk.nashorn.api.tree.Parser", false, nashornClassLoader); - return nashornClassLoader; - } catch (ClassNotFoundException | LinkageError ignored) { - } - } - if (plugin == null) { - return null; - } - - URLClassLoader loader = createParserClassLoader(PRIMARY_NASHORN_VERSION, ASM_VERSION_FOR_PRIMARY); - if (loader == null) { - loader = createParserClassLoader(FALLBACK_NASHORN_VERSION, ASM_VERSION_FOR_FALLBACK); - } - if (loader != null) { - nashornClassLoader = loader; - } - return loader; - } - - private URLClassLoader createParserClassLoader(String nashornVersion, String asmVersion) { - try { - List jars = getOrDownloadJars(nashornVersion, asmVersion); - if (jars.isEmpty()) { - return null; - } - URLClassLoader loader = createClassLoader(jars); - try { - Class.forName("org.openjdk.nashorn.api.tree.Parser", false, loader); - return loader; - } catch (ClassNotFoundException | LinkageError e) { - closeQuietly(loader); - return null; - } - } catch (IOException e) { - logDebug(e); - return null; - } - } - -''' -if marker not in text: - raise SystemExit("handler insertion marker missing") -text = text.replace(marker, method + marker, 1) -handler.write_text(text) - -pom = Path("AdvancedCore/pom.xml") -text = pom.read_text() -text = replace_once(text, -''' - org.openjdk.nashorn - nashorn-core - 15.7 - compile - -''', -''' - org.openjdk.nashorn - nashorn-core - 15.7 - provided - -''', "nashorn scope") -pom.write_text(text) - -test = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java") -text = test.read_text() -marker = ''' @Test - void unresolvedTokensRemainUntouched() { -''' -tests = r''' @Test - void parserFailurePreservesQuotedPlaceholderUnderModernSyntax() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", - ignored -> "Ben", bindings::put); - - assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); - assertTrue(bindings.isEmpty()); - } - - @Test - void parserFailurePreservesTemplateTextUnderModernSyntax() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", - ignored -> "Ben` ${attack}", bindings::put); - - assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); - assertTrue(bindings.isEmpty()); - } - - @Test - void parserFailurePreservesRegexPlaceholderUnderModernSyntax() { - HashMap bindings = new HashMap<>(); - - String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", - ignored -> "Ben.*", bindings::put); - - assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); - assertTrue(bindings.isEmpty()); - } - -''' -if marker not in text: - raise SystemExit("test marker missing") -text = text.replace(marker, tests + marker, 1) -test.write_text(text) diff --git a/.github/workflows/run-fix-302-codex-round.yml b/.github/workflows/run-fix-302-codex-round.yml deleted file mode 100644 index c5ab55b5c..000000000 --- a/.github/workflows/run-fix-302-codex-round.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Apply Codex parser fallback fixes -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply fixes - shell: bash - run: | - python3 .github/fix_302_codex_round.py - rm -f .github/fix_302_codex_round.py .github/workflows/run-fix-302-codex-round.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Harden parser fallback compatibility' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index 265dfeea9..ae2ed4bc6 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -235,7 +235,7 @@ org.openjdk.nashorn nashorn-core 15.7 - compile + provided org.slf4j diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java index 5a1e709d0..0a75b059d 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java @@ -274,6 +274,55 @@ private ScriptEngine getServerProvidedEngine() { } } + /** + * Gets or prepares a Nashorn classloader for parser-only use. This is separate + * from the active ScriptEngine so a server-provided Rhino/GraalJS engine can + * still use Nashorn's parser for safe placeholder context detection. + * + * @return a classloader containing Nashorn's parser API, or null if unavailable + */ + public ClassLoader getOrCreateNashornParserClassLoader() { + if (nashornClassLoader != null) { + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, nashornClassLoader); + return nashornClassLoader; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + if (plugin == null) { + return null; + } + + URLClassLoader loader = createParserClassLoader(PRIMARY_NASHORN_VERSION, ASM_VERSION_FOR_PRIMARY); + if (loader == null) { + loader = createParserClassLoader(FALLBACK_NASHORN_VERSION, ASM_VERSION_FOR_FALLBACK); + } + if (loader != null) { + nashornClassLoader = loader; + } + return loader; + } + + private URLClassLoader createParserClassLoader(String nashornVersion, String asmVersion) { + try { + List jars = getOrDownloadJars(nashornVersion, asmVersion); + if (jars.isEmpty()) { + return null; + } + URLClassLoader loader = createClassLoader(jars); + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, loader); + return loader; + } catch (ClassNotFoundException | LinkageError e) { + closeQuietly(loader); + return null; + } + } catch (IOException e) { + logDebug(e); + return null; + } + } + /** * Gets a downloaded Nashorn engine. * diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 6713511c1..445e714b5 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -44,6 +44,9 @@ public final class JavascriptPlaceholderBinder { private static final String DIAGNOSTIC_LISTENER_CLASS = "org.openjdk.nashorn.api.tree.DiagnosticListener"; private static final String TREE_CLASS = "org.openjdk.nashorn.api.tree.Tree"; private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; + private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\""); + private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\.|[^`\\])*`"); + private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\r\n])+/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { } @@ -84,6 +87,9 @@ static String bind(String expression, Function resolver, BiConsu } JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); + if (!contexts.parsed) { + contexts = JavascriptContexts.fallback(expression); + } String[] replacements = new String[matches.size()]; int bindingIndex = 0; for (int i = 0; i < matches.size(); i++) { @@ -300,6 +306,7 @@ private static final class JavascriptContexts { private final List regexes = new ArrayList<>(); private final List templates = new ArrayList<>(); private final List templateExpressions = new ArrayList<>(); + private boolean parsed; private static JavascriptContexts parse(String source) { JavascriptContexts contexts = new JavascriptContexts(); @@ -318,6 +325,7 @@ private static JavascriptContexts parse(String source) { Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); if (root != null) { + contexts.parsed = true; walk(root, treeClass, contexts, new IdentityHashMap<>()); } } catch (ReflectiveOperationException | RuntimeException ignored) { @@ -328,6 +336,80 @@ private static JavascriptContexts parse(String source) { return contexts; } + private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); + contexts.sort(); + return contexts; + } + + private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + Range candidate = new Range(matcher.start(), matcher.end()); + if (existing == null || !existing.overlapsLiteral(candidate)) { + target.add(candidate); + } + } + } + + private static void addFallbackTemplateExpressions(String source, Range template, List target) { + boolean escaped = false; + int expressionStart = -1; + int depth = 0; + for (int i = template.start + 1; i < template.end - 1; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (expressionStart < 0) { + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + expressionStart = i + 2; + depth = 1; + i++; + } + continue; + } + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + target.add(new Range(expressionStart, i)); + expressionStart = -1; + } + } + } + } + + private boolean overlapsLiteral(Range candidate) { + return overlaps(strings, candidate) || overlaps(templates, candidate); + } + + private boolean overlaps(List ranges, Range candidate) { + for (Range range : ranges) { + if (candidate.start < range.end && range.start < candidate.end) { + return true; + } + } + return false; + } + private static Object createParser(Class parserClass) throws ReflectiveOperationException { for (Method method : parserClass.getMethods()) { if (!method.getName().equals("create") || !Modifier.isStatic(method.getModifiers())) { @@ -360,7 +442,12 @@ private static ClassLoader parserClassLoader() { ScriptEngine cached = handler.getCachedEngine(); ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); - return canLoadParser(cachedLoader) ? cachedLoader : null; + if (canLoadParser(cachedLoader)) { + return cachedLoader; + } + + ClassLoader prepared = handler.getOrCreateNashornParserClassLoader(); + return canLoadParser(prepared) ? prepared : null; } private static boolean canLoadParser(ClassLoader loader) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java index 744f30258..8cd0b4892 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -170,6 +170,39 @@ void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); } + @Test + void parserFailurePreservesQuotedPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", + ignored -> "Ben", bindings::put); + + assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesTemplateTextUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", + ignored -> "Ben` ${attack}", bindings::put); + + assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesRegexPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + @Test void unresolvedTokensRemainUntouched() { HashMap bindings = new HashMap<>(); From 4e8204c036af2a135995224319c18916849b1ab7 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:47:32 -0600 Subject: [PATCH 12/63] Test modern JavaScript placeholder fallback --- ...ptPlaceholderModernSyntaxFallbackTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java new file mode 100644 index 000000000..c981c1e35 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -0,0 +1,29 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderModernSyntaxFallbackTest { + + @BeforeEach + void resetJavascriptHandler() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + + @Test + void newerEngineSyntaxStillPreservesQuotedPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", + ignored -> "Ben", bindings::put); + + assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } +} From c453979c9b7a733bdc27814cacf48028fc1272d8 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:50:01 -0600 Subject: [PATCH 13/63] Prepare fallback regex compile fix --- .github/workflows/fix-302-fallback-regex.yml | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/fix-302-fallback-regex.yml diff --git a/.github/workflows/fix-302-fallback-regex.yml b/.github/workflows/fix-302-fallback-regex.yml new file mode 100644 index 000000000..228978f6b --- /dev/null +++ b/.github/workflows/fix-302-fallback-regex.yml @@ -0,0 +1,38 @@ +name: Fix JavaScript fallback regex +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Fix fallback regex + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = p.read_text() + lines = text.splitlines() + found = False + for i, line in enumerate(lines): + if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): + lines[i] = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\r\\n])+/[dgimsuvy]*");' + found = True + break + if not found: + raise SystemExit('FALLBACK_REGEX line not found') + p.write_text('\n'.join(lines) + '\n') + PY + rm -f .github/workflows/fix-302-fallback-regex.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Fix JavaScript fallback regex compilation' + git push origin HEAD:security/javascript-placeholder-bindings From ca796b0d0bc6d8bac8bf96293805f256d7ec4f93 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 01:50:09 +0000 Subject: [PATCH 14/63] Fix JavaScript fallback regex compilation --- .github/workflows/fix-302-fallback-regex.yml | 38 ------------------- .../JavascriptPlaceholderBinder.java | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 .github/workflows/fix-302-fallback-regex.yml diff --git a/.github/workflows/fix-302-fallback-regex.yml b/.github/workflows/fix-302-fallback-regex.yml deleted file mode 100644 index 228978f6b..000000000 --- a/.github/workflows/fix-302-fallback-regex.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Fix JavaScript fallback regex -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Fix fallback regex - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = p.read_text() - lines = text.splitlines() - found = False - for i, line in enumerate(lines): - if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): - lines[i] = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\r\\n])+/[dgimsuvy]*");' - found = True - break - if not found: - raise SystemExit('FALLBACK_REGEX line not found') - p.write_text('\n'.join(lines) + '\n') - PY - rm -f .github/workflows/fix-302-fallback-regex.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Fix JavaScript fallback regex compilation' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 445e714b5..ce4ee6429 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -46,7 +46,7 @@ public final class JavascriptPlaceholderBinder { private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\""); private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\.|[^`\\])*`"); - private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\r\n])+/[dgimsuvy]*"); + private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|[^/\r\n])+/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { } From fe87cb9c13397a64cc033401fc82d1544e4571f4 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:50:39 -0600 Subject: [PATCH 15/63] Strengthen modern JavaScript placeholder fallback tests --- ...JavascriptPlaceholderModernSyntaxFallbackTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index c981c1e35..b43bd9b89 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -26,4 +26,15 @@ void newerEngineSyntaxStillPreservesQuotedPlaceholderSemantics() { assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); assertTrue(bindings.isEmpty()); } + + @Test + void newerEngineSyntaxStillPreservesRegexPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } } From 559204f089e01e6a42776625273cc5b91a3de8a9 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:52:51 -0600 Subject: [PATCH 16/63] Prepare fallback literal pattern fix --- .../workflows/fix-302-fallback-literals.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/fix-302-fallback-literals.yml diff --git a/.github/workflows/fix-302-fallback-literals.yml b/.github/workflows/fix-302-fallback-literals.yml new file mode 100644 index 000000000..0dc46f2a9 --- /dev/null +++ b/.github/workflows/fix-302-fallback-literals.yml @@ -0,0 +1,40 @@ +name: Fix JavaScript fallback literal patterns +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Fix fallback literal patterns + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + lines = p.read_text().splitlines() + found_string = found_template = False + for i, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith('private static final Pattern FALLBACK_STRING ='): + lines[i] = r''' private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\"");''' + found_string = True + elif stripped.startswith('private static final Pattern FALLBACK_TEMPLATE ='): + lines[i] = r''' private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`");''' + found_template = True + if not found_string or not found_template: + raise SystemExit(f'patterns missing string={found_string} template={found_template}') + p.write_text('\n'.join(lines) + '\n') + PY + rm -f .github/workflows/fix-302-fallback-literals.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Fix JavaScript fallback literal patterns' + git push origin HEAD:security/javascript-placeholder-bindings From 01a6ddcfdab42cc6bcb3b6556a3989519f5088f6 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 01:52:58 +0000 Subject: [PATCH 17/63] Fix JavaScript fallback literal patterns --- .../workflows/fix-302-fallback-literals.yml | 40 ------------------- .../JavascriptPlaceholderBinder.java | 4 +- 2 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/fix-302-fallback-literals.yml diff --git a/.github/workflows/fix-302-fallback-literals.yml b/.github/workflows/fix-302-fallback-literals.yml deleted file mode 100644 index 0dc46f2a9..000000000 --- a/.github/workflows/fix-302-fallback-literals.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Fix JavaScript fallback literal patterns -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Fix fallback literal patterns - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - lines = p.read_text().splitlines() - found_string = found_template = False - for i, line in enumerate(lines): - stripped = line.strip() - if stripped.startswith('private static final Pattern FALLBACK_STRING ='): - lines[i] = r''' private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\"");''' - found_string = True - elif stripped.startswith('private static final Pattern FALLBACK_TEMPLATE ='): - lines[i] = r''' private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`");''' - found_template = True - if not found_string or not found_template: - raise SystemExit(f'patterns missing string={found_string} template={found_template}') - p.write_text('\n'.join(lines) + '\n') - PY - rm -f .github/workflows/fix-302-fallback-literals.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Fix JavaScript fallback literal patterns' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index ce4ee6429..0097f8ab8 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -44,8 +44,8 @@ public final class JavascriptPlaceholderBinder { private static final String DIAGNOSTIC_LISTENER_CLASS = "org.openjdk.nashorn.api.tree.DiagnosticListener"; private static final String TREE_CLASS = "org.openjdk.nashorn.api.tree.Tree"; private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; - private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\""); - private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\.|[^`\\])*`"); + private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\""); + private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|[^/\r\n])+/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { From 0586a6c551d3693c2e9b1e352a05f6009e8918b9 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:53:30 -0600 Subject: [PATCH 18/63] Cover modern JavaScript template fallback --- ...JavascriptPlaceholderModernSyntaxFallbackTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index b43bd9b89..cad41a9f5 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -37,4 +37,15 @@ void newerEngineSyntaxStillPreservesRegexPlaceholderSemantics() { assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); assertTrue(bindings.isEmpty()); } + + @Test + void newerEngineSyntaxStillPreservesTemplatePlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", + ignored -> "Ben` ${attack}", bindings::put); + + assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); + assertTrue(bindings.isEmpty()); + } } From 15655afe84190c1c1550c41788348305f1574deb Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:55:59 -0600 Subject: [PATCH 19/63] Prepare parser diagnostic fallback fix --- .../workflows/fix-302-parser-diagnostics.yml | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/fix-302-parser-diagnostics.yml diff --git a/.github/workflows/fix-302-parser-diagnostics.yml b/.github/workflows/fix-302-parser-diagnostics.yml new file mode 100644 index 000000000..d191a4cf4 --- /dev/null +++ b/.github/workflows/fix-302-parser-diagnostics.yml @@ -0,0 +1,58 @@ +name: Fix JavaScript parser diagnostic fallback +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply parser diagnostic fix + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = p.read_text() + old = ''' Object parser = createParser(parserClass); + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> null); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''' + new = ''' Object parser = createParser(parserClass); + boolean[] hadParseDiagnostic = new boolean[1]; + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> { + if ("report".equals(method.getName())) { + hadParseDiagnostic[0] = true; + } + return null; + }); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null && !hadParseDiagnostic[0]) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''' + count = text.count(old) + if count != 1: + raise SystemExit(f'parse diagnostic block: expected 1 match, got {count}') + p.write_text(text.replace(old, new, 1)) + PY + rm -f .github/workflows/fix-302-parser-diagnostics.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Fall back on JavaScript parser diagnostics' + git push origin HEAD:security/javascript-placeholder-bindings From a14d89e88f916dff4cf41bf3516e9b0431c0f342 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:56:42 -0600 Subject: [PATCH 20/63] Add parser diagnostic patch script --- .github/fix_302_parser_diagnostic.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/fix_302_parser_diagnostic.py diff --git a/.github/fix_302_parser_diagnostic.py b/.github/fix_302_parser_diagnostic.py new file mode 100644 index 000000000..bf533ec36 --- /dev/null +++ b/.github/fix_302_parser_diagnostic.py @@ -0,0 +1,34 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() +old = ''' Object parser = createParser(parserClass); + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> null); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''' +new = ''' Object parser = createParser(parserClass); + boolean[] hadParseDiagnostic = new boolean[1]; + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> { + if ("report".equals(method.getName())) { + hadParseDiagnostic[0] = true; + } + return null; + }); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null && !hadParseDiagnostic[0]) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } +''' +count = text.count(old) +if count != 1: + raise SystemExit(f'parse diagnostic block: expected 1 match, got {count}') +p.write_text(text.replace(old, new, 1)) From f21fe39f7bacbfdec3ddd0e616a915e9f96f6f51 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:56:52 -0600 Subject: [PATCH 21/63] Run parser diagnostic patch script --- .../workflows/fix-302-parser-diagnostics.yml | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/.github/workflows/fix-302-parser-diagnostics.yml b/.github/workflows/fix-302-parser-diagnostics.yml index d191a4cf4..17892a1ae 100644 --- a/.github/workflows/fix-302-parser-diagnostics.yml +++ b/.github/workflows/fix-302-parser-diagnostics.yml @@ -15,42 +15,8 @@ jobs: - name: Apply parser diagnostic fix shell: bash run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = p.read_text() - old = ''' Object parser = createParser(parserClass); - Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, - (proxy, method, args) -> null); - Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); - Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null) { - contexts.parsed = true; - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''' - new = ''' Object parser = createParser(parserClass); - boolean[] hadParseDiagnostic = new boolean[1]; - Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, - (proxy, method, args) -> { - if ("report".equals(method.getName())) { - hadParseDiagnostic[0] = true; - } - return null; - }); - Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); - Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null && !hadParseDiagnostic[0]) { - contexts.parsed = true; - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''' - count = text.count(old) - if count != 1: - raise SystemExit(f'parse diagnostic block: expected 1 match, got {count}') - p.write_text(text.replace(old, new, 1)) - PY - rm -f .github/workflows/fix-302-parser-diagnostics.yml + python3 .github/fix_302_parser_diagnostic.py + rm -f .github/fix_302_parser_diagnostic.py .github/workflows/fix-302-parser-diagnostics.yml git config user.name 'Ben' git config user.email 'benbergen12@gmail.com' git add -A From 32596fa7c711deb66dc513f55991b5d01863d1a1 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 01:56:59 +0000 Subject: [PATCH 22/63] Fall back on JavaScript parser diagnostics --- .github/fix_302_parser_diagnostic.py | 34 ------------------- .../workflows/fix-302-parser-diagnostics.yml | 24 ------------- .../JavascriptPlaceholderBinder.java | 10 ++++-- 3 files changed, 8 insertions(+), 60 deletions(-) delete mode 100644 .github/fix_302_parser_diagnostic.py delete mode 100644 .github/workflows/fix-302-parser-diagnostics.yml diff --git a/.github/fix_302_parser_diagnostic.py b/.github/fix_302_parser_diagnostic.py deleted file mode 100644 index bf533ec36..000000000 --- a/.github/fix_302_parser_diagnostic.py +++ /dev/null @@ -1,34 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() -old = ''' Object parser = createParser(parserClass); - Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, - (proxy, method, args) -> null); - Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); - Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null) { - contexts.parsed = true; - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''' -new = ''' Object parser = createParser(parserClass); - boolean[] hadParseDiagnostic = new boolean[1]; - Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, - (proxy, method, args) -> { - if ("report".equals(method.getName())) { - hadParseDiagnostic[0] = true; - } - return null; - }); - Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); - Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null && !hadParseDiagnostic[0]) { - contexts.parsed = true; - walk(root, treeClass, contexts, new IdentityHashMap<>()); - } -''' -count = text.count(old) -if count != 1: - raise SystemExit(f'parse diagnostic block: expected 1 match, got {count}') -p.write_text(text.replace(old, new, 1)) diff --git a/.github/workflows/fix-302-parser-diagnostics.yml b/.github/workflows/fix-302-parser-diagnostics.yml deleted file mode 100644 index 17892a1ae..000000000 --- a/.github/workflows/fix-302-parser-diagnostics.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Fix JavaScript parser diagnostic fallback -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply parser diagnostic fix - shell: bash - run: | - python3 .github/fix_302_parser_diagnostic.py - rm -f .github/fix_302_parser_diagnostic.py .github/workflows/fix-302-parser-diagnostics.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Fall back on JavaScript parser diagnostics' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 0097f8ab8..3fe8ff346 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -320,11 +320,17 @@ private static JavascriptContexts parse(String source) { Class diagnosticClass = Class.forName(DIAGNOSTIC_LISTENER_CLASS, true, loader); Class treeClass = Class.forName(TREE_CLASS, true, loader); Object parser = createParser(parserClass); + boolean[] hadParseDiagnostic = new boolean[1]; Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, - (proxy, method, args) -> null); + (proxy, method, args) -> { + if ("report".equals(method.getName())) { + hadParseDiagnostic[0] = true; + } + return null; + }); Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); - if (root != null) { + if (root != null && !hadParseDiagnostic[0]) { contexts.parsed = true; walk(root, treeClass, contexts, new IdentityHashMap<>()); } From 3b8cbec84303082d38cc6675ac704434cad76763 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 19:57:37 -0600 Subject: [PATCH 23/63] Test modern syntax placeholder injection fallback --- ...scriptPlaceholderModernSyntaxFallbackTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index cad41a9f5..1699cfa4c 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.api.javascript; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashMap; @@ -27,6 +28,20 @@ void newerEngineSyntaxStillPreservesQuotedPlaceholderSemantics() { assertTrue(bindings.isEmpty()); } + @Test + void newerEngineSyntaxStillEscapesQuotedPlaceholderInjection() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, 'op attacker'); '"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'safe'", + ignored -> injection, bindings::put); + + assertEquals("obj?.name && '\\'; Bukkit.dispatchCommand(Console, \\'op attacker\\'); \\'' === 'safe'", + prepared); + assertFalse(prepared.contains("&& ''; Bukkit")); + assertTrue(bindings.isEmpty()); + } + @Test void newerEngineSyntaxStillPreservesRegexPlaceholderSemantics() { HashMap bindings = new HashMap<>(); From 36ec123b06a97998ce14c83c05b0475919eba042 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:00:11 -0600 Subject: [PATCH 24/63] Prepare CodeQL regex fallback fix --- .github/workflows/fix-302-codeql-regex.yml | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/fix-302-codeql-regex.yml diff --git a/.github/workflows/fix-302-codeql-regex.yml b/.github/workflows/fix-302-codeql-regex.yml new file mode 100644 index 000000000..87e6fa4fd --- /dev/null +++ b/.github/workflows/fix-302-codeql-regex.yml @@ -0,0 +1,33 @@ +name: Fix JavaScript fallback regex performance +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Fix fallback regex + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = p.read_text() + old = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\r\\\\n])+/[dgimsuvy]*");' + new = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\\\\\\\\\r\\\\n])+/[dgimsuvy]*");' + if old not in text: + raise SystemExit('FALLBACK_REGEX source line not found') + p.write_text(text.replace(old, new, 1)) + PY + rm -f .github/workflows/fix-302-codeql-regex.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Make JavaScript fallback regex linear' + git push origin HEAD:security/javascript-placeholder-bindings From 44985fd472263e5b48f98022f78d64bfe55cf984 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:01:22 -0600 Subject: [PATCH 25/63] Run CodeQL regex performance fix --- .github/workflows/fix-302-codeql-regex.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fix-302-codeql-regex.yml b/.github/workflows/fix-302-codeql-regex.yml index 87e6fa4fd..cab606623 100644 --- a/.github/workflows/fix-302-codeql-regex.yml +++ b/.github/workflows/fix-302-codeql-regex.yml @@ -18,12 +18,18 @@ jobs: python3 - <<'PY' from pathlib import Path p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = p.read_text() - old = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\r\\\\n])+/[dgimsuvy]*");' - new = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\\\\\\\\\r\\\\n])+/[dgimsuvy]*");' - if old not in text: + lines = p.read_text().splitlines() + found = False + for i, line in enumerate(lines): + if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): + if ')++/' not in line: + line = line.replace(')+/', ')++/') + lines[i] = line + found = True + break + if not found: raise SystemExit('FALLBACK_REGEX source line not found') - p.write_text(text.replace(old, new, 1)) + p.write_text('\n'.join(lines) + '\n') PY rm -f .github/workflows/fix-302-codeql-regex.yml git config user.name 'Ben' From cd5a33207f9f92e7786cf88887b4450df3b4046b Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 02:01:29 +0000 Subject: [PATCH 26/63] Make JavaScript fallback regex linear --- .github/workflows/fix-302-codeql-regex.yml | 39 ------------------- .../JavascriptPlaceholderBinder.java | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .github/workflows/fix-302-codeql-regex.yml diff --git a/.github/workflows/fix-302-codeql-regex.yml b/.github/workflows/fix-302-codeql-regex.yml deleted file mode 100644 index cab606623..000000000 --- a/.github/workflows/fix-302-codeql-regex.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Fix JavaScript fallback regex performance -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Fix fallback regex - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - lines = p.read_text().splitlines() - found = False - for i, line in enumerate(lines): - if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): - if ')++/' not in line: - line = line.replace(')+/', ')++/') - lines[i] = line - found = True - break - if not found: - raise SystemExit('FALLBACK_REGEX source line not found') - p.write_text('\n'.join(lines) + '\n') - PY - rm -f .github/workflows/fix-302-codeql-regex.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Make JavaScript fallback regex linear' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 3fe8ff346..12e3e6371 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -46,7 +46,7 @@ public final class JavascriptPlaceholderBinder { private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\""); private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); - private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|[^/\r\n])+/[dgimsuvy]*"); + private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|[^/\r\n])++/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { } From 4b5ecf4e33d10ffa407489afce08ef32a1f2bd8a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:02:12 -0600 Subject: [PATCH 27/63] Test possessive JavaScript regex fallback semantics --- ...JavascriptPlaceholderModernSyntaxFallbackTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index 1699cfa4c..c2779d3a5 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -53,6 +53,17 @@ void newerEngineSyntaxStillPreservesRegexPlaceholderSemantics() { assertTrue(bindings.isEmpty()); } + @Test + void newerEngineSyntaxRegexFallbackKeepsEscapedSlashSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^path\\/%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^path\\/Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + @Test void newerEngineSyntaxStillPreservesTemplatePlaceholderSemantics() { HashMap bindings = new HashMap<>(); From f40e12e9154261d408857972580c986744c08b1c Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:03:48 -0600 Subject: [PATCH 28/63] Prepare linear regex fallback correction --- .../workflows/fix-302-codeql-regex-linear.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/fix-302-codeql-regex-linear.yml diff --git a/.github/workflows/fix-302-codeql-regex-linear.yml b/.github/workflows/fix-302-codeql-regex-linear.yml new file mode 100644 index 000000000..c34a24db2 --- /dev/null +++ b/.github/workflows/fix-302-codeql-regex-linear.yml @@ -0,0 +1,37 @@ +name: Correct JavaScript fallback regex +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Correct fallback regex + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + lines = p.read_text().splitlines() + found = False + for i, line in enumerate(lines): + if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): + lines[i] = r''' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\\\\\r\\n])+/[dgimsuvy]*");''' + found = True + break + if not found: + raise SystemExit('FALLBACK_REGEX source line not found') + p.write_text('\n'.join(lines) + '\n') + PY + rm -f .github/workflows/fix-302-codeql-regex-linear.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Use disjoint JavaScript regex fallback' + git push origin HEAD:security/javascript-placeholder-bindings From 118eb6acb616e793398a5b14ea61a29328a5d635 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 02:03:55 +0000 Subject: [PATCH 29/63] Use disjoint JavaScript regex fallback --- .../workflows/fix-302-codeql-regex-linear.yml | 37 ------------------- .../JavascriptPlaceholderBinder.java | 2 +- 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 .github/workflows/fix-302-codeql-regex-linear.yml diff --git a/.github/workflows/fix-302-codeql-regex-linear.yml b/.github/workflows/fix-302-codeql-regex-linear.yml deleted file mode 100644 index c34a24db2..000000000 --- a/.github/workflows/fix-302-codeql-regex-linear.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Correct JavaScript fallback regex -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Correct fallback regex - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - lines = p.read_text().splitlines() - found = False - for i, line in enumerate(lines): - if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): - lines[i] = r''' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\\\\\r\\n])+/[dgimsuvy]*");''' - found = True - break - if not found: - raise SystemExit('FALLBACK_REGEX source line not found') - p.write_text('\n'.join(lines) + '\n') - PY - rm -f .github/workflows/fix-302-codeql-regex-linear.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Use disjoint JavaScript regex fallback' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 12e3e6371..20c8dc51e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -46,7 +46,7 @@ public final class JavascriptPlaceholderBinder { private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\""); private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); - private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\.|[^/\r\n])++/[dgimsuvy]*"); + private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\\\\\r\\n])+/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { } From b4fcc69277c29688e43ed37b8eadc45f8b9f23cb Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:04:37 -0600 Subject: [PATCH 30/63] Stress JavaScript regex fallback matching --- ...vascriptPlaceholderModernSyntaxFallbackTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index c2779d3a5..c54af2f96 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -64,6 +64,19 @@ void newerEngineSyntaxRegexFallbackKeepsEscapedSlashSemantics() { assertTrue(bindings.isEmpty()); } + @Test + void regexFallbackHandlesLongOrdinaryRunsWithoutChangingPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + String dots = ".".repeat(10_000); + String script = "obj?.name && /^" + dots + "%name%$/.test(value)"; + + String prepared = JavascriptPlaceholderBinder.bind(script, ignored -> "Ben", bindings::put); + + assertTrue(prepared.startsWith("obj?.name && /^" + dots)); + assertTrue(prepared.endsWith("Ben$/.test(value)")); + assertTrue(bindings.isEmpty()); + } + @Test void newerEngineSyntaxStillPreservesTemplatePlaceholderSemantics() { HashMap bindings = new HashMap<>(); From 4385f8b7a1db86b964c6de5b4938f98e2a0965d1 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:07:12 -0600 Subject: [PATCH 31/63] Prepare iterative regex fallback fix --- .github/fix_302_iterative_regex.py | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/fix_302_iterative_regex.py diff --git a/.github/fix_302_iterative_regex.py b/.github/fix_302_iterative_regex.py new file mode 100644 index 000000000..211d17e4c --- /dev/null +++ b/.github/fix_302_iterative_regex.py @@ -0,0 +1,100 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() + +old = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\\\\\\\r\\n])+/[dgimsuvy]*");\n' +if old not in text: + # Match the currently rendered Java source regardless of Python escape confusion. + lines = text.splitlines(True) + filtered = [] + removed = False + for line in lines: + if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): + removed = True + continue + filtered.append(line) + if not removed: + raise SystemExit('FALLBACK_REGEX constant not found') + text = ''.join(filtered) +else: + text = text.replace(old, '', 1) + +old = ''' addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); + contexts.sort(); + return contexts; + } + + private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { +''' +new = ''' addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } + + private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { + for (int i = 0; i < source.length(); i++) { + if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null) { + continue; + } + + boolean escaped = false; + boolean characterClass = false; + for (int j = i + 1; j < source.length(); j++) { + char current = source.charAt(j); + if (current == '\\r' || current == '\\n') { + break; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + continue; + } + if (current == ']') { + characterClass = false; + continue; + } + if (current != '/' || characterClass) { + continue; + } + + int end = j + 1; + while (end < source.length() && "dgimsuvy".indexOf(source.charAt(end)) >= 0) { + end++; + } + Range candidate = new Range(i, end); + if (!contexts.overlapsLiteral(candidate)) { + contexts.regexes.add(candidate); + } + i = end - 1; + break; + } + } + } + + private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { +''' +if text.count(old) != 1: + raise SystemExit(f'fallback insertion point expected once, got {text.count(old)}') +text = text.replace(old, new, 1) + +old_comment = ''' // nashorn-core is packaged with AdvancedCore so parser support remains + // available even when the active ScriptEngine is Rhino/GraalJS. +''' +new_comment = ''' // Use a parser already visible to AdvancedCore when available. If not, + // the handler can prepare a dedicated runtime Nashorn parser loader below. +''' +if old_comment in text: + text = text.replace(old_comment, new_comment, 1) + +p.write_text(text) From 14e74ff9302039d68a64a9dbc7b8107ab0e1881a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:07:27 -0600 Subject: [PATCH 32/63] Run iterative regex fallback fix --- .../workflows/run-fix-302-iterative-regex.yml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/run-fix-302-iterative-regex.yml diff --git a/.github/workflows/run-fix-302-iterative-regex.yml b/.github/workflows/run-fix-302-iterative-regex.yml new file mode 100644 index 000000000..11d38df69 --- /dev/null +++ b/.github/workflows/run-fix-302-iterative-regex.yml @@ -0,0 +1,24 @@ +name: Replace JavaScript regex fallback scanner +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply iterative scanner + shell: bash + run: | + python3 .github/fix_302_iterative_regex.py + rm -f .github/fix_302_iterative_regex.py .github/workflows/run-fix-302-iterative-regex.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Use iterative JavaScript regex fallback scanner' + git push origin HEAD:security/javascript-placeholder-bindings From 8c606910e0071db435da72b0975e1fa4d1387536 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 02:07:35 +0000 Subject: [PATCH 33/63] Use iterative JavaScript regex fallback scanner --- .github/fix_302_iterative_regex.py | 100 ------------------ .../workflows/run-fix-302-iterative-regex.yml | 24 ----- .../JavascriptPlaceholderBinder.java | 55 +++++++++- 3 files changed, 51 insertions(+), 128 deletions(-) delete mode 100644 .github/fix_302_iterative_regex.py delete mode 100644 .github/workflows/run-fix-302-iterative-regex.yml diff --git a/.github/fix_302_iterative_regex.py b/.github/fix_302_iterative_regex.py deleted file mode 100644 index 211d17e4c..000000000 --- a/.github/fix_302_iterative_regex.py +++ /dev/null @@ -1,100 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() - -old = ' private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\\\\\.|[^/\\\\\\\\\\r\\n])+/[dgimsuvy]*");\n' -if old not in text: - # Match the currently rendered Java source regardless of Python escape confusion. - lines = text.splitlines(True) - filtered = [] - removed = False - for line in lines: - if line.strip().startswith('private static final Pattern FALLBACK_REGEX ='): - removed = True - continue - filtered.append(line) - if not removed: - raise SystemExit('FALLBACK_REGEX constant not found') - text = ''.join(filtered) -else: - text = text.replace(old, '', 1) - -old = ''' addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); - contexts.sort(); - return contexts; - } - - private static void addPatternRanges(String source, Pattern pattern, List target, - JavascriptContexts existing) { -''' -new = ''' addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } - - private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { - for (int i = 0; i < source.length(); i++) { - if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null) { - continue; - } - - boolean escaped = false; - boolean characterClass = false; - for (int j = i + 1; j < source.length(); j++) { - char current = source.charAt(j); - if (current == '\\r' || current == '\\n') { - break; - } - if (escaped) { - escaped = false; - continue; - } - if (current == '\\\\') { - escaped = true; - continue; - } - if (current == '[') { - characterClass = true; - continue; - } - if (current == ']') { - characterClass = false; - continue; - } - if (current != '/' || characterClass) { - continue; - } - - int end = j + 1; - while (end < source.length() && "dgimsuvy".indexOf(source.charAt(end)) >= 0) { - end++; - } - Range candidate = new Range(i, end); - if (!contexts.overlapsLiteral(candidate)) { - contexts.regexes.add(candidate); - } - i = end - 1; - break; - } - } - } - - private static void addPatternRanges(String source, Pattern pattern, List target, - JavascriptContexts existing) { -''' -if text.count(old) != 1: - raise SystemExit(f'fallback insertion point expected once, got {text.count(old)}') -text = text.replace(old, new, 1) - -old_comment = ''' // nashorn-core is packaged with AdvancedCore so parser support remains - // available even when the active ScriptEngine is Rhino/GraalJS. -''' -new_comment = ''' // Use a parser already visible to AdvancedCore when available. If not, - // the handler can prepare a dedicated runtime Nashorn parser loader below. -''' -if old_comment in text: - text = text.replace(old_comment, new_comment, 1) - -p.write_text(text) diff --git a/.github/workflows/run-fix-302-iterative-regex.yml b/.github/workflows/run-fix-302-iterative-regex.yml deleted file mode 100644 index 11d38df69..000000000 --- a/.github/workflows/run-fix-302-iterative-regex.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Replace JavaScript regex fallback scanner -on: - push: - branches: [security/javascript-placeholder-bindings] -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply iterative scanner - shell: bash - run: | - python3 .github/fix_302_iterative_regex.py - rm -f .github/fix_302_iterative_regex.py .github/workflows/run-fix-302-iterative-regex.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Use iterative JavaScript regex fallback scanner' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 20c8dc51e..9532cdb28 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -46,7 +46,6 @@ public final class JavascriptPlaceholderBinder { private static final String TREE_PACKAGE = "org.openjdk.nashorn.api.tree"; private static final Pattern FALLBACK_STRING = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\""); private static final Pattern FALLBACK_TEMPLATE = Pattern.compile("`(?:\\\\.|[^`\\\\])*`"); - private static final Pattern FALLBACK_REGEX = Pattern.compile("/(?:\\\\.|[^/\\\\\\r\\n])+/[dgimsuvy]*"); private JavascriptPlaceholderBinder() { } @@ -353,11 +352,59 @@ private static JavascriptContexts fallback(String source) { addFallbackTemplateExpressions(source, template, contexts.templateExpressions); } - addPatternRanges(source, FALLBACK_REGEX, contexts.regexes, contexts); + addFallbackRegexRanges(source, contexts); contexts.sort(); return contexts; } + private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { + for (int i = 0; i < source.length(); i++) { + if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null) { + continue; + } + + boolean escaped = false; + boolean characterClass = false; + for (int j = i + 1; j < source.length(); j++) { + char current = source.charAt(j); + if (current == '\r' || current == '\n') { + break; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + continue; + } + if (current == ']') { + characterClass = false; + continue; + } + if (current != '/' || characterClass) { + continue; + } + + int end = j + 1; + while (end < source.length() && "dgimsuvy".indexOf(source.charAt(end)) >= 0) { + end++; + } + Range candidate = new Range(i, end); + if (!contexts.overlapsLiteral(candidate)) { + contexts.regexes.add(candidate); + } + i = end - 1; + break; + } + } + } + private static void addPatternRanges(String source, Pattern pattern, List target, JavascriptContexts existing) { Matcher matcher = pattern.matcher(source); @@ -439,8 +486,8 @@ private static ClassLoader parserClassLoader() { return downloaded; } - // nashorn-core is packaged with AdvancedCore so parser support remains - // available even when the active ScriptEngine is Rhino/GraalJS. + // Use a parser already visible to AdvancedCore when available. If not, + // the handler can prepare a dedicated runtime Nashorn parser loader below. ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); if (canLoadParser(own)) { return own; From 9ac0de6fa1a26b2cb18077104b5dbf4cbb5ff046 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:08:24 -0600 Subject: [PATCH 34/63] Test iterative JavaScript regex fallback --- ...JavascriptPlaceholderModernSyntaxFallbackTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index c54af2f96..f4a1c454d 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -64,6 +64,17 @@ void newerEngineSyntaxRegexFallbackKeepsEscapedSlashSemantics() { assertTrue(bindings.isEmpty()); } + @Test + void newerEngineSyntaxRegexFallbackKeepsCharacterClassSlashes() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^[a/b]%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^[a/b]Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + @Test void regexFallbackHandlesLongOrdinaryRunsWithoutChangingPlaceholderSemantics() { HashMap bindings = new HashMap<>(); From 1706d73e342ab5ce5b4076100d8d785d6b6b6564 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:56:14 -0600 Subject: [PATCH 35/63] Prepare latest Codex fallback fixes --- .../workflows/fix-302-template-division.yml | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 .github/workflows/fix-302-template-division.yml diff --git a/.github/workflows/fix-302-template-division.yml b/.github/workflows/fix-302-template-division.yml new file mode 100644 index 000000000..a56ca3ff1 --- /dev/null +++ b/.github/workflows/fix-302-template-division.yml @@ -0,0 +1,153 @@ +name: Fix latest JavaScript fallback findings +on: + push: + branches: [security/javascript-placeholder-bindings] +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply fallback fixes + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = p.read_text() + + old = ' contexts = JavascriptContexts.fallback(expression);' + new = ' contexts = JavascriptContexts.fallback(sanitized.toString());' + if text.count(old) != 1: + raise SystemExit('fallback call match failed') + text = text.replace(old, new, 1) + + old = ''' if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { +''' + new = ''' if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + // Template text wins over quote-looking text inside the template. A value + // containing ${...} must never become a live interpolation. + replacements[i] = escapeTemplate(match.value); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + replacements[i] = escapeString(match.value, delimiter); + } else { +''' + if text.count(old) != 1: + raise SystemExit('binding priority match failed') + text = text.replace(old, new, 1) + + old = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } +''' + new = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + // Do not classify quote-looking text inside a template as a string literal. + addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } +''' + if text.count(old) != 1: + raise SystemExit('fallback ordering match failed') + text = text.replace(old, new, 1) + + old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null) { + continue; + } +''' + new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { + continue; + } +''' + if text.count(old) != 1: + raise SystemExit('regex start condition match failed') + text = text.replace(old, new, 1) + + marker = ''' private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { +''' + helper = ''' private static boolean canStartRegex(String source, int slashIndex) { + int previousIndex = slashIndex - 1; + while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { + previousIndex--; + } + if (previousIndex < 0) { + return true; + } + + char previous = source.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + + if (Character.isJavaIdentifierPart(previous)) { + int end = previousIndex + 1; + int start = previousIndex; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end); + return word.equals("return") || word.equals("case") || word.equals("throw") + || word.equals("else") || word.equals("do") || word.equals("yield") + || word.equals("await") || word.equals("typeof") || word.equals("void") + || word.equals("delete") || word.equals("instanceof") || word.equals("in") + || word.equals("new"); + } + return false; + } + +''' + if text.count(marker) != 1: + raise SystemExit('helper insertion marker failed') + text = text.replace(marker, helper + marker, 1) + + p.write_text(text) + PY + rm -f .github/workflows/fix-302-template-division.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Harden JavaScript fallback contexts' + git push origin HEAD:security/javascript-placeholder-bindings From 4cad359cbadcecacf43314c37fbc2324a81d605d Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:56:52 -0600 Subject: [PATCH 36/63] Add latest Codex fallback patch script --- .github/fix-302-latest.py | 123 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/fix-302-latest.py diff --git a/.github/fix-302-latest.py b/.github/fix-302-latest.py new file mode 100644 index 000000000..193c69483 --- /dev/null +++ b/.github/fix-302-latest.py @@ -0,0 +1,123 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() + +old = ' contexts = JavascriptContexts.fallback(expression);' +new = ' contexts = JavascriptContexts.fallback(sanitized.toString());' +assert text.count(old) == 1 +text = text.replace(old, new, 1) + +old = ''' if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { +''' +new = ''' if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + // Template text wins over quote-looking text inside the template. A value + // containing ${...} must never become a live interpolation. + replacements[i] = escapeTemplate(match.value); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + replacements[i] = escapeString(match.value, delimiter); + } else { +''' +assert text.count(old) == 1 +text = text.replace(old, new, 1) + +old = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } +''' +new = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + // Do not classify quote-looking text inside a template as a string literal. + addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } +''' +assert text.count(old) == 1 +text = text.replace(old, new, 1) + +old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null) { + continue; + } +''' +new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { + continue; + } +''' +assert text.count(old) == 1 +text = text.replace(old, new, 1) + +marker = ''' private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { +''' +helper = ''' private static boolean canStartRegex(String source, int slashIndex) { + int previousIndex = slashIndex - 1; + while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { + previousIndex--; + } + if (previousIndex < 0) { + return true; + } + + char previous = source.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + + if (Character.isJavaIdentifierPart(previous)) { + int end = previousIndex + 1; + int start = previousIndex; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end); + return word.equals("return") || word.equals("case") || word.equals("throw") + || word.equals("else") || word.equals("do") || word.equals("yield") + || word.equals("await") || word.equals("typeof") || word.equals("void") + || word.equals("delete") || word.equals("instanceof") || word.equals("in") + || word.equals("new"); + } + return false; + } + +''' +assert text.count(marker) == 1 +text = text.replace(marker, helper + marker, 1) +p.write_text(text) From 85c379cfdbd256f62905e72236a3186a00fb88ad Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:57:10 -0600 Subject: [PATCH 37/63] Run latest Codex fallback patch --- .../workflows/fix-302-template-division.yml | 138 +----------------- 1 file changed, 5 insertions(+), 133 deletions(-) diff --git a/.github/workflows/fix-302-template-division.yml b/.github/workflows/fix-302-template-division.yml index a56ca3ff1..fe6346baa 100644 --- a/.github/workflows/fix-302-template-division.yml +++ b/.github/workflows/fix-302-template-division.yml @@ -1,7 +1,8 @@ name: Fix latest JavaScript fallback findings on: push: - branches: [security/javascript-placeholder-bindings] + branches: + - security/javascript-placeholder-bindings permissions: contents: write jobs: @@ -13,139 +14,10 @@ jobs: with: ref: security/javascript-placeholder-bindings - name: Apply fallback fixes - shell: bash + run: python3 .github/fix-302-latest.py + - name: Commit fixes run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = p.read_text() - - old = ' contexts = JavascriptContexts.fallback(expression);' - new = ' contexts = JavascriptContexts.fallback(sanitized.toString());' - if text.count(old) != 1: - raise SystemExit('fallback call match failed') - text = text.replace(old, new, 1) - - old = ''' if (regex != null) { - replacements[i] = escapeRegex(match.value, expression, regex, match.start); - } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { - replacements[i] = escapeString(match.value, delimiter); - } - } else if (template != null && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { -''' - new = ''' if (regex != null) { - replacements[i] = escapeRegex(match.value, expression, regex, match.start); - } else if (template != null && !contexts.insideTemplateExpression(match.start)) { - // Template text wins over quote-looking text inside the template. A value - // containing ${...} must never become a live interpolation. - replacements[i] = escapeTemplate(match.value); - } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - replacements[i] = escapeString(match.value, delimiter); - } else { -''' - if text.count(old) != 1: - raise SystemExit('binding priority match failed') - text = text.replace(old, new, 1) - - old = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } -''' - new = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - // Do not classify quote-looking text inside a template as a string literal. - addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } -''' - if text.count(old) != 1: - raise SystemExit('fallback ordering match failed') - text = text.replace(old, new, 1) - - old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null) { - continue; - } -''' - new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { - continue; - } -''' - if text.count(old) != 1: - raise SystemExit('regex start condition match failed') - text = text.replace(old, new, 1) - - marker = ''' private static void addPatternRanges(String source, Pattern pattern, List target, - JavascriptContexts existing) { -''' - helper = ''' private static boolean canStartRegex(String source, int slashIndex) { - int previousIndex = slashIndex - 1; - while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { - previousIndex--; - } - if (previousIndex < 0) { - return true; - } - - char previous = source.charAt(previousIndex); - if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { - return true; - } - - if (Character.isJavaIdentifierPart(previous)) { - int end = previousIndex + 1; - int start = previousIndex; - while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { - start--; - } - String word = source.substring(start + 1, end); - return word.equals("return") || word.equals("case") || word.equals("throw") - || word.equals("else") || word.equals("do") || word.equals("yield") - || word.equals("await") || word.equals("typeof") || word.equals("void") - || word.equals("delete") || word.equals("instanceof") || word.equals("in") - || word.equals("new"); - } - return false; - } - -''' - if text.count(marker) != 1: - raise SystemExit('helper insertion marker failed') - text = text.replace(marker, helper + marker, 1) - - p.write_text(text) - PY - rm -f .github/workflows/fix-302-template-division.yml + rm -f .github/fix-302-latest.py .github/workflows/fix-302-template-division.yml git config user.name 'Ben' git config user.email 'benbergen12@gmail.com' git add -A From 5b1140e858d954af83017b97ef60af9ded86eea7 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 02:57:17 +0000 Subject: [PATCH 38/63] Harden JavaScript fallback contexts --- .github/fix-302-latest.py | 123 ------------------ .../workflows/fix-302-template-division.yml | 25 ---- .../JavascriptPlaceholderBinder.java | 49 +++++-- 3 files changed, 39 insertions(+), 158 deletions(-) delete mode 100644 .github/fix-302-latest.py delete mode 100644 .github/workflows/fix-302-template-division.yml diff --git a/.github/fix-302-latest.py b/.github/fix-302-latest.py deleted file mode 100644 index 193c69483..000000000 --- a/.github/fix-302-latest.py +++ /dev/null @@ -1,123 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() - -old = ' contexts = JavascriptContexts.fallback(expression);' -new = ' contexts = JavascriptContexts.fallback(sanitized.toString());' -assert text.count(old) == 1 -text = text.replace(old, new, 1) - -old = ''' if (regex != null) { - replacements[i] = escapeRegex(match.value, expression, regex, match.start); - } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { - replacements[i] = escapeString(match.value, delimiter); - } - } else if (template != null && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { -''' -new = ''' if (regex != null) { - replacements[i] = escapeRegex(match.value, expression, regex, match.start); - } else if (template != null && !contexts.insideTemplateExpression(match.start)) { - // Template text wins over quote-looking text inside the template. A value - // containing ${...} must never become a live interpolation. - replacements[i] = escapeTemplate(match.value); - } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - replacements[i] = escapeString(match.value, delimiter); - } else { -''' -assert text.count(old) == 1 -text = text.replace(old, new, 1) - -old = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } -''' -new = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - // Do not classify quote-looking text inside a template as a string literal. - addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } -''' -assert text.count(old) == 1 -text = text.replace(old, new, 1) - -old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null) { - continue; - } -''' -new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { - continue; - } -''' -assert text.count(old) == 1 -text = text.replace(old, new, 1) - -marker = ''' private static void addPatternRanges(String source, Pattern pattern, List target, - JavascriptContexts existing) { -''' -helper = ''' private static boolean canStartRegex(String source, int slashIndex) { - int previousIndex = slashIndex - 1; - while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { - previousIndex--; - } - if (previousIndex < 0) { - return true; - } - - char previous = source.charAt(previousIndex); - if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { - return true; - } - - if (Character.isJavaIdentifierPart(previous)) { - int end = previousIndex + 1; - int start = previousIndex; - while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { - start--; - } - String word = source.substring(start + 1, end); - return word.equals("return") || word.equals("case") || word.equals("throw") - || word.equals("else") || word.equals("do") || word.equals("yield") - || word.equals("await") || word.equals("typeof") || word.equals("void") - || word.equals("delete") || word.equals("instanceof") || word.equals("in") - || word.equals("new"); - } - return false; - } - -''' -assert text.count(marker) == 1 -text = text.replace(marker, helper + marker, 1) -p.write_text(text) diff --git a/.github/workflows/fix-302-template-division.yml b/.github/workflows/fix-302-template-division.yml deleted file mode 100644 index fe6346baa..000000000 --- a/.github/workflows/fix-302-template-division.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Fix latest JavaScript fallback findings -on: - push: - branches: - - security/javascript-placeholder-bindings -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply fallback fixes - run: python3 .github/fix-302-latest.py - - name: Commit fixes - run: | - rm -f .github/fix-302-latest.py .github/workflows/fix-302-template-division.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Harden JavaScript fallback contexts' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 9532cdb28..281ea3653 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -87,7 +87,7 @@ static String bind(String expression, Function resolver, BiConsu JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); if (!contexts.parsed) { - contexts = JavascriptContexts.fallback(expression); + contexts = JavascriptContexts.fallback(sanitized.toString()); } String[] replacements = new String[matches.size()]; int bindingIndex = 0; @@ -103,15 +103,13 @@ static String bind(String expression, Function resolver, BiConsu Range template = contexts.containing(contexts.templates, match.start); if (regex != null) { replacements[i] = escapeRegex(match.value, expression, regex, match.start); - } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { - replacements[i] = escapeString(match.value, delimiter); - } } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + // Template text wins over quote-looking text inside the template. A value + // containing ${...} must never become a live interpolation. replacements[i] = escapeTemplate(match.value); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + replacements[i] = escapeString(match.value, delimiter); } else { String variable = VARIABLE_PREFIX + bindingIndex++; bindings.accept(variable, coerce(match.value)); @@ -343,7 +341,6 @@ private static JavascriptContexts parse(String source) { private static JavascriptContexts fallback(String source) { JavascriptContexts contexts = new JavascriptContexts(); - addPatternRanges(source, FALLBACK_STRING, contexts.strings, null); Matcher templates = FALLBACK_TEMPLATE.matcher(source); while (templates.find()) { @@ -352,6 +349,8 @@ private static JavascriptContexts fallback(String source) { addFallbackTemplateExpressions(source, template, contexts.templateExpressions); } + // Do not classify quote-looking text inside a template as a string literal. + addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); addFallbackRegexRanges(source, contexts); contexts.sort(); return contexts; @@ -360,7 +359,7 @@ private static JavascriptContexts fallback(String source) { private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { for (int i = 0; i < source.length(); i++) { if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null) { + || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { continue; } @@ -405,6 +404,36 @@ private static void addFallbackRegexRanges(String source, JavascriptContexts con } } + private static boolean canStartRegex(String source, int slashIndex) { + int previousIndex = slashIndex - 1; + while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { + previousIndex--; + } + if (previousIndex < 0) { + return true; + } + + char previous = source.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + + if (Character.isJavaIdentifierPart(previous)) { + int end = previousIndex + 1; + int start = previousIndex; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end); + return word.equals("return") || word.equals("case") || word.equals("throw") + || word.equals("else") || word.equals("do") || word.equals("yield") + || word.equals("await") || word.equals("typeof") || word.equals("void") + || word.equals("delete") || word.equals("instanceof") || word.equals("in") + || word.equals("new"); + } + return false; + } + private static void addPatternRanges(String source, Pattern pattern, List target, JavascriptContexts existing) { Matcher matcher = pattern.matcher(source); From 43d8be350d40981017e1c22c9868a0ff53829469 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:58:03 -0600 Subject: [PATCH 39/63] Test latest JavaScript fallback findings --- ...ptPlaceholderModernSyntaxFallbackTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index f4a1c454d..19e78c4d4 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -98,4 +98,26 @@ void newerEngineSyntaxStillPreservesTemplatePlaceholderSemantics() { assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); assertTrue(bindings.isEmpty()); } + + @Test + void quoteLookingTextInsideTemplateStillUsesTemplateEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `'%name%'`", + ignored -> "${attack()}", bindings::put); + + assertEquals("obj?.name; `'\\${attack()}'`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void divisionOperatorsDoNotBecomeFallbackRegexRanges() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.value; 10 / %count% / 2", + ignored -> "2.5", bindings::put); + + assertEquals("obj?.value; 10 / __advancedCorePlaceholder0 / 2", prepared); + assertEquals(2.5D, bindings.get("__advancedCorePlaceholder0")); + } } From 2a54fbe42dff6a2491a37e369dcf715b5b42944a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 20:59:33 -0600 Subject: [PATCH 40/63] Prepare template parser compatibility fix --- .../fix-302-template-parser-regression.yml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/fix-302-template-parser-regression.yml diff --git a/.github/workflows/fix-302-template-parser-regression.yml b/.github/workflows/fix-302-template-parser-regression.yml new file mode 100644 index 000000000..701c3f6ff --- /dev/null +++ b/.github/workflows/fix-302-template-parser-regression.yml @@ -0,0 +1,45 @@ +name: Fix JavaScript template parser compatibility +on: + push: + branches: + - security/javascript-placeholder-bindings +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Restore template delimiter handling + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') + text = p.read_text() + old = ''' } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + replacements[i] = escapeString(match.value, delimiter); + } else { +''' + new = ''' } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } + } else { +''' + if text.count(old) != 1: + raise SystemExit('template string branch not found') + p.write_text(text.replace(old, new, 1)) + PY + rm -f .github/workflows/fix-302-template-parser-regression.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Preserve parser template escaping' + git push origin HEAD:security/javascript-placeholder-bindings From c77c6e523163822681c903268d3058f23590184e Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 21:00:15 -0600 Subject: [PATCH 41/63] Add template parser compatibility patch script --- .github/fix-302-template-parser.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/fix-302-template-parser.py diff --git a/.github/fix-302-template-parser.py b/.github/fix-302-template-parser.py new file mode 100644 index 000000000..3e97b03c5 --- /dev/null +++ b/.github/fix-302-template-parser.py @@ -0,0 +1,21 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() +old = ''' } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + replacements[i] = escapeString(match.value, delimiter); + } else { +''' +new = ''' } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } + } else { +''' +if text.count(old) != 1: + raise SystemExit('template string branch not found') +p.write_text(text.replace(old, new, 1)) From 57be00247ff954c1fd136a5669c8ed585b69c2f9 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 21:00:40 -0600 Subject: [PATCH 42/63] Run template parser compatibility patch --- .../fix-302-template-parser-regression.yml | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/.github/workflows/fix-302-template-parser-regression.yml b/.github/workflows/fix-302-template-parser-regression.yml index 701c3f6ff..a340decfd 100644 --- a/.github/workflows/fix-302-template-parser-regression.yml +++ b/.github/workflows/fix-302-template-parser-regression.yml @@ -13,31 +13,11 @@ jobs: - uses: actions/checkout@v4 with: ref: security/javascript-placeholder-bindings - - name: Restore template delimiter handling + - name: Apply compatibility fix + run: python3 .github/fix-302-template-parser.py + - name: Commit compatibility fix run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') - text = p.read_text() - old = ''' } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - replacements[i] = escapeString(match.value, delimiter); - } else { -''' - new = ''' } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { - replacements[i] = escapeString(match.value, delimiter); - } - } else { -''' - if text.count(old) != 1: - raise SystemExit('template string branch not found') - p.write_text(text.replace(old, new, 1)) - PY - rm -f .github/workflows/fix-302-template-parser-regression.yml + rm -f .github/fix-302-template-parser.py .github/workflows/fix-302-template-parser-regression.yml git config user.name 'Ben' git config user.email 'benbergen12@gmail.com' git add -A From dd09c0204d28609c82b59eb476fcc9f7f1bf168c Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 03:00:48 +0000 Subject: [PATCH 43/63] Preserve parser template escaping --- .github/fix-302-template-parser.py | 21 ---------------- .../fix-302-template-parser-regression.yml | 25 ------------------- .../JavascriptPlaceholderBinder.java | 6 ++++- 3 files changed, 5 insertions(+), 47 deletions(-) delete mode 100644 .github/fix-302-template-parser.py delete mode 100644 .github/workflows/fix-302-template-parser-regression.yml diff --git a/.github/fix-302-template-parser.py b/.github/fix-302-template-parser.py deleted file mode 100644 index 3e97b03c5..000000000 --- a/.github/fix-302-template-parser.py +++ /dev/null @@ -1,21 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() -old = ''' } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - replacements[i] = escapeString(match.value, delimiter); - } else { -''' -new = ''' } else if (string != null) { - char delimiter = literalDelimiter(expression, string); - if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { - replacements[i] = escapeTemplate(match.value); - } else { - replacements[i] = escapeString(match.value, delimiter); - } - } else { -''' -if text.count(old) != 1: - raise SystemExit('template string branch not found') -p.write_text(text.replace(old, new, 1)) diff --git a/.github/workflows/fix-302-template-parser-regression.yml b/.github/workflows/fix-302-template-parser-regression.yml deleted file mode 100644 index a340decfd..000000000 --- a/.github/workflows/fix-302-template-parser-regression.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Fix JavaScript template parser compatibility -on: - push: - branches: - - security/javascript-placeholder-bindings -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply compatibility fix - run: python3 .github/fix-302-template-parser.py - - name: Commit compatibility fix - run: | - rm -f .github/fix-302-template-parser.py .github/workflows/fix-302-template-parser-regression.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Preserve parser template escaping' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 281ea3653..af621d45a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -109,7 +109,11 @@ static String bind(String expression, Function resolver, BiConsu replacements[i] = escapeTemplate(match.value); } else if (string != null) { char delimiter = literalDelimiter(expression, string); - replacements[i] = escapeString(match.value, delimiter); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } } else { String variable = VARIABLE_PREFIX + bindingIndex++; bindings.accept(variable, coerce(match.value)); From ce036cf16e6ba01f84eaa706fb8c8886e483d1b0 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 21:01:42 -0600 Subject: [PATCH 44/63] Strengthen division fallback regression --- .../JavascriptPlaceholderModernSyntaxFallbackTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index 19e78c4d4..06fa9b383 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -118,6 +118,7 @@ void divisionOperatorsDoNotBecomeFallbackRegexRanges() { ignored -> "2.5", bindings::put); assertEquals("obj?.value; 10 / __advancedCorePlaceholder0 / 2", prepared); + assertEquals(1, bindings.size()); assertEquals(2.5D, bindings.get("__advancedCorePlaceholder0")); } } From 41135991464922544fab81adf5db8a1d36b6d50c Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 16:56:59 -0600 Subject: [PATCH 45/63] Prepare latest JavaScript fallback fixes --- .github/scripts/fix302_latest_fallback.py | 210 ++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 .github/scripts/fix302_latest_fallback.py diff --git a/.github/scripts/fix302_latest_fallback.py b/.github/scripts/fix302_latest_fallback.py new file mode 100644 index 000000000..76ba323b0 --- /dev/null +++ b/.github/scripts/fix302_latest_fallback.py @@ -0,0 +1,210 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() + +old = ''' Range candidate = new Range(i, end); + if (!contexts.overlapsLiteral(candidate)) { + contexts.regexes.add(candidate); + } + i = end - 1; + break; +''' +new = ''' Range candidate = new Range(i, end); + // A regex literal may legitimately contain quote characters. Since the + // opening slash was already proven to be outside a string/template and + // can start a regex, discard fallback string ranges fully contained by + // this regex instead of letting quote-looking regex text win. + contexts.removeContained(contexts.strings, candidate); + if (!contexts.overlaps(contexts.templates, candidate)) { + contexts.regexes.add(candidate); + } + i = end - 1; + break; +''' +if text.count(old) != 1: + raise SystemExit('regex candidate block not found') +text = text.replace(old, new, 1) + +old = ''' private static void addFallbackTemplateExpressions(String source, Range template, List target) { + boolean escaped = false; + int expressionStart = -1; + int depth = 0; + for (int i = template.start + 1; i < template.end - 1; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\\\') { + escaped = true; + continue; + } + if (expressionStart < 0) { + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + expressionStart = i + 2; + depth = 1; + i++; + } + continue; + } + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + target.add(new Range(expressionStart, i)); + expressionStart = -1; + } + } + } + } +''' +new = ''' private static void addFallbackTemplateExpressions(String source, Range template, List target) { + boolean escaped = false; + for (int i = template.start + 1; i < template.end - 1; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\\\') { + escaped = true; + continue; + } + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + int expressionStart = i + 2; + int expressionEnd = findTemplateExpressionEnd(source, expressionStart, template.end - 1); + if (expressionEnd >= 0) { + target.add(new Range(expressionStart, expressionEnd)); + i = expressionEnd; + } + } + } + } + + private static int findTemplateExpressionEnd(String source, int start, int limit) { + int depth = 1; + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\\'' || current == '"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, limit); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; + } + + private static int skipQuotedLiteral(String source, int start, int limit, char quote) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + } else if (current == '\\\\') { + escaped = true; + } else if (current == quote) { + return i; + } + } + return limit - 1; + } + + private static int skipTemplateLiteral(String source, int start, int limit) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\\\') { + escaped = true; + continue; + } + if (current == '$' && i + 1 < limit && source.charAt(i + 1) == '{') { + int expressionEnd = findTemplateExpressionEnd(source, i + 2, limit); + if (expressionEnd >= 0) { + i = expressionEnd; + continue; + } + } + if (current == '`') { + return i; + } + } + return limit - 1; + } + + private static int skipRegexLiteral(String source, int start, int limit) { + boolean escaped = false; + boolean characterClass = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (current == '\\r' || current == '\\n') { + return start; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + } else if (current == ']') { + characterClass = false; + } else if (current == '/' && !characterClass) { + int end = i; + while (end + 1 < limit && "dgimsuvy".indexOf(source.charAt(end + 1)) >= 0) { + end++; + } + return end; + } + } + return start; + } +''' +if text.count(old) != 1: + raise SystemExit('template expression block not found') +text = text.replace(old, new, 1) + +marker = ''' private boolean overlaps(List ranges, Range candidate) { + for (Range range : ranges) { + if (candidate.start < range.end && range.start < candidate.end) { + return true; + } + } + return false; + } +''' +replacement = marker + '''\n private void removeContained(List ranges, Range container) { + ranges.removeIf(range -> range.start >= container.start && range.end <= container.end); + } +''' +if text.count(marker) != 1: + raise SystemExit('overlaps marker not found') +text = text.replace(marker, replacement, 1) + +p.write_text(text) From 8f42f7afb06a3173b0ed59474b83da66b4661913 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 16:57:10 -0600 Subject: [PATCH 46/63] Run latest JavaScript fallback fixes --- .../workflows/run-fix302-latest-fallback.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/run-fix302-latest-fallback.yml diff --git a/.github/workflows/run-fix302-latest-fallback.yml b/.github/workflows/run-fix302-latest-fallback.yml new file mode 100644 index 000000000..d185e4e7c --- /dev/null +++ b/.github/workflows/run-fix302-latest-fallback.yml @@ -0,0 +1,25 @@ +name: Run latest JavaScript fallback fixes +on: + push: + branches: + - security/javascript-placeholder-bindings +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply latest fallback fixes + run: python3 .github/scripts/fix302_latest_fallback.py + - name: Commit fallback fixes + run: | + rm -f .github/scripts/fix302_latest_fallback.py .github/workflows/run-fix302-latest-fallback.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Harden template and regex fallback scanning' + git push origin HEAD:security/javascript-placeholder-bindings From fb2d94623bc67df2fcc6c42012840fe388b07888 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 22:57:17 +0000 Subject: [PATCH 47/63] Harden template and regex fallback scanning --- .github/scripts/fix302_latest_fallback.py | 210 ------------------ .../workflows/run-fix302-latest-fallback.yml | 25 --- .../JavascriptPlaceholderBinder.java | 121 +++++++++- 3 files changed, 111 insertions(+), 245 deletions(-) delete mode 100644 .github/scripts/fix302_latest_fallback.py delete mode 100644 .github/workflows/run-fix302-latest-fallback.yml diff --git a/.github/scripts/fix302_latest_fallback.py b/.github/scripts/fix302_latest_fallback.py deleted file mode 100644 index 76ba323b0..000000000 --- a/.github/scripts/fix302_latest_fallback.py +++ /dev/null @@ -1,210 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() - -old = ''' Range candidate = new Range(i, end); - if (!contexts.overlapsLiteral(candidate)) { - contexts.regexes.add(candidate); - } - i = end - 1; - break; -''' -new = ''' Range candidate = new Range(i, end); - // A regex literal may legitimately contain quote characters. Since the - // opening slash was already proven to be outside a string/template and - // can start a regex, discard fallback string ranges fully contained by - // this regex instead of letting quote-looking regex text win. - contexts.removeContained(contexts.strings, candidate); - if (!contexts.overlaps(contexts.templates, candidate)) { - contexts.regexes.add(candidate); - } - i = end - 1; - break; -''' -if text.count(old) != 1: - raise SystemExit('regex candidate block not found') -text = text.replace(old, new, 1) - -old = ''' private static void addFallbackTemplateExpressions(String source, Range template, List target) { - boolean escaped = false; - int expressionStart = -1; - int depth = 0; - for (int i = template.start + 1; i < template.end - 1; i++) { - char current = source.charAt(i); - if (escaped) { - escaped = false; - continue; - } - if (current == '\\\\') { - escaped = true; - continue; - } - if (expressionStart < 0) { - if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { - expressionStart = i + 2; - depth = 1; - i++; - } - continue; - } - if (current == '{') { - depth++; - } else if (current == '}') { - depth--; - if (depth == 0) { - target.add(new Range(expressionStart, i)); - expressionStart = -1; - } - } - } - } -''' -new = ''' private static void addFallbackTemplateExpressions(String source, Range template, List target) { - boolean escaped = false; - for (int i = template.start + 1; i < template.end - 1; i++) { - char current = source.charAt(i); - if (escaped) { - escaped = false; - continue; - } - if (current == '\\\\') { - escaped = true; - continue; - } - if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { - int expressionStart = i + 2; - int expressionEnd = findTemplateExpressionEnd(source, expressionStart, template.end - 1); - if (expressionEnd >= 0) { - target.add(new Range(expressionStart, expressionEnd)); - i = expressionEnd; - } - } - } - } - - private static int findTemplateExpressionEnd(String source, int start, int limit) { - int depth = 1; - for (int i = start; i < limit; i++) { - char current = source.charAt(i); - if (current == '\\'' || current == '"') { - i = skipQuotedLiteral(source, i, limit, current); - continue; - } - if (current == '`') { - i = skipTemplateLiteral(source, i, limit); - continue; - } - if (current == '/' && canStartRegex(source, i)) { - int regexEnd = skipRegexLiteral(source, i, limit); - if (regexEnd > i) { - i = regexEnd; - continue; - } - } - if (current == '{') { - depth++; - } else if (current == '}') { - depth--; - if (depth == 0) { - return i; - } - } - } - return -1; - } - - private static int skipQuotedLiteral(String source, int start, int limit, char quote) { - boolean escaped = false; - for (int i = start + 1; i < limit; i++) { - char current = source.charAt(i); - if (escaped) { - escaped = false; - } else if (current == '\\\\') { - escaped = true; - } else if (current == quote) { - return i; - } - } - return limit - 1; - } - - private static int skipTemplateLiteral(String source, int start, int limit) { - boolean escaped = false; - for (int i = start + 1; i < limit; i++) { - char current = source.charAt(i); - if (escaped) { - escaped = false; - continue; - } - if (current == '\\\\') { - escaped = true; - continue; - } - if (current == '$' && i + 1 < limit && source.charAt(i + 1) == '{') { - int expressionEnd = findTemplateExpressionEnd(source, i + 2, limit); - if (expressionEnd >= 0) { - i = expressionEnd; - continue; - } - } - if (current == '`') { - return i; - } - } - return limit - 1; - } - - private static int skipRegexLiteral(String source, int start, int limit) { - boolean escaped = false; - boolean characterClass = false; - for (int i = start + 1; i < limit; i++) { - char current = source.charAt(i); - if (current == '\\r' || current == '\\n') { - return start; - } - if (escaped) { - escaped = false; - continue; - } - if (current == '\\\\') { - escaped = true; - continue; - } - if (current == '[') { - characterClass = true; - } else if (current == ']') { - characterClass = false; - } else if (current == '/' && !characterClass) { - int end = i; - while (end + 1 < limit && "dgimsuvy".indexOf(source.charAt(end + 1)) >= 0) { - end++; - } - return end; - } - } - return start; - } -''' -if text.count(old) != 1: - raise SystemExit('template expression block not found') -text = text.replace(old, new, 1) - -marker = ''' private boolean overlaps(List ranges, Range candidate) { - for (Range range : ranges) { - if (candidate.start < range.end && range.start < candidate.end) { - return true; - } - } - return false; - } -''' -replacement = marker + '''\n private void removeContained(List ranges, Range container) { - ranges.removeIf(range -> range.start >= container.start && range.end <= container.end); - } -''' -if text.count(marker) != 1: - raise SystemExit('overlaps marker not found') -text = text.replace(marker, replacement, 1) - -p.write_text(text) diff --git a/.github/workflows/run-fix302-latest-fallback.yml b/.github/workflows/run-fix302-latest-fallback.yml deleted file mode 100644 index d185e4e7c..000000000 --- a/.github/workflows/run-fix302-latest-fallback.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Run latest JavaScript fallback fixes -on: - push: - branches: - - security/javascript-placeholder-bindings -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply latest fallback fixes - run: python3 .github/scripts/fix302_latest_fallback.py - - name: Commit fallback fixes - run: | - rm -f .github/scripts/fix302_latest_fallback.py .github/workflows/run-fix302-latest-fallback.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Harden template and regex fallback scanning' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index af621d45a..685c191d4 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -399,7 +399,12 @@ private static void addFallbackRegexRanges(String source, JavascriptContexts con end++; } Range candidate = new Range(i, end); - if (!contexts.overlapsLiteral(candidate)) { + // A regex literal may legitimately contain quote characters. Since the + // opening slash was already proven to be outside a string/template and + // can start a regex, discard fallback string ranges fully contained by + // this regex instead of letting quote-looking regex text win. + contexts.removeContained(contexts.strings, candidate); + if (!contexts.overlaps(contexts.templates, candidate)) { contexts.regexes.add(candidate); } i = end - 1; @@ -451,8 +456,6 @@ private static void addPatternRanges(String source, Pattern pattern, List private static void addFallbackTemplateExpressions(String source, Range template, List target) { boolean escaped = false; - int expressionStart = -1; - int depth = 0; for (int i = template.start + 1; i < template.end - 1; i++) { char current = source.charAt(i); if (escaped) { @@ -463,24 +466,118 @@ private static void addFallbackTemplateExpressions(String source, Range template escaped = true; continue; } - if (expressionStart < 0) { - if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { - expressionStart = i + 2; - depth = 1; - i++; + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + int expressionStart = i + 2; + int expressionEnd = findTemplateExpressionEnd(source, expressionStart, template.end - 1); + if (expressionEnd >= 0) { + target.add(new Range(expressionStart, expressionEnd)); + i = expressionEnd; } + } + } + } + + private static int findTemplateExpressionEnd(String source, int start, int limit) { + int depth = 1; + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, limit); continue; } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } if (current == '{') { depth++; } else if (current == '}') { depth--; if (depth == 0) { - target.add(new Range(expressionStart, i)); - expressionStart = -1; + return i; } } } + return -1; + } + + private static int skipQuotedLiteral(String source, int start, int limit, char quote) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + } else if (current == '\\') { + escaped = true; + } else if (current == quote) { + return i; + } + } + return limit - 1; + } + + private static int skipTemplateLiteral(String source, int start, int limit) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '$' && i + 1 < limit && source.charAt(i + 1) == '{') { + int expressionEnd = findTemplateExpressionEnd(source, i + 2, limit); + if (expressionEnd >= 0) { + i = expressionEnd; + continue; + } + } + if (current == '`') { + return i; + } + } + return limit - 1; + } + + private static int skipRegexLiteral(String source, int start, int limit) { + boolean escaped = false; + boolean characterClass = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (current == '\r' || current == '\n') { + return start; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + } else if (current == ']') { + characterClass = false; + } else if (current == '/' && !characterClass) { + int end = i; + while (end + 1 < limit && "dgimsuvy".indexOf(source.charAt(end + 1)) >= 0) { + end++; + } + return end; + } + } + return start; } private boolean overlapsLiteral(Range candidate) { @@ -496,6 +593,10 @@ private boolean overlaps(List ranges, Range candidate) { return false; } + private void removeContained(List ranges, Range container) { + ranges.removeIf(range -> range.start >= container.start && range.end <= container.end); + } + private static Object createParser(Class parserClass) throws ReflectiveOperationException { for (Method method : parserClass.getMethods()) { if (!method.getName().equals("create") || !Modifier.isStatic(method.getModifiers())) { From 36d75a5fd947828f380b816e76591cf8ca99b2ea Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 16:57:59 -0600 Subject: [PATCH 48/63] Test nested template and quoted regex fallback contexts --- ...ptPlaceholderModernSyntaxFallbackTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index 06fa9b383..7146d4c7b 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -121,4 +121,27 @@ void divisionOperatorsDoNotBecomeFallbackRegexRanges() { assertEquals(1, bindings.size()); assertEquals(2.5D, bindings.get("__advancedCorePlaceholder0")); } + + @Test + void quotedClosingBraceInsideTemplateExpressionDoesNotEndExpression() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `${\"}\" + %name%}`", + ignored -> "Bukkit.dispatchCommand(Console, 'op attacker')", bindings::put); + + assertEquals("obj?.name; `${\"}\" + __advancedCorePlaceholder0}`", prepared); + assertEquals(1, bindings.size()); + assertEquals("Bukkit.dispatchCommand(Console, 'op attacker')", bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void quoteCharactersInsideRegexStillUseRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; /^'%name%'$/", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name; /^'Ben\\.\\*'$/", prepared); + assertTrue(bindings.isEmpty()); + } } From a98d63bf4b1d56dd4c99fe9c0e27f206df427643 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:19:39 -0600 Subject: [PATCH 49/63] Prepare Codex round six fixes --- .github/fix302-round6.py | 190 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 .github/fix302-round6.py diff --git a/.github/fix302-round6.py b/.github/fix302-round6.py new file mode 100644 index 000000000..f9d08046c --- /dev/null +++ b/.github/fix302-round6.py @@ -0,0 +1,190 @@ +from pathlib import Path + +p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = p.read_text() + +old = ''' if (placeholders != null) { + String name = token.substring(1, token.length() - 1); + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + } + return token; + } +''' +new = ''' if (placeholders != null) { + String name = token.substring(1, token.length() - 1); + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + String value = entry.getValue(); + if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, value); + if (resolved != null) { + value = resolved; + } + } + return value; + } + } + } + return token; + } +''' +if text.count(old) != 1: + raise SystemExit('custom placeholder resolve block not found') +text = text.replace(old, new, 1) + +old = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + + Matcher templates = FALLBACK_TEMPLATE.matcher(source); + while (templates.find()) { + Range template = new Range(templates.start(), templates.end()); + contexts.templates.add(template); + addFallbackTemplateExpressions(source, template, contexts.templateExpressions); + } + + // Do not classify quote-looking text inside a template as a string literal. + addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } +''' +new = ''' private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + + addFallbackTemplateRanges(source, 0, source.length(), contexts); + + // Quote-looking text is a string only outside template text. Strings inside + // ${...} remain ordinary JavaScript strings and are tracked normally. + addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(source, contexts); + contexts.sort(); + return contexts; + } + + private static void addFallbackTemplateRanges(String source, int start, int limit, + JavascriptContexts contexts) { + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\\'' || current == '\"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current != '`') { + continue; + } + + int templateEnd = skipTemplateLiteral(source, i, limit); + if (templateEnd <= i || templateEnd >= source.length() || source.charAt(templateEnd) != '`') { + continue; + } + + Range template = new Range(i, templateEnd + 1); + contexts.templates.add(template); + List expressions = new ArrayList<>(); + addFallbackTemplateExpressions(source, template, expressions); + contexts.templateExpressions.addAll(expressions); + + // Nested templates live inside an outer ${...}. Scan each interpolation + // recursively so their text ranges override the enclosing expression. + for (Range expression : expressions) { + addFallbackTemplateRanges(source, expression.start, expression.end, contexts); + } + i = templateEnd; + } + } +''' +if text.count(old) != 1: + raise SystemExit('fallback template block not found') +text = text.replace(old, new, 1) + +old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { + continue; + } +''' +new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.isTemplateText(i) || !canStartRegex(source, i)) { + continue; + } +''' +if text.count(old) != 1: + raise SystemExit('fallback regex start block not found') +text = text.replace(old, new, 1) + +old = ''' contexts.removeContained(contexts.strings, candidate); + if (!contexts.overlaps(contexts.templates, candidate)) { + contexts.regexes.add(candidate); + } + i = end - 1; +''' +new = ''' contexts.removeContained(contexts.strings, candidate); + contexts.regexes.add(candidate); + i = end - 1; +''' +if text.count(old) != 1: + raise SystemExit('fallback regex add block not found') +text = text.replace(old, new, 1) + +old = ''' Range candidate = new Range(matcher.start(), matcher.end()); + if (existing == null || !existing.overlapsLiteral(candidate)) { + target.add(candidate); + } +''' +new = ''' Range candidate = new Range(matcher.start(), matcher.end()); + if (existing == null || !existing.isTemplateText(candidate.start)) { + target.add(candidate); + } +''' +if text.count(old) != 1: + raise SystemExit('pattern range block not found') +text = text.replace(old, new, 1) + +old = ''' private boolean insideTemplateExpression(int position) { + return containing(templateExpressions, position) != null; + } +''' +new = ''' private boolean insideTemplateExpression(int position) { + Range expression = innermostContaining(templateExpressions, position); + if (expression == null) { + return false; + } + Range template = innermostContaining(templates, position); + // An enclosing template expression must not override the text context of a + // nested template literal that starts later inside that expression. + return template == null || expression.start > template.start; + } + + private boolean isTemplateText(int position) { + return innermostContaining(templates, position) != null && !insideTemplateExpression(position); + } + + private Range innermostContaining(List ranges, int position) { + Range best = null; + for (Range range : ranges) { + if (!range.contains(position)) { + continue; + } + if (best == null || (range.end - range.start) < (best.end - best.start)) { + best = range; + } + } + return best; + } +''' +if text.count(old) != 1: + raise SystemExit('insideTemplateExpression block not found') +text = text.replace(old, new, 1) + +p.write_text(text) From d58b77e7e74007a630c57d83e3115fec19ef8dfa Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:19:48 -0600 Subject: [PATCH 50/63] Run Codex round six fixes --- .github/workflows/run-fix302-round6.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/run-fix302-round6.yml diff --git a/.github/workflows/run-fix302-round6.yml b/.github/workflows/run-fix302-round6.yml new file mode 100644 index 000000000..b3859e1f8 --- /dev/null +++ b/.github/workflows/run-fix302-round6.yml @@ -0,0 +1,25 @@ +name: Run Codex round six fixes +on: + push: + branches: + - security/javascript-placeholder-bindings +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply fixes + run: python3 .github/fix302-round6.py + - name: Commit fixes + run: | + rm -f .github/fix302-round6.py .github/workflows/run-fix302-round6.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Fix nested templates and PAPI chaining' + git push origin HEAD:security/javascript-placeholder-bindings From 8fda25ca114114380f47557f01160a8b7fefe846 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 23:19:56 +0000 Subject: [PATCH 51/63] Fix nested templates and PAPI chaining --- .github/fix302-round6.py | 190 ------------------ .github/workflows/run-fix302-round6.yml | 25 --- .../JavascriptPlaceholderBinder.java | 92 +++++++-- 3 files changed, 78 insertions(+), 229 deletions(-) delete mode 100644 .github/fix302-round6.py delete mode 100644 .github/workflows/run-fix302-round6.yml diff --git a/.github/fix302-round6.py b/.github/fix302-round6.py deleted file mode 100644 index f9d08046c..000000000 --- a/.github/fix302-round6.py +++ /dev/null @@ -1,190 +0,0 @@ -from pathlib import Path - -p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = p.read_text() - -old = ''' if (placeholders != null) { - String name = token.substring(1, token.length() - 1); - for (Entry entry : placeholders.entrySet()) { - if (entry.getKey().equalsIgnoreCase(name)) { - return entry.getValue(); - } - } - } - return token; - } -''' -new = ''' if (placeholders != null) { - String name = token.substring(1, token.length() - 1); - for (Entry entry : placeholders.entrySet()) { - if (entry.getKey().equalsIgnoreCase(name)) { - String value = entry.getValue(); - if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, value); - if (resolved != null) { - value = resolved; - } - } - return value; - } - } - } - return token; - } -''' -if text.count(old) != 1: - raise SystemExit('custom placeholder resolve block not found') -text = text.replace(old, new, 1) - -old = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } - - // Do not classify quote-looking text inside a template as a string literal. - addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } -''' -new = ''' private static JavascriptContexts fallback(String source) { - JavascriptContexts contexts = new JavascriptContexts(); - - addFallbackTemplateRanges(source, 0, source.length(), contexts); - - // Quote-looking text is a string only outside template text. Strings inside - // ${...} remain ordinary JavaScript strings and are tracked normally. - addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); - addFallbackRegexRanges(source, contexts); - contexts.sort(); - return contexts; - } - - private static void addFallbackTemplateRanges(String source, int start, int limit, - JavascriptContexts contexts) { - for (int i = start; i < limit; i++) { - char current = source.charAt(i); - if (current == '\\'' || current == '\"') { - i = skipQuotedLiteral(source, i, limit, current); - continue; - } - if (current == '/' && canStartRegex(source, i)) { - int regexEnd = skipRegexLiteral(source, i, limit); - if (regexEnd > i) { - i = regexEnd; - continue; - } - } - if (current != '`') { - continue; - } - - int templateEnd = skipTemplateLiteral(source, i, limit); - if (templateEnd <= i || templateEnd >= source.length() || source.charAt(templateEnd) != '`') { - continue; - } - - Range template = new Range(i, templateEnd + 1); - contexts.templates.add(template); - List expressions = new ArrayList<>(); - addFallbackTemplateExpressions(source, template, expressions); - contexts.templateExpressions.addAll(expressions); - - // Nested templates live inside an outer ${...}. Scan each interpolation - // recursively so their text ranges override the enclosing expression. - for (Range expression : expressions) { - addFallbackTemplateRanges(source, expression.start, expression.end, contexts); - } - i = templateEnd; - } - } -''' -if text.count(old) != 1: - raise SystemExit('fallback template block not found') -text = text.replace(old, new, 1) - -old = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { - continue; - } -''' -new = ''' if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.isTemplateText(i) || !canStartRegex(source, i)) { - continue; - } -''' -if text.count(old) != 1: - raise SystemExit('fallback regex start block not found') -text = text.replace(old, new, 1) - -old = ''' contexts.removeContained(contexts.strings, candidate); - if (!contexts.overlaps(contexts.templates, candidate)) { - contexts.regexes.add(candidate); - } - i = end - 1; -''' -new = ''' contexts.removeContained(contexts.strings, candidate); - contexts.regexes.add(candidate); - i = end - 1; -''' -if text.count(old) != 1: - raise SystemExit('fallback regex add block not found') -text = text.replace(old, new, 1) - -old = ''' Range candidate = new Range(matcher.start(), matcher.end()); - if (existing == null || !existing.overlapsLiteral(candidate)) { - target.add(candidate); - } -''' -new = ''' Range candidate = new Range(matcher.start(), matcher.end()); - if (existing == null || !existing.isTemplateText(candidate.start)) { - target.add(candidate); - } -''' -if text.count(old) != 1: - raise SystemExit('pattern range block not found') -text = text.replace(old, new, 1) - -old = ''' private boolean insideTemplateExpression(int position) { - return containing(templateExpressions, position) != null; - } -''' -new = ''' private boolean insideTemplateExpression(int position) { - Range expression = innermostContaining(templateExpressions, position); - if (expression == null) { - return false; - } - Range template = innermostContaining(templates, position); - // An enclosing template expression must not override the text context of a - // nested template literal that starts later inside that expression. - return template == null || expression.start > template.start; - } - - private boolean isTemplateText(int position) { - return innermostContaining(templates, position) != null && !insideTemplateExpression(position); - } - - private Range innermostContaining(List ranges, int position) { - Range best = null; - for (Range range : ranges) { - if (!range.contains(position)) { - continue; - } - if (best == null || (range.end - range.start) < (best.end - best.start)) { - best = range; - } - } - return best; - } -''' -if text.count(old) != 1: - raise SystemExit('insideTemplateExpression block not found') -text = text.replace(old, new, 1) - -p.write_text(text) diff --git a/.github/workflows/run-fix302-round6.yml b/.github/workflows/run-fix302-round6.yml deleted file mode 100644 index b3859e1f8..000000000 --- a/.github/workflows/run-fix302-round6.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Run Codex round six fixes -on: - push: - branches: - - security/javascript-placeholder-bindings -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply fixes - run: python3 .github/fix302-round6.py - - name: Commit fixes - run: | - rm -f .github/fix302-round6.py .github/workflows/run-fix302-round6.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Fix nested templates and PAPI chaining' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 685c191d4..7c11219bd 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -145,7 +145,14 @@ private static String resolve(String token, OfflinePlayer player, Map entry : placeholders.entrySet()) { if (entry.getKey().equalsIgnoreCase(name)) { - return entry.getValue(); + String value = entry.getValue(); + if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, value); + if (resolved != null) { + value = resolved; + } + } + return value; } } } @@ -346,24 +353,59 @@ private static JavascriptContexts parse(String source) { private static JavascriptContexts fallback(String source) { JavascriptContexts contexts = new JavascriptContexts(); - Matcher templates = FALLBACK_TEMPLATE.matcher(source); - while (templates.find()) { - Range template = new Range(templates.start(), templates.end()); - contexts.templates.add(template); - addFallbackTemplateExpressions(source, template, contexts.templateExpressions); - } + addFallbackTemplateRanges(source, 0, source.length(), contexts); - // Do not classify quote-looking text inside a template as a string literal. + // Quote-looking text is a string only outside template text. Strings inside + // ${...} remain ordinary JavaScript strings and are tracked normally. addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); addFallbackRegexRanges(source, contexts); contexts.sort(); return contexts; } + private static void addFallbackTemplateRanges(String source, int start, int limit, + JavascriptContexts contexts) { + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current != '`') { + continue; + } + + int templateEnd = skipTemplateLiteral(source, i, limit); + if (templateEnd <= i || templateEnd >= source.length() || source.charAt(templateEnd) != '`') { + continue; + } + + Range template = new Range(i, templateEnd + 1); + contexts.templates.add(template); + List expressions = new ArrayList<>(); + addFallbackTemplateExpressions(source, template, expressions); + contexts.templateExpressions.addAll(expressions); + + // Nested templates live inside an outer ${...}. Scan each interpolation + // recursively so their text ranges override the enclosing expression. + for (Range expression : expressions) { + addFallbackTemplateRanges(source, expression.start, expression.end, contexts); + } + i = templateEnd; + } + } + private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { for (int i = 0; i < source.length(); i++) { if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null - || contexts.containing(contexts.templates, i) != null || !canStartRegex(source, i)) { + || contexts.isTemplateText(i) || !canStartRegex(source, i)) { continue; } @@ -404,9 +446,7 @@ private static void addFallbackRegexRanges(String source, JavascriptContexts con // can start a regex, discard fallback string ranges fully contained by // this regex instead of letting quote-looking regex text win. contexts.removeContained(contexts.strings, candidate); - if (!contexts.overlaps(contexts.templates, candidate)) { - contexts.regexes.add(candidate); - } + contexts.regexes.add(candidate); i = end - 1; break; } @@ -448,7 +488,7 @@ private static void addPatternRanges(String source, Pattern pattern, List Matcher matcher = pattern.matcher(source); while (matcher.find()) { Range candidate = new Range(matcher.start(), matcher.end()); - if (existing == null || !existing.overlapsLiteral(candidate)) { + if (existing == null || !existing.isTemplateText(candidate.start)) { target.add(candidate); } } @@ -775,7 +815,31 @@ private Range containing(List ranges, int position) { } private boolean insideTemplateExpression(int position) { - return containing(templateExpressions, position) != null; + Range expression = innermostContaining(templateExpressions, position); + if (expression == null) { + return false; + } + Range template = innermostContaining(templates, position); + // An enclosing template expression must not override the text context of a + // nested template literal that starts later inside that expression. + return template == null || expression.start > template.start; + } + + private boolean isTemplateText(int position) { + return innermostContaining(templates, position) != null && !insideTemplateExpression(position); + } + + private Range innermostContaining(List ranges, int position) { + Range best = null; + for (Range range : ranges) { + if (!range.contains(position)) { + continue; + } + if (best == null || (range.end - range.start) < (best.end - best.start)) { + best = range; + } + } + return best; } private void sort() { From adf589dc7c6644f61b33b37f329a0253b07dc531 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:20:57 -0600 Subject: [PATCH 52/63] Test nested fallback templates --- ...JavascriptPlaceholderModernSyntaxFallbackTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index 7146d4c7b..f77b77de2 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -144,4 +144,15 @@ void quoteCharactersInsideRegexStillUseRegexEscaping() { assertEquals("obj?.name; /^'Ben\\.\\*'$/", prepared); assertTrue(bindings.isEmpty()); } + + @Test + void nestedTemplateLiteralTextRemainsDataInFallback() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `${`inner %name%`}`", + ignored -> "${attack()}", bindings::put); + + assertEquals("obj?.name; `${`inner \\${attack()}`}`", prepared); + assertTrue(bindings.isEmpty()); + } } From 04f9f6e5e19cd74286053b734913480a785cf7f3 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:21:23 -0600 Subject: [PATCH 53/63] Test custom placeholder PAPI chaining --- .../JavascriptPlaceholderBinderTest.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java index 8cd0b4892..138578e21 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -3,11 +3,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; import java.util.HashMap; +import org.bukkit.OfflinePlayer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.tests.BaseTest; + +import me.clip.placeholderapi.PlaceholderAPI; class JavascriptPlaceholderBinderTest { @@ -137,7 +146,6 @@ void preservesPrimitiveTypesForExpressionPlaceholders() { assertEquals(Double.valueOf(1.5), bindings.get("__advancedCorePlaceholder2")); } - @Test void exactQuotedNumericLookingPlaceholderRemainsAString() { HashMap bindings = new HashMap<>(); @@ -203,6 +211,25 @@ void parserFailurePreservesRegexPlaceholderUnderModernSyntax() { assertTrue(bindings.isEmpty()); } + @Test + void customPlaceholderValueStillExpandsPlaceholderApiTokens() { + BaseTest base = BaseTest.getInstance(); + when(base.plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + OfflinePlayer player = mock(OfflinePlayer.class); + HashMap placeholders = new HashMap<>(); + placeholders.put("alias", "%player_name%"); + + try (MockedStatic placeholderApi = mockStatic(PlaceholderAPI.class)) { + placeholderApi.when(() -> PlaceholderAPI.setPlaceholders(player, "%alias%")).thenReturn("%alias%"); + placeholderApi.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")).thenReturn("Ben"); + + String prepared = JavascriptPlaceholderBinder.bind("'%alias%' == 'Ben'", player, placeholders, + new JavascriptEngine()); + + assertEquals("'Ben' == 'Ben'", prepared); + } + } + @Test void unresolvedTokensRemainUntouched() { HashMap bindings = new HashMap<>(); From 699254ce4eb7505cf71ca3e13a4ae1307fe36b09 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:33:01 -0600 Subject: [PATCH 54/63] Prepare Codex round seven fixes --- .github/scripts/fix302-round7.py | 149 +++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/scripts/fix302-round7.py diff --git a/.github/scripts/fix302-round7.py b/.github/scripts/fix302-round7.py new file mode 100644 index 000000000..d3b6b4acf --- /dev/null +++ b/.github/scripts/fix302-round7.py @@ -0,0 +1,149 @@ +from pathlib import Path + +path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') +text = path.read_text() + +resolve_start = text.index(' private static String resolve(String token, OfflinePlayer player, Map placeholders) {') +resolve_end = text.index(' private static Object coerce(String value) {', resolve_start) +resolve_method = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + + // Preserve the historical replacement order: AdvancedCore custom/reward + // placeholders win name collisions, then PlaceholderAPI is applied to the + // selected custom value so custom placeholders may themselves contain PAPI. + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + String value = entry.getValue(); + if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, value); + if (resolved != null) { + value = resolved; + } + } + return value; + } + } + } + + // Only consult PlaceholderAPI for the original token when no custom + // placeholder with the same name was supplied. + if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + return token; + } + +''' +text = text[:resolve_start] + resolve_method + text[resolve_end:] + +regex_start = text.index(' private static boolean canStartRegex(String source, int slashIndex) {') +regex_end = text.index(' private static void addPatternRanges(String source, Pattern pattern, List target,', regex_start) +regex_helpers = ''' private static boolean canStartRegex(String source, int slashIndex) { + int previousIndex = slashIndex - 1; + while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { + previousIndex--; + } + if (previousIndex < 0) { + return true; + } + + char previous = source.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + if (previous == ')' && closesControlStatementHead(source, previousIndex)) { + return true; + } + + if (Character.isJavaIdentifierPart(previous)) { + int end = previousIndex + 1; + int start = previousIndex; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end); + return word.equals("return") || word.equals("case") || word.equals("throw") + || word.equals("else") || word.equals("do") || word.equals("yield") + || word.equals("await") || word.equals("typeof") || word.equals("void") + || word.equals("delete") || word.equals("instanceof") || word.equals("in") + || word.equals("new"); + } + return false; + } + + private static boolean closesControlStatementHead(String source, int closeParen) { + List openingParens = new ArrayList<>(); + for (int i = 0; i <= closeParen; i++) { + char current = source.charAt(i); + if (current == '\\'' || current == '"') { + i = skipQuotedLiteral(source, i, closeParen + 1, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, closeParen + 1); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, closeParen + 1); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current == '(') { + openingParens.add(i); + } else if (current == ')') { + if (openingParens.isEmpty()) { + return false; + } + int openingParen = openingParens.remove(openingParens.size() - 1); + if (i == closeParen) { + return isControlKeywordBefore(source, openingParen); + } + } + } + return false; + } + + private static boolean isControlKeywordBefore(String source, int openingParen) { + int end = openingParen - 1; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + if (end < 0 || !Character.isJavaIdentifierPart(source.charAt(end))) { + return false; + } + + int start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end + 1); + if (word.equals("if") || word.equals("while") || word.equals("for") || word.equals("with") + || word.equals("switch") || word.equals("catch")) { + return true; + } + + // Modern JavaScript may use `for await (...)`. + if (!word.equals("await")) { + return false; + } + end = start; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + return end >= 0 && source.substring(start + 1, end + 1).equals("for"); + } + +''' +text = text[:regex_start] + regex_helpers + text[regex_end:] +path.write_text(text) From ee58d1017390dc480a438ea07f7aa717933b66ee Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:33:11 -0600 Subject: [PATCH 55/63] Run Codex round seven fixes --- .github/workflows/run-fix302-round7.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/run-fix302-round7.yml diff --git a/.github/workflows/run-fix302-round7.yml b/.github/workflows/run-fix302-round7.yml new file mode 100644 index 000000000..632c56310 --- /dev/null +++ b/.github/workflows/run-fix302-round7.yml @@ -0,0 +1,25 @@ +name: Run Codex round seven fixes +on: + push: + branches: + - security/javascript-placeholder-bindings +permissions: + contents: write +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + - name: Apply fixes + run: python3 .github/scripts/fix302-round7.py + - name: Commit fixes + run: | + rm -f .github/scripts/fix302-round7.py .github/workflows/run-fix302-round7.yml + git config user.name 'Ben' + git config user.email 'benbergen12@gmail.com' + git add -A + git commit -m 'Preserve JavaScript placeholder precedence and control regex contexts' + git push origin HEAD:security/javascript-placeholder-bindings From 1ee8653faf543750a573fb0d4360416bb095b500 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 23:33:20 +0000 Subject: [PATCH 56/63] Preserve JavaScript placeholder precedence and control regex contexts --- .github/scripts/fix302-round7.py | 149 ------------------ .github/workflows/run-fix302-round7.yml | 25 --- .../JavascriptPlaceholderBinder.java | 91 ++++++++++- 3 files changed, 83 insertions(+), 182 deletions(-) delete mode 100644 .github/scripts/fix302-round7.py delete mode 100644 .github/workflows/run-fix302-round7.yml diff --git a/.github/scripts/fix302-round7.py b/.github/scripts/fix302-round7.py deleted file mode 100644 index d3b6b4acf..000000000 --- a/.github/scripts/fix302-round7.py +++ /dev/null @@ -1,149 +0,0 @@ -from pathlib import Path - -path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java') -text = path.read_text() - -resolve_start = text.index(' private static String resolve(String token, OfflinePlayer player, Map placeholders) {') -resolve_end = text.index(' private static Object coerce(String value) {', resolve_start) -resolve_method = ''' private static String resolve(String token, OfflinePlayer player, Map placeholders) { - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - - // Preserve the historical replacement order: AdvancedCore custom/reward - // placeholders win name collisions, then PlaceholderAPI is applied to the - // selected custom value so custom placeholders may themselves contain PAPI. - if (placeholders != null) { - String name = token.substring(1, token.length() - 1); - for (Entry entry : placeholders.entrySet()) { - if (entry.getKey().equalsIgnoreCase(name)) { - String value = entry.getValue(); - if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, value); - if (resolved != null) { - value = resolved; - } - } - return value; - } - } - } - - // Only consult PlaceholderAPI for the original token when no custom - // placeholder with the same name was supplied. - if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } - return token; - } - -''' -text = text[:resolve_start] + resolve_method + text[resolve_end:] - -regex_start = text.index(' private static boolean canStartRegex(String source, int slashIndex) {') -regex_end = text.index(' private static void addPatternRanges(String source, Pattern pattern, List target,', regex_start) -regex_helpers = ''' private static boolean canStartRegex(String source, int slashIndex) { - int previousIndex = slashIndex - 1; - while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { - previousIndex--; - } - if (previousIndex < 0) { - return true; - } - - char previous = source.charAt(previousIndex); - if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { - return true; - } - if (previous == ')' && closesControlStatementHead(source, previousIndex)) { - return true; - } - - if (Character.isJavaIdentifierPart(previous)) { - int end = previousIndex + 1; - int start = previousIndex; - while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { - start--; - } - String word = source.substring(start + 1, end); - return word.equals("return") || word.equals("case") || word.equals("throw") - || word.equals("else") || word.equals("do") || word.equals("yield") - || word.equals("await") || word.equals("typeof") || word.equals("void") - || word.equals("delete") || word.equals("instanceof") || word.equals("in") - || word.equals("new"); - } - return false; - } - - private static boolean closesControlStatementHead(String source, int closeParen) { - List openingParens = new ArrayList<>(); - for (int i = 0; i <= closeParen; i++) { - char current = source.charAt(i); - if (current == '\\'' || current == '"') { - i = skipQuotedLiteral(source, i, closeParen + 1, current); - continue; - } - if (current == '`') { - i = skipTemplateLiteral(source, i, closeParen + 1); - continue; - } - if (current == '/' && canStartRegex(source, i)) { - int regexEnd = skipRegexLiteral(source, i, closeParen + 1); - if (regexEnd > i) { - i = regexEnd; - continue; - } - } - if (current == '(') { - openingParens.add(i); - } else if (current == ')') { - if (openingParens.isEmpty()) { - return false; - } - int openingParen = openingParens.remove(openingParens.size() - 1); - if (i == closeParen) { - return isControlKeywordBefore(source, openingParen); - } - } - } - return false; - } - - private static boolean isControlKeywordBefore(String source, int openingParen) { - int end = openingParen - 1; - while (end >= 0 && Character.isWhitespace(source.charAt(end))) { - end--; - } - if (end < 0 || !Character.isJavaIdentifierPart(source.charAt(end))) { - return false; - } - - int start = end; - while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { - start--; - } - String word = source.substring(start + 1, end + 1); - if (word.equals("if") || word.equals("while") || word.equals("for") || word.equals("with") - || word.equals("switch") || word.equals("catch")) { - return true; - } - - // Modern JavaScript may use `for await (...)`. - if (!word.equals("await")) { - return false; - } - end = start; - while (end >= 0 && Character.isWhitespace(source.charAt(end))) { - end--; - } - start = end; - while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { - start--; - } - return end >= 0 && source.substring(start + 1, end + 1).equals("for"); - } - -''' -text = text[:regex_start] + regex_helpers + text[regex_end:] -path.write_text(text) diff --git a/.github/workflows/run-fix302-round7.yml b/.github/workflows/run-fix302-round7.yml deleted file mode 100644 index 632c56310..000000000 --- a/.github/workflows/run-fix302-round7.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Run Codex round seven fixes -on: - push: - branches: - - security/javascript-placeholder-bindings -permissions: - contents: write -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - - name: Apply fixes - run: python3 .github/scripts/fix302-round7.py - - name: Commit fixes - run: | - rm -f .github/scripts/fix302-round7.py .github/workflows/run-fix302-round7.yml - git config user.name 'Ben' - git config user.email 'benbergen12@gmail.com' - git add -A - git commit -m 'Preserve JavaScript placeholder precedence and control regex contexts' - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 7c11219bd..8f0e226b6 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -132,15 +132,10 @@ static String bind(String expression, Function resolver, BiConsu private static String resolve(String token, OfflinePlayer player, Map placeholders) { AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - // PlaceholderAPI uses percent-delimited placeholders. Brace-delimited tokens - // are AdvancedCore's legacy custom placeholder form and are resolved below. - if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, token); - if (resolved != null && !resolved.equals(token)) { - return resolved; - } - } + // Preserve the historical replacement order: AdvancedCore custom/reward + // placeholders win name collisions, then PlaceholderAPI is applied to the + // selected custom value so custom placeholders may themselves contain PAPI. if (placeholders != null) { String name = token.substring(1, token.length() - 1); for (Entry entry : placeholders.entrySet()) { @@ -156,6 +151,15 @@ private static String resolve(String token, OfflinePlayer player, Map".indexOf(previous) >= 0) { return true; } + if (previous == ')' && closesControlStatementHead(source, previousIndex)) { + return true; + } if (Character.isJavaIdentifierPart(previous)) { int end = previousIndex + 1; @@ -483,6 +490,74 @@ private static boolean canStartRegex(String source, int slashIndex) { return false; } + private static boolean closesControlStatementHead(String source, int closeParen) { + List openingParens = new ArrayList<>(); + for (int i = 0; i <= closeParen; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, closeParen + 1, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, closeParen + 1); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, closeParen + 1); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current == '(') { + openingParens.add(i); + } else if (current == ')') { + if (openingParens.isEmpty()) { + return false; + } + int openingParen = openingParens.remove(openingParens.size() - 1); + if (i == closeParen) { + return isControlKeywordBefore(source, openingParen); + } + } + } + return false; + } + + private static boolean isControlKeywordBefore(String source, int openingParen) { + int end = openingParen - 1; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + if (end < 0 || !Character.isJavaIdentifierPart(source.charAt(end))) { + return false; + } + + int start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end + 1); + if (word.equals("if") || word.equals("while") || word.equals("for") || word.equals("with") + || word.equals("switch") || word.equals("catch")) { + return true; + } + + // Modern JavaScript may use `for await (...)`. + if (!word.equals("await")) { + return false; + } + end = start; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + return end >= 0 && source.substring(start + 1, end + 1).equals("for"); + } + private static void addPatternRanges(String source, Pattern pattern, List target, JavascriptContexts existing) { Matcher matcher = pattern.matcher(source); From 0bd092c73e204335aad36bce3fc07feaf4007012 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:33:49 -0600 Subject: [PATCH 57/63] Test custom placeholder precedence over PAPI --- ...riptPlaceholderResolutionPriorityTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java new file mode 100644 index 000000000..78954af8b --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java @@ -0,0 +1,46 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.bukkit.OfflinePlayer; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.AdvancedCorePlugin; + +import me.clip.placeholderapi.PlaceholderAPI; + +class JavascriptPlaceholderResolutionPriorityTest { + + @Test + void customPlaceholderWinsSameNamedPapiTokenThenExpandsPapiInsideCustomValue() { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + OfflinePlayer player = mock(OfflinePlayer.class); + JavascriptEngine engine = new JavascriptEngine(); + + when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + + try (MockedStatic pluginStatic = mockStatic(AdvancedCorePlugin.class); + MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { + pluginStatic.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%reward_alias%")) + .thenReturn("CustomName"); + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")) + .thenReturn("PapiName"); + + String prepared = JavascriptPlaceholderBinder.bind("'%player_name%' == 'CustomName'", player, + Map.of("player_name", "%reward_alias%"), engine); + + assertEquals("'CustomName' == 'CustomName'", prepared); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%reward_alias%")); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"), never()); + } + } +} From 5051d908035926e90b78ab09a5653b45a2a01b53 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:34:01 -0600 Subject: [PATCH 58/63] Test regex literals after control statement heads --- ...ptPlaceholderControlRegexFallbackTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java new file mode 100644 index 000000000..7bb81e30f --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java @@ -0,0 +1,40 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderControlRegexFallbackTest { + + @BeforeEach + void forceFallbackCompatibleSetup() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + + @Test + void regexAfterIfControlHeadUsesRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("if (obj?.name) /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("if (obj?.name) /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void regexAfterNestedWhileControlHeadUsesRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("while ((obj?.name)) /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("while ((obj?.name)) /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } +} From 26429e5ca21d3aeca77d35fc2d8cbd16f02808db Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 17:35:22 -0600 Subject: [PATCH 59/63] Fix placeholder precedence test harness --- .../JavascriptPlaceholderResolutionPriorityTest.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java index 78954af8b..058519a05 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java @@ -4,7 +4,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Map; @@ -14,6 +13,7 @@ import org.mockito.MockedStatic; import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.tests.BaseTest; import me.clip.placeholderapi.PlaceholderAPI; @@ -21,15 +21,13 @@ class JavascriptPlaceholderResolutionPriorityTest { @Test void customPlaceholderWinsSameNamedPapiTokenThenExpandsPapiInsideCustomValue() { - AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCorePlugin plugin = BaseTest.getInstance().plugin; OfflinePlayer player = mock(OfflinePlayer.class); JavascriptEngine engine = new JavascriptEngine(); when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true); - try (MockedStatic pluginStatic = mockStatic(AdvancedCorePlugin.class); - MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { - pluginStatic.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); + try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%reward_alias%")) .thenReturn("CustomName"); papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")) From b198e423571fe76fc161cdb837d92f54a6c0aa00 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 20:44:18 -0600 Subject: [PATCH 60/63] Add temporary round eight patch --- .github/fix302_round8.py | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/fix302_round8.py diff --git a/.github/fix302_round8.py b/.github/fix302_round8.py new file mode 100644 index 000000000..522bd3d35 --- /dev/null +++ b/.github/fix302_round8.py @@ -0,0 +1,75 @@ +from pathlib import Path + +binder_path = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") +modern_test_path = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java") +priority_test_path = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java") + +binder = binder_path.read_text() + + +def replace_once(text, old, new, label): + if old not in text: + raise SystemExit(f"missing expected block: {label}") + return text.replace(old, new, 1) + +binder = replace_once( + binder, + ''' public static String bind(String expression, OfflinePlayer player, Map placeholders,\n JavascriptEngine engine) {\n return bind(expression, token -> resolve(token, player, placeholders), engine::addToEngine);\n }\n\n static String bind(String expression, Function resolver, BiConsumer bindings) {\n if (expression == null || expression.isEmpty()) {\n''', + ''' public static String bind(String expression, OfflinePlayer player, Map placeholders,\n JavascriptEngine engine) {\n return bind(expression, token -> resolve(token, player, placeholders),\n value -> resolvePapiValue(value, player), engine::addToEngine);\n }\n\n static String bind(String expression, Function resolver, BiConsumer bindings) {\n return bind(expression, resolver, Function.identity(), bindings);\n }\n\n private static String bind(String expression, Function resolver,\n Function decodedResolver, BiConsumer bindings) {\n if (expression == null || expression.isEmpty()) {\n''', + "binder overload") + +binder = replace_once( + binder, + ''' String token = matcher.group();\n String value = JavascriptPlaceholderValue.decode(token);\n if (value == null) {\n value = resolver.apply(token);\n }\n''', + ''' String token = matcher.group();\n String value = JavascriptPlaceholderValue.decode(token);\n if (value == null) {\n value = resolver.apply(token);\n } else {\n // Values encoded by PlaceholderUtils are already known to be data, but\n // they may still contain PlaceholderAPI tokens from legacy custom -> PAPI\n // replacement chains. Resolve those tokens before escaping/binding.\n value = decodedResolver.apply(value);\n }\n''', + "decoded resolver") + +binder = replace_once( + binder, + ''' String value = entry.getValue();\n if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) {\n String resolved = PlaceholderAPI.setPlaceholders(player, value);\n if (resolved != null) {\n value = resolved;\n }\n }\n return value;\n''', + ''' return resolvePapiValue(entry.getValue(), player);\n''', + "custom papi helper") + +binder = replace_once( + binder, + ''' return token;\n }\n\n private static Object coerce(String value) {\n''', + ''' return token;\n }\n\n private static String resolvePapiValue(String value, OfflinePlayer player) {\n AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance();\n if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) {\n String resolved = PlaceholderAPI.setPlaceholders(player, value);\n if (resolved != null) {\n return resolved;\n }\n }\n return value;\n }\n\n private static Object coerce(String value) {\n''', + "papi helper") + +binder = replace_once( + binder, + ''' private final List strings = new ArrayList<>();\n private final List regexes = new ArrayList<>();\n private final List templates = new ArrayList<>();\n private final List templateExpressions = new ArrayList<>();\n private boolean parsed;\n''', + ''' private final List strings = new ArrayList<>();\n private final List regexes = new ArrayList<>();\n private final List templates = new ArrayList<>();\n private final List templateExpressions = new ArrayList<>();\n private final List comments = new ArrayList<>();\n private boolean parsed;\n''', + "comments field") + +binder = replace_once( + binder, + ''' private static JavascriptContexts fallback(String source) {\n JavascriptContexts contexts = new JavascriptContexts();\n\n addFallbackTemplateRanges(source, 0, source.length(), contexts);\n\n // Quote-looking text is a string only outside template text. Strings inside\n // ${...} remain ordinary JavaScript strings and are tracked normally.\n addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts);\n addFallbackRegexRanges(source, contexts);\n contexts.sort();\n return contexts;\n }\n''', + ''' private static JavascriptContexts fallback(String source) {\n JavascriptContexts contexts = new JavascriptContexts();\n\n // First identify template text so comment delimiters inside template text are\n // ignored. Then find comments, mask them with same-length whitespace, and\n // rebuild every literal range from the masked source. This prevents quotes or\n // backticks inside comments from manufacturing fake literal ranges around\n // executable placeholders.\n addFallbackTemplateRanges(source, 0, source.length(), contexts);\n addFallbackCommentRanges(source, contexts);\n String scanSource = maskRanges(source, contexts.comments);\n\n contexts.templates.clear();\n contexts.templateExpressions.clear();\n addFallbackTemplateRanges(scanSource, 0, scanSource.length(), contexts);\n\n // Quote-looking text is a string only outside template text. Strings inside\n // ${...} remain ordinary JavaScript strings and are tracked normally.\n addPatternRanges(scanSource, FALLBACK_STRING, contexts.strings, contexts);\n addFallbackRegexRanges(scanSource, contexts);\n contexts.sort();\n return contexts;\n }\n\n private static void addFallbackCommentRanges(String source, JavascriptContexts contexts) {\n for (int i = 0; i < source.length(); i++) {\n if (contexts.isTemplateText(i)) {\n continue;\n }\n\n char current = source.charAt(i);\n if (current == '\\'' || current == '\"') {\n i = skipQuotedLiteral(source, i, source.length(), current);\n continue;\n }\n if (current != '/' || i + 1 >= source.length()) {\n continue;\n }\n\n char next = source.charAt(i + 1);\n if (next == '/') {\n int end = i + 2;\n while (end < source.length() && source.charAt(end) != '\\n' && source.charAt(end) != '\\r') {\n end++;\n }\n addFallbackComment(contexts, new Range(i, end));\n i = end - 1;\n continue;\n }\n if (next == '*') {\n int end = i + 2;\n while (end + 1 < source.length()\n && !(source.charAt(end) == '*' && source.charAt(end + 1) == '/')) {\n end++;\n }\n end = end + 1 < source.length() ? end + 2 : source.length();\n addFallbackComment(contexts, new Range(i, end));\n i = end - 1;\n continue;\n }\n\n if (canStartRegex(source, i)) {\n int regexEnd = skipRegexLiteral(source, i, source.length());\n if (regexEnd > i) {\n i = regexEnd;\n }\n }\n }\n }\n\n private static void addFallbackComment(JavascriptContexts contexts, Range comment) {\n contexts.comments.add(comment);\n // Initial template discovery is only used to distinguish template text from\n // comments. A backtick inside a comment can create a false template range, so\n // discard any such range as soon as the comment is known.\n contexts.removeOverlapping(contexts.templates, comment);\n contexts.removeOverlapping(contexts.templateExpressions, comment);\n }\n\n private static String maskRanges(String source, List ranges) {\n StringBuilder masked = new StringBuilder(source);\n for (Range range : ranges) {\n for (int i = Math.max(0, range.start); i < Math.min(masked.length(), range.end); i++) {\n char current = masked.charAt(i);\n if (current != '\\n' && current != '\\r') {\n masked.setCharAt(i, ' ');\n }\n }\n }\n return masked.toString();\n }\n''', + "fallback comments") + +binder = replace_once( + binder, + ''' private void removeContained(List ranges, Range container) {\n ranges.removeIf(range -> range.start >= container.start && range.end <= container.end);\n }\n''', + ''' private void removeContained(List ranges, Range container) {\n ranges.removeIf(range -> range.start >= container.start && range.end <= container.end);\n }\n\n private void removeOverlapping(List ranges, Range overlap) {\n ranges.removeIf(range -> range.start < overlap.end && overlap.start < range.end);\n }\n''', + "remove overlap") + +binder = replace_once( + binder, + ''' strings.sort(comparator);\n regexes.sort(comparator);\n templates.sort(comparator);\n templateExpressions.sort(comparator);\n''', + ''' strings.sort(comparator);\n regexes.sort(comparator);\n templates.sort(comparator);\n templateExpressions.sort(comparator);\n comments.sort(comparator);\n''', + "sort comments") + +binder_path.write_text(binder) + +modern = modern_test_path.read_text() +insert = '''\n @Test\n void commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() {\n HashMap bindings = new HashMap<>();\n String injection = "Bukkit.dispatchCommand(Console, 'op attacker')";\n\n String prepared = JavascriptPlaceholderBinder.bind("obj?.x; /* ' */ %name%; /* ' */",\n ignored -> injection, bindings::put);\n\n assertEquals("obj?.x; /* ' */ __advancedCorePlaceholder0; /* ' */", prepared);\n assertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n assertFalse(prepared.contains(injection));\n }\n\n @Test\n void lineCommentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() {\n HashMap bindings = new HashMap<>();\n String injection = "Bukkit.dispatchCommand(Console, 'op attacker')";\n\n String prepared = JavascriptPlaceholderBinder.bind("obj?.x; // '\\n%name%; // '\\n",\n ignored -> injection, bindings::put);\n\n assertEquals("obj?.x; // '\\n__advancedCorePlaceholder0; // '\\n", prepared);\n assertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n assertFalse(prepared.contains(injection));\n }\n''' +if "commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder" not in modern: + modern = modern.replace("\n}\n", insert + "\n}\n") +modern_test_path.write_text(modern) + +priority = priority_test_path.read_text() +insert = '''\n @Test\n void decodedCustomValueStillExpandsNestedPapiToken() {\n AdvancedCorePlugin plugin = BaseTest.getInstance().plugin;\n OfflinePlayer player = mock(OfflinePlayer.class);\n JavascriptEngine engine = new JavascriptEngine();\n\n when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true);\n String encoded = JavascriptPlaceholderValue.encode("%player_name%");\n\n try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) {\n papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"))\n .thenReturn("Ben");\n\n String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "' == 'Ben'", player,\n Map.of(), engine);\n\n assertEquals("'Ben' == 'Ben'", prepared);\n papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"));\n }\n }\n''' +if "decodedCustomValueStillExpandsNestedPapiToken" not in priority: + priority = priority.replace("\n}\n", insert + "\n}\n") +priority_test_path.write_text(priority) From 3063d0c3b0973b515fc7fd689b289eec3ef6fe3a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 20:44:27 -0600 Subject: [PATCH 61/63] Run round eight JavaScript placeholder fixes --- .github/workflows/run-fix302-round8.yml | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/run-fix302-round8.yml diff --git a/.github/workflows/run-fix302-round8.yml b/.github/workflows/run-fix302-round8.yml new file mode 100644 index 000000000..89ab03d72 --- /dev/null +++ b/.github/workflows/run-fix302-round8.yml @@ -0,0 +1,27 @@ +name: Run round eight JavaScript placeholder fixes + +on: + push: + +permissions: + contents: write + +jobs: + patch: + if: github.ref == 'refs/heads/security/javascript-placeholder-bindings' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-bindings + fetch-depth: 0 + - name: Apply fixes + run: python .github/fix302_round8.py + - name: Commit fixes + run: | + git config user.name "Ben" + git config user.email "benbergen12@gmail.com" + git rm .github/fix302_round8.py .github/workflows/run-fix302-round8.yml + git add -A + git commit -m "Fix fallback comments and decoded PAPI values" + git push origin HEAD:security/javascript-placeholder-bindings From ab7b2fbeb4fa0c16931858a0609f5d3417656d0a Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 27 Aug 2026 02:44:39 +0000 Subject: [PATCH 62/63] Fix fallback comments and decoded PAPI values --- .github/fix302_round8.py | 75 ----------- .github/workflows/run-fix302-round8.yml | 27 ---- .../JavascriptPlaceholderBinder.java | 122 ++++++++++++++++-- ...ptPlaceholderModernSyntaxFallbackTest.java | 26 ++++ ...riptPlaceholderResolutionPriorityTest.java | 21 +++ 5 files changed, 158 insertions(+), 113 deletions(-) delete mode 100644 .github/fix302_round8.py delete mode 100644 .github/workflows/run-fix302-round8.yml diff --git a/.github/fix302_round8.py b/.github/fix302_round8.py deleted file mode 100644 index 522bd3d35..000000000 --- a/.github/fix302_round8.py +++ /dev/null @@ -1,75 +0,0 @@ -from pathlib import Path - -binder_path = Path("AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java") -modern_test_path = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java") -priority_test_path = Path("AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java") - -binder = binder_path.read_text() - - -def replace_once(text, old, new, label): - if old not in text: - raise SystemExit(f"missing expected block: {label}") - return text.replace(old, new, 1) - -binder = replace_once( - binder, - ''' public static String bind(String expression, OfflinePlayer player, Map placeholders,\n JavascriptEngine engine) {\n return bind(expression, token -> resolve(token, player, placeholders), engine::addToEngine);\n }\n\n static String bind(String expression, Function resolver, BiConsumer bindings) {\n if (expression == null || expression.isEmpty()) {\n''', - ''' public static String bind(String expression, OfflinePlayer player, Map placeholders,\n JavascriptEngine engine) {\n return bind(expression, token -> resolve(token, player, placeholders),\n value -> resolvePapiValue(value, player), engine::addToEngine);\n }\n\n static String bind(String expression, Function resolver, BiConsumer bindings) {\n return bind(expression, resolver, Function.identity(), bindings);\n }\n\n private static String bind(String expression, Function resolver,\n Function decodedResolver, BiConsumer bindings) {\n if (expression == null || expression.isEmpty()) {\n''', - "binder overload") - -binder = replace_once( - binder, - ''' String token = matcher.group();\n String value = JavascriptPlaceholderValue.decode(token);\n if (value == null) {\n value = resolver.apply(token);\n }\n''', - ''' String token = matcher.group();\n String value = JavascriptPlaceholderValue.decode(token);\n if (value == null) {\n value = resolver.apply(token);\n } else {\n // Values encoded by PlaceholderUtils are already known to be data, but\n // they may still contain PlaceholderAPI tokens from legacy custom -> PAPI\n // replacement chains. Resolve those tokens before escaping/binding.\n value = decodedResolver.apply(value);\n }\n''', - "decoded resolver") - -binder = replace_once( - binder, - ''' String value = entry.getValue();\n if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) {\n String resolved = PlaceholderAPI.setPlaceholders(player, value);\n if (resolved != null) {\n value = resolved;\n }\n }\n return value;\n''', - ''' return resolvePapiValue(entry.getValue(), player);\n''', - "custom papi helper") - -binder = replace_once( - binder, - ''' return token;\n }\n\n private static Object coerce(String value) {\n''', - ''' return token;\n }\n\n private static String resolvePapiValue(String value, OfflinePlayer player) {\n AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance();\n if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) {\n String resolved = PlaceholderAPI.setPlaceholders(player, value);\n if (resolved != null) {\n return resolved;\n }\n }\n return value;\n }\n\n private static Object coerce(String value) {\n''', - "papi helper") - -binder = replace_once( - binder, - ''' private final List strings = new ArrayList<>();\n private final List regexes = new ArrayList<>();\n private final List templates = new ArrayList<>();\n private final List templateExpressions = new ArrayList<>();\n private boolean parsed;\n''', - ''' private final List strings = new ArrayList<>();\n private final List regexes = new ArrayList<>();\n private final List templates = new ArrayList<>();\n private final List templateExpressions = new ArrayList<>();\n private final List comments = new ArrayList<>();\n private boolean parsed;\n''', - "comments field") - -binder = replace_once( - binder, - ''' private static JavascriptContexts fallback(String source) {\n JavascriptContexts contexts = new JavascriptContexts();\n\n addFallbackTemplateRanges(source, 0, source.length(), contexts);\n\n // Quote-looking text is a string only outside template text. Strings inside\n // ${...} remain ordinary JavaScript strings and are tracked normally.\n addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts);\n addFallbackRegexRanges(source, contexts);\n contexts.sort();\n return contexts;\n }\n''', - ''' private static JavascriptContexts fallback(String source) {\n JavascriptContexts contexts = new JavascriptContexts();\n\n // First identify template text so comment delimiters inside template text are\n // ignored. Then find comments, mask them with same-length whitespace, and\n // rebuild every literal range from the masked source. This prevents quotes or\n // backticks inside comments from manufacturing fake literal ranges around\n // executable placeholders.\n addFallbackTemplateRanges(source, 0, source.length(), contexts);\n addFallbackCommentRanges(source, contexts);\n String scanSource = maskRanges(source, contexts.comments);\n\n contexts.templates.clear();\n contexts.templateExpressions.clear();\n addFallbackTemplateRanges(scanSource, 0, scanSource.length(), contexts);\n\n // Quote-looking text is a string only outside template text. Strings inside\n // ${...} remain ordinary JavaScript strings and are tracked normally.\n addPatternRanges(scanSource, FALLBACK_STRING, contexts.strings, contexts);\n addFallbackRegexRanges(scanSource, contexts);\n contexts.sort();\n return contexts;\n }\n\n private static void addFallbackCommentRanges(String source, JavascriptContexts contexts) {\n for (int i = 0; i < source.length(); i++) {\n if (contexts.isTemplateText(i)) {\n continue;\n }\n\n char current = source.charAt(i);\n if (current == '\\'' || current == '\"') {\n i = skipQuotedLiteral(source, i, source.length(), current);\n continue;\n }\n if (current != '/' || i + 1 >= source.length()) {\n continue;\n }\n\n char next = source.charAt(i + 1);\n if (next == '/') {\n int end = i + 2;\n while (end < source.length() && source.charAt(end) != '\\n' && source.charAt(end) != '\\r') {\n end++;\n }\n addFallbackComment(contexts, new Range(i, end));\n i = end - 1;\n continue;\n }\n if (next == '*') {\n int end = i + 2;\n while (end + 1 < source.length()\n && !(source.charAt(end) == '*' && source.charAt(end + 1) == '/')) {\n end++;\n }\n end = end + 1 < source.length() ? end + 2 : source.length();\n addFallbackComment(contexts, new Range(i, end));\n i = end - 1;\n continue;\n }\n\n if (canStartRegex(source, i)) {\n int regexEnd = skipRegexLiteral(source, i, source.length());\n if (regexEnd > i) {\n i = regexEnd;\n }\n }\n }\n }\n\n private static void addFallbackComment(JavascriptContexts contexts, Range comment) {\n contexts.comments.add(comment);\n // Initial template discovery is only used to distinguish template text from\n // comments. A backtick inside a comment can create a false template range, so\n // discard any such range as soon as the comment is known.\n contexts.removeOverlapping(contexts.templates, comment);\n contexts.removeOverlapping(contexts.templateExpressions, comment);\n }\n\n private static String maskRanges(String source, List ranges) {\n StringBuilder masked = new StringBuilder(source);\n for (Range range : ranges) {\n for (int i = Math.max(0, range.start); i < Math.min(masked.length(), range.end); i++) {\n char current = masked.charAt(i);\n if (current != '\\n' && current != '\\r') {\n masked.setCharAt(i, ' ');\n }\n }\n }\n return masked.toString();\n }\n''', - "fallback comments") - -binder = replace_once( - binder, - ''' private void removeContained(List ranges, Range container) {\n ranges.removeIf(range -> range.start >= container.start && range.end <= container.end);\n }\n''', - ''' private void removeContained(List ranges, Range container) {\n ranges.removeIf(range -> range.start >= container.start && range.end <= container.end);\n }\n\n private void removeOverlapping(List ranges, Range overlap) {\n ranges.removeIf(range -> range.start < overlap.end && overlap.start < range.end);\n }\n''', - "remove overlap") - -binder = replace_once( - binder, - ''' strings.sort(comparator);\n regexes.sort(comparator);\n templates.sort(comparator);\n templateExpressions.sort(comparator);\n''', - ''' strings.sort(comparator);\n regexes.sort(comparator);\n templates.sort(comparator);\n templateExpressions.sort(comparator);\n comments.sort(comparator);\n''', - "sort comments") - -binder_path.write_text(binder) - -modern = modern_test_path.read_text() -insert = '''\n @Test\n void commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() {\n HashMap bindings = new HashMap<>();\n String injection = "Bukkit.dispatchCommand(Console, 'op attacker')";\n\n String prepared = JavascriptPlaceholderBinder.bind("obj?.x; /* ' */ %name%; /* ' */",\n ignored -> injection, bindings::put);\n\n assertEquals("obj?.x; /* ' */ __advancedCorePlaceholder0; /* ' */", prepared);\n assertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n assertFalse(prepared.contains(injection));\n }\n\n @Test\n void lineCommentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() {\n HashMap bindings = new HashMap<>();\n String injection = "Bukkit.dispatchCommand(Console, 'op attacker')";\n\n String prepared = JavascriptPlaceholderBinder.bind("obj?.x; // '\\n%name%; // '\\n",\n ignored -> injection, bindings::put);\n\n assertEquals("obj?.x; // '\\n__advancedCorePlaceholder0; // '\\n", prepared);\n assertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n assertFalse(prepared.contains(injection));\n }\n''' -if "commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder" not in modern: - modern = modern.replace("\n}\n", insert + "\n}\n") -modern_test_path.write_text(modern) - -priority = priority_test_path.read_text() -insert = '''\n @Test\n void decodedCustomValueStillExpandsNestedPapiToken() {\n AdvancedCorePlugin plugin = BaseTest.getInstance().plugin;\n OfflinePlayer player = mock(OfflinePlayer.class);\n JavascriptEngine engine = new JavascriptEngine();\n\n when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true);\n String encoded = JavascriptPlaceholderValue.encode("%player_name%");\n\n try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) {\n papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"))\n .thenReturn("Ben");\n\n String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "' == 'Ben'", player,\n Map.of(), engine);\n\n assertEquals("'Ben' == 'Ben'", prepared);\n papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"));\n }\n }\n''' -if "decodedCustomValueStillExpandsNestedPapiToken" not in priority: - priority = priority.replace("\n}\n", insert + "\n}\n") -priority_test_path.write_text(priority) diff --git a/.github/workflows/run-fix302-round8.yml b/.github/workflows/run-fix302-round8.yml deleted file mode 100644 index 89ab03d72..000000000 --- a/.github/workflows/run-fix302-round8.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Run round eight JavaScript placeholder fixes - -on: - push: - -permissions: - contents: write - -jobs: - patch: - if: github.ref == 'refs/heads/security/javascript-placeholder-bindings' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-bindings - fetch-depth: 0 - - name: Apply fixes - run: python .github/fix302_round8.py - - name: Commit fixes - run: | - git config user.name "Ben" - git config user.email "benbergen12@gmail.com" - git rm .github/fix302_round8.py .github/workflows/run-fix302-round8.yml - git add -A - git commit -m "Fix fallback comments and decoded PAPI values" - git push origin HEAD:security/javascript-placeholder-bindings diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 8f0e226b6..afcb9b44f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -52,10 +52,16 @@ private JavascriptPlaceholderBinder() { public static String bind(String expression, OfflinePlayer player, Map placeholders, JavascriptEngine engine) { - return bind(expression, token -> resolve(token, player, placeholders), engine::addToEngine); + return bind(expression, token -> resolve(token, player, placeholders), + value -> resolvePapiValue(value, player), engine::addToEngine); } static String bind(String expression, Function resolver, BiConsumer bindings) { + return bind(expression, resolver, Function.identity(), bindings); + } + + private static String bind(String expression, Function resolver, + Function decodedResolver, BiConsumer bindings) { if (expression == null || expression.isEmpty()) { return expression; } @@ -68,6 +74,11 @@ static String bind(String expression, Function resolver, BiConsu String value = JavascriptPlaceholderValue.decode(token); if (value == null) { value = resolver.apply(token); + } else { + // Values encoded by PlaceholderUtils are already known to be data, but + // they may still contain PlaceholderAPI tokens from legacy custom -> PAPI + // replacement chains. Resolve those tokens before escaping/binding. + value = decodedResolver.apply(value); } matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); // Keep all source offsets unchanged while making resolved placeholders parse @@ -140,14 +151,7 @@ private static String resolve(String token, OfflinePlayer player, Map entry : placeholders.entrySet()) { if (entry.getKey().equalsIgnoreCase(name)) { - String value = entry.getValue(); - if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { - String resolved = PlaceholderAPI.setPlaceholders(player, value); - if (resolved != null) { - value = resolved; - } - } - return value; + return resolvePapiValue(entry.getValue(), player); } } } @@ -163,6 +167,17 @@ private static String resolve(String token, OfflinePlayer player, Map regexes = new ArrayList<>(); private final List templates = new ArrayList<>(); private final List templateExpressions = new ArrayList<>(); + private final List comments = new ArrayList<>(); private boolean parsed; private static JavascriptContexts parse(String source) { @@ -357,16 +373,95 @@ private static JavascriptContexts parse(String source) { private static JavascriptContexts fallback(String source) { JavascriptContexts contexts = new JavascriptContexts(); + // First identify template text so comment delimiters inside template text are + // ignored. Then find comments, mask them with same-length whitespace, and + // rebuild every literal range from the masked source. This prevents quotes or + // backticks inside comments from manufacturing fake literal ranges around + // executable placeholders. addFallbackTemplateRanges(source, 0, source.length(), contexts); + addFallbackCommentRanges(source, contexts); + String scanSource = maskRanges(source, contexts.comments); + + contexts.templates.clear(); + contexts.templateExpressions.clear(); + addFallbackTemplateRanges(scanSource, 0, scanSource.length(), contexts); // Quote-looking text is a string only outside template text. Strings inside // ${...} remain ordinary JavaScript strings and are tracked normally. - addPatternRanges(source, FALLBACK_STRING, contexts.strings, contexts); - addFallbackRegexRanges(source, contexts); + addPatternRanges(scanSource, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(scanSource, contexts); contexts.sort(); return contexts; } + private static void addFallbackCommentRanges(String source, JavascriptContexts contexts) { + for (int i = 0; i < source.length(); i++) { + if (contexts.isTemplateText(i)) { + continue; + } + + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, source.length(), current); + continue; + } + if (current != '/' || i + 1 >= source.length()) { + continue; + } + + char next = source.charAt(i + 1); + if (next == '/') { + int end = i + 2; + while (end < source.length() && source.charAt(end) != '\n' && source.charAt(end) != '\r') { + end++; + } + addFallbackComment(contexts, new Range(i, end)); + i = end - 1; + continue; + } + if (next == '*') { + int end = i + 2; + while (end + 1 < source.length() + && !(source.charAt(end) == '*' && source.charAt(end + 1) == '/')) { + end++; + } + end = end + 1 < source.length() ? end + 2 : source.length(); + addFallbackComment(contexts, new Range(i, end)); + i = end - 1; + continue; + } + + if (canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, source.length()); + if (regexEnd > i) { + i = regexEnd; + } + } + } + } + + private static void addFallbackComment(JavascriptContexts contexts, Range comment) { + contexts.comments.add(comment); + // Initial template discovery is only used to distinguish template text from + // comments. A backtick inside a comment can create a false template range, so + // discard any such range as soon as the comment is known. + contexts.removeOverlapping(contexts.templates, comment); + contexts.removeOverlapping(contexts.templateExpressions, comment); + } + + private static String maskRanges(String source, List ranges) { + StringBuilder masked = new StringBuilder(source); + for (Range range : ranges) { + for (int i = Math.max(0, range.start); i < Math.min(masked.length(), range.end); i++) { + char current = masked.charAt(i); + if (current != '\n' && current != '\r') { + masked.setCharAt(i, ' '); + } + } + } + return masked.toString(); + } + private static void addFallbackTemplateRanges(String source, int start, int limit, JavascriptContexts contexts) { for (int i = start; i < limit; i++) { @@ -712,6 +807,10 @@ private void removeContained(List ranges, Range container) { ranges.removeIf(range -> range.start >= container.start && range.end <= container.end); } + private void removeOverlapping(List ranges, Range overlap) { + ranges.removeIf(range -> range.start < overlap.end && overlap.start < range.end); + } + private static Object createParser(Class parserClass) throws ReflectiveOperationException { for (Method method : parserClass.getMethods()) { if (!method.getName().equals("create") || !Modifier.isStatic(method.getModifiers())) { @@ -923,6 +1022,7 @@ private void sort() { regexes.sort(comparator); templates.sort(comparator); templateExpressions.sort(comparator); + comments.sort(comparator); } } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java index f77b77de2..392f47812 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -155,4 +155,30 @@ void nestedTemplateLiteralTextRemainsDataInFallback() { assertEquals("obj?.name; `${`inner \\${attack()}`}`", prepared); assertTrue(bindings.isEmpty()); } + @Test + void commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.x; /* ' */ %name%; /* ' */", + ignored -> injection, bindings::put); + + assertEquals("obj?.x; /* ' */ __advancedCorePlaceholder0; /* ' */", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + + @Test + void lineCommentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.x; // '\n%name%; // '\n", + ignored -> injection, bindings::put); + + assertEquals("obj?.x; // '\n__advancedCorePlaceholder0; // '\n", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java index 058519a05..6c31ed0bc 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java @@ -41,4 +41,25 @@ void customPlaceholderWinsSameNamedPapiTokenThenExpandsPapiInsideCustomValue() { papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"), never()); } } + @Test + void decodedCustomValueStillExpandsNestedPapiToken() { + AdvancedCorePlugin plugin = BaseTest.getInstance().plugin; + OfflinePlayer player = mock(OfflinePlayer.class); + JavascriptEngine engine = new JavascriptEngine(); + + when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + String encoded = JavascriptPlaceholderValue.encode("%player_name%"); + + try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")) + .thenReturn("Ben"); + + String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "' == 'Ben'", player, + Map.of(), engine); + + assertEquals("'Ben' == 'Ben'", prepared); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")); + } + } + } From 3561f097de6ec9cfd1ddba3a03f6bb8768a4a9aa Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 26 Aug 2026 20:45:30 -0600 Subject: [PATCH 63/63] Strengthen decoded placeholder regression --- .../JavascriptPlaceholderResolutionPriorityTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java index 6c31ed0bc..72b54b42c 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.api.javascript; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; @@ -41,6 +42,7 @@ void customPlaceholderWinsSameNamedPapiTokenThenExpandsPapiInsideCustomValue() { papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"), never()); } } + @Test void decodedCustomValueStillExpandsNestedPapiToken() { AdvancedCorePlugin plugin = BaseTest.getInstance().plugin; @@ -58,8 +60,8 @@ void decodedCustomValueStillExpandsNestedPapiToken() { Map.of(), engine); assertEquals("'Ben' == 'Ben'", prepared); + assertFalse(prepared.contains("%player_name%")); papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")); } } - }