From db94ad794687a33e19e5b6ddb944bf9cd28f3e18 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:15:09 -0600 Subject: [PATCH 01/54] Harden JavaScript placeholder evaluation --- .../api/javascript/JavascriptEngine.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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..0fbb34a46 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 @@ -15,6 +15,8 @@ import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.simpleapi.messages.MessageAPI; +import me.clip.placeholderapi.PlaceholderAPI; + public class JavascriptEngine { private HashMap engineAPI; @@ -102,6 +104,32 @@ public JavascriptEngine addToEngine(String text, Object ob) { return this; } + public String preparePlaceholders(OfflinePlayer player, String expression) { + return preparePlaceholders(player, expression, null); + } + + public String preparePlaceholders(OfflinePlayer player, String expression, HashMap placeholders) { + if (expression == null || expression.isEmpty()) { + return expression; + } + + return JavascriptPlaceholderParser.replace(expression, placeholder -> { + String key = placeholder.substring(1, placeholder.length() - 1); + if (placeholders != null) { + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + } + + if (player != null && AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { + return PlaceholderAPI.setPlaceholders(player, placeholder); + } + return placeholder; + }, this::addToEngine); + } + public void execute(String expression) { getResult(expression); } From 813107cb8b500f6e3123a19a3cf79e46089c428d Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:15:19 -0600 Subject: [PATCH 02/54] Add safe JavaScript placeholder parser --- .../JavascriptPlaceholderParser.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java new file mode 100644 index 000000000..9fd82cce9 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -0,0 +1,69 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class JavascriptPlaceholderParser { + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%"); + private static final String VARIABLE_PREFIX = "__advancedCorePlaceholder"; + + private JavascriptPlaceholderParser() { + } + + static String replace(String script, Function resolver, BiConsumer bindings) { + Matcher matcher = PLACEHOLDER_PATTERN.matcher(script); + StringBuffer result = new StringBuffer(); + int index = 0; + while (matcher.find()) { + String placeholder = matcher.group(); + String value = resolver.apply(placeholder); + if (value == null || value.equals(placeholder)) { + matcher.appendReplacement(result, Matcher.quoteReplacement(placeholder)); + continue; + } + + char quote = quoteAt(script, matcher.start()); + String replacement; + if (quote == 0) { + String variable = VARIABLE_PREFIX + index++; + bindings.accept(variable, value); + replacement = variable; + } else { + replacement = escape(value, quote); + } + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + return result.toString(); + } + + private static char quoteAt(String script, int end) { + char quote = 0; + boolean escaped = false; + for (int i = 0; i < end; i++) { + char current = script.charAt(i); + if (escaped) { + escaped = false; + } else if (current == '\\' && quote != 0) { + escaped = true; + } else if (current == quote) { + quote = 0; + } else if (quote == 0 && (current == '\'' || current == '"' || current == '`')) { + quote = current; + } + } + return quote; + } + + private static String escape(String value, char quote) { + String escaped = value.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n") + .replace("\u2028", "\\u2028").replace("\u2029", "\\u2029"); + escaped = escaped.replace(String.valueOf(quote), "\\" + quote); + if (quote == '`') { + escaped = escaped.replace("${", "\\${"); + } + return escaped; + } +} From 8bb99ba1be28c20add25fdd639cc34e04c29b7f1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:15:27 -0600 Subject: [PATCH 03/54] Bind placeholders before JavaScript requirements --- .../builtin/requirements/RequirementJavascript.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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..27aebdd0b 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,9 @@ 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()))); + JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()); + return expression.equals("") || engine.getBooleanValue( + engine.preparePlaceholders(user.getOfflinePlayer(), expression, rewardOptions.getPlaceholders())); } }.priority(90).addEditButton(new EditGUIButton(new ItemBuilder("DETECTOR_RAIL"), new EditGUIValueString("JavascriptExpression", null) { From 1fcc83194c510a1fd42475cf24306fa631d2af12 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:15:38 -0600 Subject: [PATCH 04/54] Bind placeholders before reward JavaScript execution --- .../api/rewards/builtin/RewardJavascript.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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..77b796aaf 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; @@ -39,8 +38,7 @@ public String onRewardRequest(Reward reward, AdvancedCoreUser user, ArrayList 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()); + if (engine.getBooleanValue( + engine.preparePlaceholders(user.getOfflinePlayer(), expression, placeholders))) { new RewardBuilder(section, "TrueRewards").withPrefix(reward.getName() + ".Javascript").send(user); } else { new RewardBuilder(section, "FalseRewards").withPrefix(reward.getName() + ".Javascript").send(user); From e65f1e8da7977dbda916709502935f536ab7d33e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:15:47 -0600 Subject: [PATCH 05/54] Add JavaScript placeholder injection regression tests --- .../JavascriptPlaceholderParserTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java new file mode 100644 index 000000000..caaced79b --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -0,0 +1,51 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderParserTest { + @Test + void bindsUnquotedPlaceholderValuesInsteadOfAddingThemToSource() { + HashMap bindings = new HashMap<>(); + String injection = "\"); Bukkit.dispatchCommand(Console, \"op attacker\"); //"; + + String script = JavascriptPlaceholderParser.replace("%player_name% == 'allowed'", ignored -> injection, + bindings::put); + + assertEquals("__advancedCorePlaceholder0 == 'allowed'", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void escapesPlaceholderValuesInsideQuotedStrings() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("check('%player_name%', \"%player_name%\")", + ignored -> "'\"\\\n${attack}", bindings::put); + + assertEquals("check('\\'\"\\\\\\n${attack}', \"'\\\"\\\\\\n${attack}\")", script); + assertEquals(0, bindings.size()); + } + + @Test + void escapesTemplateLiteralInterpolation() { + String script = JavascriptPlaceholderParser.replace("`Hello %player_name%`", ignored -> "${attack}`", + (name, value) -> { + }); + + assertEquals("`Hello \\${attack}\\``", script); + } + + @Test + void leavesUnresolvedPlaceholdersUntouched() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("%unknown% == true", value -> value, bindings::put); + + assertEquals("%unknown% == true", script); + assertEquals(0, bindings.size()); + } +} From b473c306ddf8cd412d55c1d22c3da3b46d9c5e54 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:19:49 -0600 Subject: [PATCH 06/54] Prevent placeholder values from creating JavaScript blocks --- .../api/messages/PlaceholderUtils.java | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) 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..2de179a0e 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 @@ -19,6 +19,9 @@ import net.md_5.bungee.api.chat.TextComponent; public class PlaceholderUtils { + private static final String JAVASCRIPT_MARKER = "[Javascript="; + private static final String SAFE_JAVASCRIPT_MARKER = "[\u200BJavascript="; + @SuppressWarnings("deprecation") public static TextComponent parseJson(String msg) { TextComponent comp = new TextComponent(""); @@ -69,19 +72,11 @@ 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)); - comp.addExtra(t); - comp.addExtra(parseJson(postMessage)); } else { - comp.addExtra(new TextComponent(TextComponent.fromLegacyText(msg))); } return comp; @@ -92,13 +87,14 @@ public static String parseText(Player player, String str) { } public static String parseText(Player player, String str, HashMap placeholders) { + if (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { + JavascriptEngine engine = new JavascriptEngine().addPlayer(player); + str = replaceJavascript(str, engine, player, placeholders); + } if (placeholders != null) { str = replacePlaceHolder(str, placeholders); } - str = replacePlaceHolders(player, str); - - str = replaceJavascript(player, str); return MessageAPI.colorize(str); } @@ -107,11 +103,12 @@ public static String parseText(String str) { } public static String parseText(String str, HashMap placeholders) { + if (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { + str = replaceJavascript(str, new JavascriptEngine(), null, placeholders); + } if (placeholders != null) { str = replacePlaceHolder(str, placeholders); } - - str = replaceJavascript(str); return MessageAPI.colorize(str); } @@ -126,7 +123,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(), null); } return text; } @@ -176,9 +173,10 @@ public static String replaceJavascript(OfflinePlayer player, String text) { return replaceJavascript(player.getPlayer(), text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + String parsed = replaceJavascript(text, engine, player, null); + return replacePlaceHolders(player, parsed); } - return text; + return replacePlaceHolders(player, text); } public static ArrayList replaceJavascriptOnly(OfflinePlayer player, ArrayList list) { @@ -195,7 +193,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, null); } return text; } @@ -209,12 +207,12 @@ public static ArrayList replaceJavascript(Player player, ArrayList replaceJavascriptOnly(Player player, ArrayList list) { @@ -228,7 +226,7 @@ public static ArrayList replaceJavascriptOnly(Player player, ArrayList placeholders) { String msg = ""; - if (MessageAPI.containsIgnorecase(text, "[Javascript=")) { + if (MessageAPI.containsIgnorecase(text, JAVASCRIPT_MARKER)) { if (engine == null) { engine = new JavascriptEngine(); } @@ -247,7 +250,7 @@ public static String replaceJavascript(String text, JavascriptEngine engine) { int startIndex = 0; int num = 0; while (startIndex != -1) { - startIndex = text.indexOf("[Javascript=", lastIndex); + startIndex = text.indexOf(JAVASCRIPT_MARKER, lastIndex); int endIndex = -1; if (startIndex != -1) { @@ -258,28 +261,25 @@ public static String replaceJavascript(String text, JavascriptEngine engine) { } num++; endIndex = text.indexOf("]", startIndex); - String str = text.substring(startIndex + "[Javascript=".length(), endIndex); - // plugin.debug(startIndex + ":" + endIndex + " from " + - // text + " to " + str + " currently " + msg); - String script = engine.getStringValue(str); + if (endIndex == -1) { + return text; + } + String scriptText = text.substring(startIndex + JAVASCRIPT_MARKER.length(), endIndex); + String prepared = engine.preparePlaceholders(player, scriptText, placeholders); + String script = engine.getStringValue(prepared); if (script == null) { - script = "" + engine.getBooleanValue(str); - + script = "" + engine.getBooleanValue(prepared); } - if (script != null) { msg += script; } lastIndex = endIndex; } - } msg += text.substring(lastIndex + 1); - } else { msg = text; } - // plugin.debug(msg); return msg; } @@ -322,16 +322,25 @@ public static String replacePlaceHolder(String str, String toReplace, String rep } public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) { + String safeReplacement = neutralizeJavascriptMarker(replaceWith); if (ignoreCase) { - return MessageAPI.replaceIgnoreCase(MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", replaceWith), - "\\{" + toReplace + "\\}", replaceWith); + return MessageAPI.replaceIgnoreCase( + MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", safeReplacement), + "\\{" + toReplace + "\\}", safeReplacement); } str = str.replaceAll("\\{", "%"); str = str.replaceAll("\\}", "%"); - str = str.replace("%" + toReplace + "%", replaceWith); + str = str.replace("%" + toReplace + "%", safeReplacement); return str; } + static String neutralizeJavascriptMarker(String value) { + if (value == null || value.isEmpty()) { + return value; + } + return MessageAPI.replaceIgnoreCase(value, JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); + } + public static ArrayList replacePlaceHolders(ArrayList list, Player p) { ArrayList newList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { From 682163eefcd817211c8b7d511da966bfe4a06e9e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:19:58 -0600 Subject: [PATCH 07/54] Test JavaScript marker neutralization in placeholder values --- .../PlaceholderUtilsSecurityTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java new file mode 100644 index 000000000..28e321af9 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java @@ -0,0 +1,32 @@ +package com.bencodez.advancedcore.api.messages; + +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 PlaceholderUtilsSecurityTest { + + @Test + void placeholderValueCannotCreateJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("displayname", "[Javascript=Bukkit.dispatchCommand(Console,'op attacker')]"); + + String result = PlaceholderUtils.replacePlaceHolder("Thanks %displayname%", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertTrue(result.contains("Javascript=")); + } + + @Test + void normalPlaceholderFormattingIsPreserved() { + HashMap placeholders = new HashMap<>(); + placeholders.put("displayname", "&aDisplay Name"); + + String result = PlaceholderUtils.replacePlaceHolder("Thanks %displayname%", placeholders); + + assertTrue(result.contains("&aDisplay Name")); + } +} From 84c8461bbd8155a4e4d3d3eba2d35815cd2b10e7 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:22:07 -0600 Subject: [PATCH 08/54] Preserve JavaScript behavior with mocked engines --- .../builtin/requirements/RequirementJavascript.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 27aebdd0b..cc656feb6 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 @@ -27,9 +27,16 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { @Override public boolean onRequirementsRequest(Reward reward, AdvancedCoreUser user, String expression, RewardOptions rewardOptions) { + if (expression.equals("")) { + return true; + } JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()); - return expression.equals("") || engine.getBooleanValue( - engine.preparePlaceholders(user.getOfflinePlayer(), expression, rewardOptions.getPlaceholders())); + String prepared = engine.preparePlaceholders(user.getOfflinePlayer(), expression, + rewardOptions.getPlaceholders()); + if (prepared == null) { + prepared = expression; + } + return engine.getBooleanValue(prepared); } }.priority(90).addEditButton(new EditGUIButton(new ItemBuilder("DETECTOR_RAIL"), new EditGUIValueString("JavascriptExpression", null) { From d7e86bd1f0747c8f67262fb462db46cf142cada1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:22:19 -0600 Subject: [PATCH 09/54] Preserve reward JavaScript behavior with mocked engines --- .../advancedcore/api/rewards/builtin/RewardJavascript.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 77b796aaf..a2dff62b0 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 @@ -38,7 +38,8 @@ public String onRewardRequest(Reward reward, AdvancedCoreUser user, ArrayList Date: Sun, 23 Aug 2026 19:22:53 -0600 Subject: [PATCH 10/54] Fix literal JavaScript marker neutralization --- .../api/messages/PlaceholderUtils.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) 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 2de179a0e..5864dd844 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 @@ -42,7 +42,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("\","); @@ -70,7 +69,6 @@ public static TextComponent parseJson(String msg) { } else if (type.equalsIgnoreCase("suggest_command")) { t.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, typeData)); } - } comp.addExtra(parseJson(preMessage)); @@ -266,6 +264,9 @@ private static String replaceJavascript(String text, JavascriptEngine engine, Of } String scriptText = text.substring(startIndex + JAVASCRIPT_MARKER.length(), endIndex); String prepared = engine.preparePlaceholders(player, scriptText, placeholders); + if (prepared == null) { + prepared = scriptText; + } String script = engine.getStringValue(prepared); if (script == null) { script = "" + engine.getBooleanValue(prepared); @@ -309,14 +310,6 @@ public static String replacePlaceHolder(String str, HashMap plac return str; } - /** - * Replace place holder. - * - * @param str the str - * @param toReplace the to replace - * @param replaceWith the replace with - * @return the string - */ public static String replacePlaceHolder(String str, String toReplace, String replaceWith) { return replacePlaceHolder(str, toReplace, replaceWith, true); } @@ -338,7 +331,7 @@ static String neutralizeJavascriptMarker(String value) { if (value == null || value.isEmpty()) { return value; } - return MessageAPI.replaceIgnoreCase(value, JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); + return value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); } public static ArrayList replacePlaceHolders(ArrayList list, Player p) { @@ -367,13 +360,6 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { return text; } - /** - * Replace place holders. - * - * @param player the player - * @param text the text - * @return the string - */ public static String replacePlaceHolders(Player player, String text) { if (player == null) { return text; From 7b672768f7cdc289d9d8349f93638e0eb36a3f6b Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:43:21 -0600 Subject: [PATCH 11/54] Handle template expressions and primitive placeholder values --- .../JavascriptPlaceholderParser.java | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 9fd82cce9..4ba34ce83 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -6,7 +6,9 @@ import java.util.regex.Pattern; final class JavascriptPlaceholderParser { - private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%"); + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%|\\{([^{}]+)\\}"); + private static final Pattern INTEGER_PATTERN = Pattern.compile("[-+]?\\d+"); + private static final Pattern DECIMAL_PATTERN = Pattern.compile("[-+]?(?:\\d+\\.\\d*|\\d*\\.\\d+|\\d+)(?:[eE][-+]?\\d+)?"); private static final String VARIABLE_PREFIX = "__advancedCorePlaceholder"; private JavascriptPlaceholderParser() { @@ -25,10 +27,11 @@ static String replace(String script, Function resolver, BiConsum } char quote = quoteAt(script, matcher.start()); + boolean templateExpression = quote == '`' && isInsideTemplateExpression(script, matcher.start()); String replacement; - if (quote == 0) { + if (quote == 0 || templateExpression) { String variable = VARIABLE_PREFIX + index++; - bindings.accept(variable, value); + bindings.accept(variable, coercePrimitive(value)); replacement = variable; } else { replacement = escape(value, quote); @@ -39,6 +42,27 @@ static String replace(String script, Function resolver, BiConsum return result.toString(); } + private static Object coercePrimitive(String value) { + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return Boolean.valueOf(value); + } + if (INTEGER_PATTERN.matcher(value).matches()) { + try { + return Long.valueOf(value); + } catch (NumberFormatException ignored) { + // Fall through to string binding for values outside the long range. + } + } + if (DECIMAL_PATTERN.matcher(value).matches()) { + try { + return Double.valueOf(value); + } catch (NumberFormatException ignored) { + // Fall through to string binding for values outside the double range. + } + } + return value; + } + private static char quoteAt(String script, int end) { char quote = 0; boolean escaped = false; @@ -57,6 +81,65 @@ private static char quoteAt(String script, int end) { return quote; } + private static boolean isInsideTemplateExpression(String script, int end) { + boolean inTemplate = false; + boolean escaped = false; + int expressionDepth = 0; + char expressionQuote = 0; + + for (int i = 0; i < end; i++) { + char current = script.charAt(i); + if (!inTemplate) { + if (current == '`') { + inTemplate = true; + expressionDepth = 0; + escaped = false; + } + continue; + } + + if (expressionDepth == 0) { + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '`') { + inTemplate = false; + continue; + } + if (current == '$' && i + 1 < end && script.charAt(i + 1) == '{') { + expressionDepth = 1; + i++; + } + continue; + } + + if (expressionQuote != 0) { + if (escaped) { + escaped = false; + } else if (current == '\\') { + escaped = true; + } else if (current == expressionQuote) { + expressionQuote = 0; + } + continue; + } + + if (current == '\'' || current == '"') { + expressionQuote = current; + } else if (current == '{') { + expressionDepth++; + } else if (current == '}') { + expressionDepth--; + } + } + return inTemplate && expressionDepth > 0; + } + private static String escape(String value, char quote) { String escaped = value.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n") .replace("\u2028", "\\u2028").replace("\u2029", "\\u2029"); From 3759d0fdb50b5fd76270c549ddaba886eba81b39 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:43:38 -0600 Subject: [PATCH 12/54] Preserve brace placeholder compatibility --- .../advancedcore/api/javascript/JavascriptEngine.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0fbb34a46..f3b2c8d0c 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 @@ -120,10 +120,10 @@ public String preparePlaceholders(OfflinePlayer player, String expression, HashM if (entry.getKey().equalsIgnoreCase(key)) { return entry.getValue(); } - } } - if (player != null && AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { + if (placeholder.startsWith("%") && player != null + && AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { return PlaceholderAPI.setPlaceholders(player, placeholder); } return placeholder; From 519f894cfe32ea0fc72908a2346efe088d943d81 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:43:51 -0600 Subject: [PATCH 13/54] Add parser regression coverage from review feedback --- .../JavascriptPlaceholderParserTest.java | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index caaced79b..65c6b1386 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.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.assertTrue; import java.util.HashMap; @@ -31,7 +32,7 @@ void escapesPlaceholderValuesInsideQuotedStrings() { } @Test - void escapesTemplateLiteralInterpolation() { + void escapesTemplateLiteralText() { String script = JavascriptPlaceholderParser.replace("`Hello %player_name%`", ignored -> "${attack}`", (name, value) -> { }); @@ -39,13 +40,59 @@ void escapesTemplateLiteralInterpolation() { assertEquals("`Hello \\${attack}\\``", script); } + @Test + void bindsPlaceholdersInsideTemplateExpressions() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("`${%untrusted%}`", ignored -> injection, bindings::put); + + assertEquals("`${__advancedCorePlaceholder0}`", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void preservesBooleanAndNumericPlaceholderTypes() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("%bool% == true && %count% > 2 && %ratio% < 2.0", + placeholder -> { + switch (placeholder) { + case "%bool%": + return "true"; + case "%count%": + return "5"; + default: + return "1.5"; + } + }, bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true && __advancedCorePlaceholder1 > 2 && __advancedCorePlaceholder2 < 2.0", + script); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder1")); + assertEquals(Double.valueOf(1.5), bindings.get("__advancedCorePlaceholder2")); + } + + @Test + void supportsBraceFormCustomPlaceholders() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("{permission_result} == true", + placeholder -> placeholder.equals("{permission_result}") ? "true" : placeholder, bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true", script); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + @Test void leavesUnresolvedPlaceholdersUntouched() { HashMap bindings = new HashMap<>(); - String script = JavascriptPlaceholderParser.replace("%unknown% == true", value -> value, bindings::put); + String script = JavascriptPlaceholderParser.replace("%unknown% == true && {unknown_brace} == false", value -> value, + bindings::put); - assertEquals("%unknown% == true", script); - assertEquals(0, bindings.size()); + assertEquals("%unknown% == true && {unknown_brace} == false", script); + assertTrue(bindings.isEmpty()); } } From 6c1d32cf8c2ac029e6e8cd565deea7d2a699be0f Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:45:18 -0600 Subject: [PATCH 14/54] Fix JavaScript placeholder resolver block --- .../bencodez/advancedcore/api/javascript/JavascriptEngine.java | 1 + 1 file changed, 1 insertion(+) 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 f3b2c8d0c..48b6f5404 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 @@ -120,6 +120,7 @@ public String preparePlaceholders(OfflinePlayer player, String expression, HashM if (entry.getKey().equalsIgnoreCase(key)) { return entry.getValue(); } + } } if (placeholder.startsWith("%") && player != null From abe43c0d7fb22ac51ec81818fed040c2e5b0599e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:47:43 -0600 Subject: [PATCH 15/54] Fix template expression placeholder matching --- .../api/javascript/JavascriptPlaceholderParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 4ba34ce83..bbe9b982e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -6,7 +6,7 @@ import java.util.regex.Pattern; final class JavascriptPlaceholderParser { - private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%|\\{([^{}]+)\\}"); + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%|(? Date: Sun, 23 Aug 2026 20:01:33 -0600 Subject: [PATCH 16/54] Handle comments and nested templates safely --- .../JavascriptPlaceholderParser.java | 138 +++++++++++------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index bbe9b982e..28d371992 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -1,5 +1,7 @@ package com.bencodez.advancedcore.api.javascript; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -26,15 +28,15 @@ static String replace(String script, Function resolver, BiConsum continue; } - char quote = quoteAt(script, matcher.start()); - boolean templateExpression = quote == '`' && isInsideTemplateExpression(script, matcher.start()); + Context context = contextAt(script, matcher.start()); String replacement; - if (quote == 0 || templateExpression) { + if (context == Context.CODE || context == Context.TEMPLATE_EXPRESSION || context == Context.COMMENT) { String variable = VARIABLE_PREFIX + index++; bindings.accept(variable, coercePrimitive(value)); replacement = variable; } else { - replacement = escape(value, quote); + replacement = escape(value, context == Context.SINGLE_QUOTE ? '\'' + : context == Context.DOUBLE_QUOTE ? '"' : '`'); } matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } @@ -50,55 +52,51 @@ private static Object coercePrimitive(String value) { try { return Long.valueOf(value); } catch (NumberFormatException ignored) { - // Fall through to string binding for values outside the long range. } } if (DECIMAL_PATTERN.matcher(value).matches()) { try { return Double.valueOf(value); } catch (NumberFormatException ignored) { - // Fall through to string binding for values outside the double range. } } return value; } - private static char quoteAt(String script, int end) { - char quote = 0; + private static Context contextAt(String script, int end) { + Deque templates = new ArrayDeque<>(); + Context context = Context.CODE; boolean escaped = false; - for (int i = 0; i < end; i++) { - char current = script.charAt(i); - if (escaped) { - escaped = false; - } else if (current == '\\' && quote != 0) { - escaped = true; - } else if (current == quote) { - quote = 0; - } else if (quote == 0 && (current == '\'' || current == '"' || current == '`')) { - quote = current; - } - } - return quote; - } - - private static boolean isInsideTemplateExpression(String script, int end) { - boolean inTemplate = false; - boolean escaped = false; - int expressionDepth = 0; - char expressionQuote = 0; for (int i = 0; i < end; i++) { char current = script.charAt(i); - if (!inTemplate) { - if (current == '`') { - inTemplate = true; - expressionDepth = 0; + char next = i + 1 < end ? script.charAt(i + 1) : '\0'; + + if (context == Context.LINE_COMMENT) { + if (current == '\n' || current == '\r') { + context = codeContext(templates); + } + continue; + } + if (context == Context.BLOCK_COMMENT) { + if (current == '*' && next == '/') { + context = codeContext(templates); + i++; + } + continue; + } + if (context == Context.SINGLE_QUOTE || context == Context.DOUBLE_QUOTE) { + char quote = context == Context.SINGLE_QUOTE ? '\'' : '"'; + if (escaped) { escaped = false; + } else if (current == '\\') { + escaped = true; + } else if (current == quote) { + context = codeContext(templates); } continue; } - - if (expressionDepth == 0) { + if (context == Context.TEMPLATE_TEXT) { if (escaped) { escaped = false; continue; @@ -108,36 +106,62 @@ private static boolean isInsideTemplateExpression(String script, int end) { continue; } if (current == '`') { - inTemplate = false; + templates.pop(); + context = codeContext(templates); continue; } - if (current == '$' && i + 1 < end && script.charAt(i + 1) == '{') { - expressionDepth = 1; + if (current == '$' && next == '{') { + templates.peek().expressionDepth = 1; + context = Context.TEMPLATE_EXPRESSION; i++; } continue; } - if (expressionQuote != 0) { - if (escaped) { - escaped = false; - } else if (current == '\\') { - escaped = true; - } else if (current == expressionQuote) { - expressionQuote = 0; - } + if (current == '/' && next == '/') { + context = Context.LINE_COMMENT; + i++; continue; } - - if (current == '\'' || current == '"') { - expressionQuote = current; - } else if (current == '{') { - expressionDepth++; - } else if (current == '}') { - expressionDepth--; + if (current == '/' && next == '*') { + context = Context.BLOCK_COMMENT; + i++; + continue; + } + if (current == '\'') { + context = Context.SINGLE_QUOTE; + escaped = false; + continue; + } + if (current == '"') { + context = Context.DOUBLE_QUOTE; + escaped = false; + continue; } + if (current == '`') { + templates.push(new TemplateFrame()); + context = Context.TEMPLATE_TEXT; + continue; + } + if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { + if (current == '{') { + templates.peek().expressionDepth++; + } else if (current == '}') { + templates.peek().expressionDepth--; + if (templates.peek().expressionDepth == 0) { + context = Context.TEMPLATE_TEXT; + } + } + } + } + if (context == Context.LINE_COMMENT || context == Context.BLOCK_COMMENT) { + return Context.COMMENT; } - return inTemplate && expressionDepth > 0; + return context; + } + + private static Context codeContext(Deque templates) { + return !templates.isEmpty() && templates.peek().expressionDepth > 0 ? Context.TEMPLATE_EXPRESSION : Context.CODE; } private static String escape(String value, char quote) { @@ -149,4 +173,12 @@ private static String escape(String value, char quote) { } return escaped; } + + private enum Context { + CODE, SINGLE_QUOTE, DOUBLE_QUOTE, TEMPLATE_TEXT, TEMPLATE_EXPRESSION, LINE_COMMENT, BLOCK_COMMENT, COMMENT + } + + private static final class TemplateFrame { + private int expressionDepth; + } } From 4373528b33a9cfa51683aa28128948e6e022fb4f Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:01:50 -0600 Subject: [PATCH 17/54] Test comment and nested template placeholder contexts --- .../JavascriptPlaceholderParserTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 65c6b1386..1d92f6451 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -51,6 +51,28 @@ void bindsPlaceholdersInsideTemplateExpressions() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void ignoresCommentBracesInsideTemplateExpressions() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("`${/* } */ %untrusted%}`", ignored -> injection, + bindings::put); + + assertEquals("`${/* } */ __advancedCorePlaceholder0}`", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void preservesNestedTemplateLiteralTextInsideExpressions() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("`${`Hello %name%`}`", ignored -> "Ben", bindings::put); + + assertEquals("`${`Hello Ben`}`", script); + assertTrue(bindings.isEmpty()); + } + @Test void preservesBooleanAndNumericPlaceholderTypes() { HashMap bindings = new HashMap<>(); From ae72815d471f17dfe4e89a35ce60d0f1d5a184ca Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:49:57 -0600 Subject: [PATCH 18/54] Handle regex literals in JavaScript placeholder parsing --- .../JavascriptPlaceholderParser.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 28d371992..18ae4a373 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -30,7 +30,8 @@ static String replace(String script, Function resolver, BiConsum Context context = contextAt(script, matcher.start()); String replacement; - if (context == Context.CODE || context == Context.TEMPLATE_EXPRESSION || context == Context.COMMENT) { + if (context == Context.CODE || context == Context.TEMPLATE_EXPRESSION || context == Context.COMMENT + || context == Context.REGEX) { String variable = VARIABLE_PREFIX + index++; bindings.accept(variable, coercePrimitive(value)); replacement = variable; @@ -67,6 +68,7 @@ private static Context contextAt(String script, int end) { Deque templates = new ArrayDeque<>(); Context context = Context.CODE; boolean escaped = false; + boolean regexCharacterClass = false; for (int i = 0; i < end; i++) { char current = script.charAt(i); @@ -85,6 +87,28 @@ private static Context contextAt(String script, int end) { } continue; } + if (context == Context.REGEX) { + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '[') { + regexCharacterClass = true; + continue; + } + if (current == ']' && regexCharacterClass) { + regexCharacterClass = false; + continue; + } + if (current == '/' && !regexCharacterClass) { + context = codeContext(templates); + } + continue; + } if (context == Context.SINGLE_QUOTE || context == Context.DOUBLE_QUOTE) { char quote = context == Context.SINGLE_QUOTE ? '\'' : '"'; if (escaped) { @@ -128,6 +152,12 @@ private static Context contextAt(String script, int end) { i++; continue; } + if (current == '/' && startsRegexLiteral(script, i)) { + context = Context.REGEX; + escaped = false; + regexCharacterClass = false; + continue; + } if (current == '\'') { context = Context.SINGLE_QUOTE; escaped = false; @@ -160,6 +190,17 @@ private static Context contextAt(String script, int end) { return context; } + private static boolean startsRegexLiteral(String script, int slashIndex) { + for (int i = slashIndex - 1; i >= 0; i--) { + char previous = script.charAt(i); + if (Character.isWhitespace(previous)) { + continue; + } + return "([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0; + } + return true; + } + private static Context codeContext(Deque templates) { return !templates.isEmpty() && templates.peek().expressionDepth > 0 ? Context.TEMPLATE_EXPRESSION : Context.CODE; } @@ -175,7 +216,7 @@ private static String escape(String value, char quote) { } private enum Context { - CODE, SINGLE_QUOTE, DOUBLE_QUOTE, TEMPLATE_TEXT, TEMPLATE_EXPRESSION, LINE_COMMENT, BLOCK_COMMENT, COMMENT + CODE, SINGLE_QUOTE, DOUBLE_QUOTE, TEMPLATE_TEXT, TEMPLATE_EXPRESSION, REGEX, LINE_COMMENT, BLOCK_COMMENT, COMMENT } private static final class TemplateFrame { From 123177fe21e945c90fab8640c8962837aa2b6fda Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:50:10 -0600 Subject: [PATCH 19/54] Test regex literal placeholder hardening --- .../JavascriptPlaceholderParserTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 1d92f6451..756b6836a 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -73,6 +73,29 @@ void preservesNestedTemplateLiteralTextInsideExpressions() { assertTrue(bindings.isEmpty()); } + @Test + void regexLiteralQuotesDoNotChangePlaceholderContext() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("/[']/.test('x'); %untrusted%", ignored -> injection, + bindings::put); + + assertEquals("/[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void regexCharacterClassesAndEscapesRemainRegexText() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("/[/\\']+/.test(value); %name%", ignored -> "Ben", + bindings::put); + + assertEquals("/[/\\']+/.test(value); __advancedCorePlaceholder0", script); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } + @Test void preservesBooleanAndNumericPlaceholderTypes() { HashMap bindings = new HashMap<>(); From 7fdc9fe168e4f6d473681ccf22cf5867cf5bbf41 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:58:54 -0600 Subject: [PATCH 20/54] Preserve placeholder values inside regex literals --- .../JavascriptPlaceholderParser.java | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 18ae4a373..e373e225d 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -30,8 +30,9 @@ static String replace(String script, Function resolver, BiConsum Context context = contextAt(script, matcher.start()); String replacement; - if (context == Context.CODE || context == Context.TEMPLATE_EXPRESSION || context == Context.COMMENT - || context == Context.REGEX) { + if (context == Context.REGEX || context == Context.REGEX_CHARACTER_CLASS) { + replacement = escapeRegexLiteral(value, context == Context.REGEX_CHARACTER_CLASS); + } else if (context == Context.CODE || context == Context.TEMPLATE_EXPRESSION || context == Context.COMMENT) { String variable = VARIABLE_PREFIX + index++; bindings.accept(variable, coercePrimitive(value)); replacement = variable; @@ -187,6 +188,9 @@ private static Context contextAt(String script, int end) { if (context == Context.LINE_COMMENT || context == Context.BLOCK_COMMENT) { return Context.COMMENT; } + if (context == Context.REGEX && regexCharacterClass) { + return Context.REGEX_CHARACTER_CLASS; + } return context; } @@ -205,6 +209,35 @@ private static Context codeContext(Deque templates) { return !templates.isEmpty() && templates.peek().expressionDepth > 0 ? Context.TEMPLATE_EXPRESSION : Context.CODE; } + private static String escapeRegexLiteral(String value, boolean characterClass) { + StringBuilder escaped = new StringBuilder(value.length()); + String special = characterClass ? "\\/]^-" : "\\/.*+?^${}()|[]"; + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + switch (current) { + case '\r': + escaped.append("\\r"); + break; + case '\n': + escaped.append("\\n"); + break; + case '\u2028': + escaped.append("\\u2028"); + break; + case '\u2029': + escaped.append("\\u2029"); + break; + default: + if (special.indexOf(current) >= 0) { + escaped.append('\\'); + } + escaped.append(current); + break; + } + } + return escaped.toString(); + } + private static String escape(String value, char quote) { String escaped = value.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n") .replace("\u2028", "\\u2028").replace("\u2029", "\\u2029"); @@ -216,7 +249,8 @@ private static String escape(String value, char quote) { } private enum Context { - CODE, SINGLE_QUOTE, DOUBLE_QUOTE, TEMPLATE_TEXT, TEMPLATE_EXPRESSION, REGEX, LINE_COMMENT, BLOCK_COMMENT, COMMENT + CODE, SINGLE_QUOTE, DOUBLE_QUOTE, TEMPLATE_TEXT, TEMPLATE_EXPRESSION, REGEX, REGEX_CHARACTER_CLASS, + LINE_COMMENT, BLOCK_COMMENT, COMMENT } private static final class TemplateFrame { From d3487a9d0bd2fcad96ab1217a1dbf609d5106c16 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:59:24 -0600 Subject: [PATCH 21/54] Test placeholders inside regex literals --- .../JavascriptPlaceholderParserTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 756b6836a..81aa5a89c 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -96,6 +96,27 @@ void regexCharacterClassesAndEscapesRemainRegexText() { assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); } + @Test + void preservesPlaceholderValuesInsideRegexLiterals() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("/^%player_name%$/.test(name)", + ignored -> "Ben.* /admin", bindings::put); + + assertEquals("/^Ben\\.\\* \\/admin$/.test(name)", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void escapesPlaceholderValuesInsideRegexCharacterClasses() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("/[%chars%]/.test(name)", ignored -> "]^-", bindings::put); + + assertEquals("/[\\]\\^\\-]/.test(name)", script); + assertTrue(bindings.isEmpty()); + } + @Test void preservesBooleanAndNumericPlaceholderTypes() { HashMap bindings = new HashMap<>(); From 167da68965562cf42395dcf970e4dc6a730f1c47 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:19:29 -0600 Subject: [PATCH 22/54] Recognize regex literals after control heads and keywords --- .../JavascriptPlaceholderParser.java | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index e373e225d..bf4ba8c9c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -2,6 +2,7 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -12,6 +13,9 @@ final class JavascriptPlaceholderParser { private static final Pattern INTEGER_PATTERN = Pattern.compile("[-+]?\\d+"); private static final Pattern DECIMAL_PATTERN = Pattern.compile("[-+]?(?:\\d+\\.\\d*|\\d*\\.\\d+|\\d+)(?:[eE][-+]?\\d+)?"); private static final String VARIABLE_PREFIX = "__advancedCorePlaceholder"; + private static final Set REGEX_PREFIX_KEYWORDS = Set.of("return", "throw", "case", "delete", "void", + "typeof", "instanceof", "in", "of", "new", "yield", "await", "else", "do"); + private static final Set CONTROL_HEAD_KEYWORDS = Set.of("if", "while", "for", "with", "switch", "catch"); private JavascriptPlaceholderParser() { } @@ -195,14 +199,56 @@ private static Context contextAt(String script, int end) { } private static boolean startsRegexLiteral(String script, int slashIndex) { - for (int i = slashIndex - 1; i >= 0; i--) { - char previous = script.charAt(i); - if (Character.isWhitespace(previous)) { - continue; + int previousIndex = previousNonWhitespace(script, slashIndex - 1); + if (previousIndex < 0) { + return true; + } + char previous = script.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + if (previous == ')' && closesControlHead(script, previousIndex)) { + return true; + } + String previousWord = previousIdentifier(script, previousIndex); + return REGEX_PREFIX_KEYWORDS.contains(previousWord); + } + + private static boolean closesControlHead(String script, int closeParenIndex) { + int depth = 1; + for (int i = closeParenIndex - 1; i >= 0; i--) { + char current = script.charAt(i); + if (current == ')') { + depth++; + } else if (current == '(') { + depth--; + if (depth == 0) { + int keywordEnd = previousNonWhitespace(script, i - 1); + return CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)); + } } - return "([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0; } - return true; + return false; + } + + private static int previousNonWhitespace(String script, int index) { + for (int i = index; i >= 0; i--) { + if (!Character.isWhitespace(script.charAt(i))) { + return i; + } + } + return -1; + } + + private static String previousIdentifier(String script, int endIndex) { + if (endIndex < 0 || !Character.isJavaIdentifierPart(script.charAt(endIndex))) { + return ""; + } + int start = endIndex; + while (start > 0 && Character.isJavaIdentifierPart(script.charAt(start - 1))) { + start--; + } + return script.substring(start, endIndex + 1); } private static Context codeContext(Deque templates) { From 26505fd6a301b0584b5795807f00ffc4a8fad32e Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:20:13 -0600 Subject: [PATCH 23/54] Test regex detection after control heads and keywords --- .../JavascriptPlaceholderParserTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 81aa5a89c..ca99908e4 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -85,6 +85,30 @@ void regexLiteralQuotesDoNotChangePlaceholderContext() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void recognizesRegexLiteralAfterControlHead() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("if (ok) /[']/.test('x'); %untrusted%", ignored -> injection, + bindings::put); + + assertEquals("if (ok) /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void recognizesRegexLiteralAfterExpressionKeyword() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("return /[']/.test('x'); %untrusted%", ignored -> injection, + bindings::put); + + assertEquals("return /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + @Test void regexCharacterClassesAndEscapesRemainRegexText() { HashMap bindings = new HashMap<>(); From 58fa7b02f10d36316f641be1ad9e7f567fea8204 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:27:26 -0600 Subject: [PATCH 24/54] Recognize regex literals after statement blocks --- .../JavascriptPlaceholderParser.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index bf4ba8c9c..fc9ebcd73 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -16,6 +16,7 @@ final class JavascriptPlaceholderParser { private static final Set REGEX_PREFIX_KEYWORDS = Set.of("return", "throw", "case", "delete", "void", "typeof", "instanceof", "in", "of", "new", "yield", "await", "else", "do"); private static final Set CONTROL_HEAD_KEYWORDS = Set.of("if", "while", "for", "with", "switch", "catch"); + private static final Set BLOCK_PREFIX_KEYWORDS = Set.of("else", "do", "try", "finally"); private JavascriptPlaceholderParser() { } @@ -210,6 +211,9 @@ private static boolean startsRegexLiteral(String script, int slashIndex) { if (previous == ')' && closesControlHead(script, previousIndex)) { return true; } + if (previous == '}' && closesStatementBlock(script, previousIndex)) { + return true; + } String previousWord = previousIdentifier(script, previousIndex); return REGEX_PREFIX_KEYWORDS.contains(previousWord); } @@ -231,6 +235,33 @@ private static boolean closesControlHead(String script, int closeParenIndex) { return false; } + private static boolean closesStatementBlock(String script, int closeBraceIndex) { + int depth = 1; + for (int i = closeBraceIndex - 1; i >= 0; i--) { + char current = script.charAt(i); + if (current == '}') { + depth++; + } else if (current == '{') { + depth--; + if (depth == 0) { + int prefixIndex = previousNonWhitespace(script, i - 1); + if (prefixIndex < 0) { + return true; + } + char prefix = script.charAt(prefixIndex); + if (prefix == ')' || prefix == '}' || prefix == ';') { + return true; + } + if (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { + return true; + } + return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); + } + } + } + return false; + } + private static int previousNonWhitespace(String script, int index) { for (int i = index; i >= 0; i--) { if (!Character.isWhitespace(script.charAt(i))) { From 3c0533d6f116895af77e27b547909468310f4ee3 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:27:58 -0600 Subject: [PATCH 25/54] Test regex statements after blocks --- .../JavascriptPlaceholderParserTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index ca99908e4..d74ae06d9 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -109,6 +109,28 @@ void recognizesRegexLiteralAfterExpressionKeyword() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void recognizesRegexLiteralAfterStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("if (ok) {} /[']/.test('x'); %untrusted%", ignored -> injection, + bindings::put); + + assertEquals("if (ok) {} /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void objectLiteralFollowedByDivisionIsNotTreatedAsRegex() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("value = {} / 2; %name%", ignored -> "Ben", bindings::put); + + assertEquals("value = {} / 2; __advancedCorePlaceholder0", script); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } + @Test void regexCharacterClassesAndEscapesRemainRegexText() { HashMap bindings = new HashMap<>(); From 215802ee404f103330d827f53dce4eac31b3690c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:09:28 -0600 Subject: [PATCH 26/54] Distinguish postfix operators before regex detection --- .../javascript/JavascriptPlaceholderParser.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index fc9ebcd73..914e3bc82 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -205,6 +205,9 @@ private static boolean startsRegexLiteral(String script, int slashIndex) { return true; } char previous = script.charAt(previousIndex); + if ((previous == '+' || previous == '-') && isPostfixIncrementOrDecrement(script, previousIndex)) { + return false; + } if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { return true; } @@ -218,6 +221,20 @@ private static boolean startsRegexLiteral(String script, int slashIndex) { return REGEX_PREFIX_KEYWORDS.contains(previousWord); } + private static boolean isPostfixIncrementOrDecrement(String script, int operatorEndIndex) { + char operator = script.charAt(operatorEndIndex); + if (operatorEndIndex == 0 || script.charAt(operatorEndIndex - 1) != operator) { + return false; + } + int operandEnd = previousNonWhitespace(script, operatorEndIndex - 2); + if (operandEnd < 0) { + return false; + } + char operand = script.charAt(operandEnd); + return Character.isJavaIdentifierPart(operand) || Character.isDigit(operand) || operand == ')' || operand == ']' + || operand == '}'; + } + private static boolean closesControlHead(String script, int closeParenIndex) { int depth = 1; for (int i = closeParenIndex - 1; i >= 0; i--) { From 84730f14b667f661972b702d55f1e89c68eb6875 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:09:59 -0600 Subject: [PATCH 27/54] Test postfix division placeholder context --- .../javascript/JavascriptPlaceholderParserTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index d74ae06d9..9c563e556 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -121,6 +121,18 @@ void recognizesRegexLiteralAfterStatementBlock() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void postfixIncrementBeforeDivisionDoesNotOpenRegexContext() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace("var allowed=false,i=1; i++ / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,i=1; i++ / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + @Test void objectLiteralFollowedByDivisionIsNotTreatedAsRegex() { HashMap bindings = new HashMap<>(); From 3046dc08f5f6a1827b416e8cd6500999af3252ad Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:10:26 -0600 Subject: [PATCH 28/54] Test full-pass JavaScript marker neutralization --- .../PlaceholderUtilsSecurityTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java index 28e321af9..31ea11283 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java @@ -20,6 +20,29 @@ void placeholderValueCannotCreateJavascriptMarker() { assertTrue(result.contains("Javascript=")); } + @Test + void substitutionsCannotAssembleJavascriptMarkerAcrossTemplateText() { + HashMap placeholders = new HashMap<>(); + placeholders.put("part", "script"); + + String result = PlaceholderUtils.replacePlaceHolder( + "[Java%part%=Bukkit.dispatchCommand(Console,'op attacker')]", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertTrue(result.contains("Javascript=")); + } + + @Test + void operatorAuthoredJavascriptMarkerRemainsAvailable() { + HashMap placeholders = new HashMap<>(); + placeholders.put("name", "Ben"); + + String result = PlaceholderUtils.replacePlaceHolder("[Javascript=1+1] %name%", placeholders); + + assertTrue(result.contains("[Javascript=1+1]")); + assertTrue(result.contains("Ben")); + } + @Test void normalPlaceholderFormattingIsPreserved() { HashMap placeholders = new HashMap<>(); From 0f44375e131009ab959be69ecfe699266152dcba Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:10:29 -0600 Subject: [PATCH 29/54] Neutralize JavaScript markers after placeholder substitution --- .../api/messages/PlaceholderUtils.java | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) 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 5864dd844..71b290b47 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 @@ -21,6 +21,7 @@ public class PlaceholderUtils { private static final String JAVASCRIPT_MARKER = "[Javascript="; private static final String SAFE_JAVASCRIPT_MARKER = "[\u200BJavascript="; + private static final String PROTECTED_JAVASCRIPT_MARKER = "[\u2063Javascript="; @SuppressWarnings("deprecation") public static TextComponent parseJson(String msg) { @@ -315,16 +316,19 @@ public static String replacePlaceHolder(String str, String toReplace, String rep } public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) { - String safeReplacement = neutralizeJavascriptMarker(replaceWith); + String protectedSource = protectJavascriptMarkers(str); + String safeReplacement = neutralizeJavascriptReplacement(replaceWith); + String replaced; if (ignoreCase) { - return MessageAPI.replaceIgnoreCase( - MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", safeReplacement), + replaced = MessageAPI.replaceIgnoreCase( + MessageAPI.replaceIgnoreCase(protectedSource, "%" + toReplace + "%", safeReplacement), "\\{" + toReplace + "\\}", safeReplacement); + } else { + replaced = protectedSource.replaceAll("\\{", "%"); + replaced = replaced.replaceAll("\\}", "%"); + replaced = replaced.replace("%" + toReplace + "%", safeReplacement); } - str = str.replaceAll("\\{", "%"); - str = str.replaceAll("\\}", "%"); - str = str.replace("%" + toReplace + "%", safeReplacement); - return str; + return restoreJavascriptMarkers(neutralizeJavascriptMarker(replaced)); } static String neutralizeJavascriptMarker(String value) { @@ -334,6 +338,27 @@ static String neutralizeJavascriptMarker(String value) { return value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); } + private static String neutralizeJavascriptReplacement(String value) { + if (value == null || value.isEmpty()) { + return value; + } + return neutralizeJavascriptMarker(value).replace(PROTECTED_JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); + } + + private static String protectJavascriptMarkers(String value) { + if (value == null || value.isEmpty()) { + return value; + } + return value.replace(JAVASCRIPT_MARKER, PROTECTED_JAVASCRIPT_MARKER); + } + + private static String restoreJavascriptMarkers(String value) { + if (value == null || value.isEmpty()) { + return value; + } + return value.replace(PROTECTED_JAVASCRIPT_MARKER, JAVASCRIPT_MARKER); + } + public static ArrayList replacePlaceHolders(ArrayList list, Player p) { ArrayList newList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { From dedc29be887a62d4405ad81018d3eabf8b3b2105 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:10:46 -0600 Subject: [PATCH 30/54] Apply full-pass placeholder marker hardening --- .../workflows/fix-placeholder-marker-pass.yml | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/fix-placeholder-marker-pass.yml diff --git a/.github/workflows/fix-placeholder-marker-pass.yml b/.github/workflows/fix-placeholder-marker-pass.yml new file mode 100644 index 000000000..8e95fba7a --- /dev/null +++ b/.github/workflows/fix-placeholder-marker-pass.yml @@ -0,0 +1,105 @@ +name: Apply placeholder marker full-pass fix + +on: + push: + branches: + - security/javascript-placeholder-hardening + paths: + - .github/workflows/fix-placeholder-marker-pass.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + fetch-depth: 0 + - name: Patch PlaceholderUtils substitution pass + run: | + python3 - <<'PY' + from pathlib import Path + import re + + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java') + text = path.read_text() + + methods = re.compile( + r'\tpublic static String replacePlaceHolder\(String str, HashMap placeholders\) \{.*?' + r'\n\t\}\n\n' + r'\tpublic static String replacePlaceHolder\(String str, HashMap placeholders, boolean ignoreCase\) \{.*?' + r'\n\t\}\n', + re.DOTALL, + ) + replacement = '''\tpublic static String replacePlaceHolder(String str, HashMap placeholders) { + \t\tif (str == null || placeholders == null || placeholders.isEmpty()) { + \t\t\treturn str; + \t\t} + \t\tString protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); + \t\tstr = str.replace(JAVASCRIPT_MARKER, protectedMarker); + \t\tfor (Entry entry : placeholders.entrySet()) { + \t\t\tstr = replacePlaceHolder(str, entry.getKey(), entry.getValue()); + \t\t} + \t\tstr = neutralizeJavascriptMarker(str); + \t\treturn str.replace(protectedMarker, JAVASCRIPT_MARKER); + \t} + + \tpublic static String replacePlaceHolder(String str, HashMap placeholders, boolean ignoreCase) { + \t\tif (str == null || placeholders == null || placeholders.isEmpty()) { + \t\t\treturn str; + \t\t} + \t\tString protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); + \t\tstr = str.replace(JAVASCRIPT_MARKER, protectedMarker); + \t\tfor (Entry entry : placeholders.entrySet()) { + \t\t\tstr = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase); + \t\t} + \t\tstr = neutralizeJavascriptMarker(str); + \t\treturn str.replace(protectedMarker, JAVASCRIPT_MARKER); + \t} + ''' + text, count = methods.subn(replacement, text, count=1) + if count != 1: + raise SystemExit('Unable to replace map placeholder methods') + + neutralize = '''\tstatic String neutralizeJavascriptMarker(String value) { + \t\tif (value == null || value.isEmpty()) { + \t\t\treturn value; + \t\t} + \t\treturn value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); + \t} + ''' + helper = neutralize + ''' + \tprivate static String createProtectedJavascriptMarkerToken(String text, HashMap placeholders) { + \t\tString token; + \t\tboolean collision; + \t\tdo { + \t\t\ttoken = ""; + \t\t\tcollision = text != null && text.contains(token); + \t\t\tif (!collision && placeholders != null) { + \t\t\t\tfor (String value : placeholders.values()) { + \t\t\t\t\tif (value != null && value.contains(token)) { + \t\t\t\t\t\tcollision = true; + \t\t\t\t\t\tbreak; + \t\t\t\t\t} + \t\t\t\t} + \t\t\t} + \t\t} while (collision); + \t\treturn token; + \t} + ''' + if neutralize not in text: + raise SystemExit('Unable to locate neutralizeJavascriptMarker') + text = text.replace(neutralize, helper, 1) + path.write_text(text) + PY + - name: Commit marker fix + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java + git rm .github/workflows/fix-placeholder-marker-pass.yml + git commit -m "Neutralize JavaScript markers after placeholder substitution" + git push origin HEAD:security/javascript-placeholder-hardening From 5fee0e8818438a6857ec4b688aec3c0de188dcd0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:10:58 +0000 Subject: [PATCH 31/54] Neutralize JavaScript markers after placeholder substitution --- .../workflows/fix-placeholder-marker-pass.yml | 105 ------------------ .../api/messages/PlaceholderUtils.java | 46 ++++++-- 2 files changed, 36 insertions(+), 115 deletions(-) delete mode 100644 .github/workflows/fix-placeholder-marker-pass.yml diff --git a/.github/workflows/fix-placeholder-marker-pass.yml b/.github/workflows/fix-placeholder-marker-pass.yml deleted file mode 100644 index 8e95fba7a..000000000 --- a/.github/workflows/fix-placeholder-marker-pass.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Apply placeholder marker full-pass fix - -on: - push: - branches: - - security/javascript-placeholder-hardening - paths: - - .github/workflows/fix-placeholder-marker-pass.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - fetch-depth: 0 - - name: Patch PlaceholderUtils substitution pass - run: | - python3 - <<'PY' - from pathlib import Path - import re - - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java') - text = path.read_text() - - methods = re.compile( - r'\tpublic static String replacePlaceHolder\(String str, HashMap placeholders\) \{.*?' - r'\n\t\}\n\n' - r'\tpublic static String replacePlaceHolder\(String str, HashMap placeholders, boolean ignoreCase\) \{.*?' - r'\n\t\}\n', - re.DOTALL, - ) - replacement = '''\tpublic static String replacePlaceHolder(String str, HashMap placeholders) { - \t\tif (str == null || placeholders == null || placeholders.isEmpty()) { - \t\t\treturn str; - \t\t} - \t\tString protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); - \t\tstr = str.replace(JAVASCRIPT_MARKER, protectedMarker); - \t\tfor (Entry entry : placeholders.entrySet()) { - \t\t\tstr = replacePlaceHolder(str, entry.getKey(), entry.getValue()); - \t\t} - \t\tstr = neutralizeJavascriptMarker(str); - \t\treturn str.replace(protectedMarker, JAVASCRIPT_MARKER); - \t} - - \tpublic static String replacePlaceHolder(String str, HashMap placeholders, boolean ignoreCase) { - \t\tif (str == null || placeholders == null || placeholders.isEmpty()) { - \t\t\treturn str; - \t\t} - \t\tString protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); - \t\tstr = str.replace(JAVASCRIPT_MARKER, protectedMarker); - \t\tfor (Entry entry : placeholders.entrySet()) { - \t\t\tstr = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase); - \t\t} - \t\tstr = neutralizeJavascriptMarker(str); - \t\treturn str.replace(protectedMarker, JAVASCRIPT_MARKER); - \t} - ''' - text, count = methods.subn(replacement, text, count=1) - if count != 1: - raise SystemExit('Unable to replace map placeholder methods') - - neutralize = '''\tstatic String neutralizeJavascriptMarker(String value) { - \t\tif (value == null || value.isEmpty()) { - \t\t\treturn value; - \t\t} - \t\treturn value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); - \t} - ''' - helper = neutralize + ''' - \tprivate static String createProtectedJavascriptMarkerToken(String text, HashMap placeholders) { - \t\tString token; - \t\tboolean collision; - \t\tdo { - \t\t\ttoken = ""; - \t\t\tcollision = text != null && text.contains(token); - \t\t\tif (!collision && placeholders != null) { - \t\t\t\tfor (String value : placeholders.values()) { - \t\t\t\t\tif (value != null && value.contains(token)) { - \t\t\t\t\t\tcollision = true; - \t\t\t\t\t\tbreak; - \t\t\t\t\t} - \t\t\t\t} - \t\t\t} - \t\t} while (collision); - \t\treturn token; - \t} - ''' - if neutralize not in text: - raise SystemExit('Unable to locate neutralizeJavascriptMarker') - text = text.replace(neutralize, helper, 1) - path.write_text(text) - PY - - name: Commit marker fix - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java - git rm .github/workflows/fix-placeholder-marker-pass.yml - git commit -m "Neutralize JavaScript markers after placeholder substitution" - git push origin HEAD:security/javascript-placeholder-hardening 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 71b290b47..8937d5832 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 @@ -294,21 +294,29 @@ public static 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 (str == null || placeholders == null || placeholders.isEmpty()) { + return str; + } + String protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); + str = str.replace(JAVASCRIPT_MARKER, protectedMarker); + for (Entry entry : placeholders.entrySet()) { + str = replacePlaceHolder(str, entry.getKey(), entry.getValue()); } - return str; + str = neutralizeJavascriptMarker(str); + return str.replace(protectedMarker, JAVASCRIPT_MARKER); } 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 (str == null || placeholders == null || placeholders.isEmpty()) { + return str; + } + String protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); + str = str.replace(JAVASCRIPT_MARKER, protectedMarker); + for (Entry entry : placeholders.entrySet()) { + str = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase); } - return str; + str = neutralizeJavascriptMarker(str); + return str.replace(protectedMarker, JAVASCRIPT_MARKER); } public static String replacePlaceHolder(String str, String toReplace, String replaceWith) { @@ -338,6 +346,24 @@ static String neutralizeJavascriptMarker(String value) { return value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); } + private static String createProtectedJavascriptMarkerToken(String text, HashMap placeholders) { + String token; + boolean collision; + do { + token = ""; + collision = text != null && text.contains(token); + if (!collision && placeholders != null) { + for (String value : placeholders.values()) { + if (value != null && value.contains(token)) { + collision = true; + break; + } + } + } + } while (collision); + return token; + } + private static String neutralizeJavascriptReplacement(String value) { if (value == null || value.isEmpty()) { return value; From de082a5caedba4a0e7461c38a6f120bb201bd468 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:15:54 -0600 Subject: [PATCH 32/54] Test full-pass JavaScript marker neutralization --- .../api/messages/PlaceholderUtilsSecurityTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java index 31ea11283..a94ea06c0 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java @@ -32,6 +32,19 @@ void substitutionsCannotAssembleJavascriptMarkerAcrossTemplateText() { assertTrue(result.contains("Javascript=")); } + @Test + void multipleSubstitutionsCannotAssembleJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("prefix", "Java"); + placeholders.put("suffix", "script"); + + String result = PlaceholderUtils.replacePlaceHolder( + "[%prefix%%suffix%=Bukkit.dispatchCommand(Console,'op attacker')]", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertTrue(result.contains("Javascript=")); + } + @Test void operatorAuthoredJavascriptMarkerRemainsAvailable() { HashMap placeholders = new HashMap<>(); From 7061974e8af7dc53b499c50bd4704fb169e46902 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:30:31 -0600 Subject: [PATCH 33/54] Apply Codex lexical fixes for PR 293 --- .github/workflows/fix-codex-293-lexical.yml | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/fix-codex-293-lexical.yml diff --git a/.github/workflows/fix-codex-293-lexical.yml b/.github/workflows/fix-codex-293-lexical.yml new file mode 100644 index 000000000..f73f5947a --- /dev/null +++ b/.github/workflows/fix-codex-293-lexical.yml @@ -0,0 +1,84 @@ +name: Apply Codex 293 lexical fixes + +on: + push: + branches: + - security/javascript-placeholder-hardening + paths: + - .github/workflows/fix-codex-293-lexical.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + fetch-depth: 0 + - name: Patch parser and tests + run: | + python3 - <<'PY' + from pathlib import Path + import re + + parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') + text = parser.read_text() + + text = text.replace( + 'Pattern.compile("%([^%]+)%|(? controlParens = new ArrayDeque<>();\n' + '\t\tint lastControlHeadClose = -1;\n') + + text = text.replace( + "\t\t\tif (current == '/' && startsRegexLiteral(script, i)) {", + "\t\t\tif (current == '/' && startsRegexLiteral(script, i, lastControlHeadClose)) {") + + needle = """\t\t\tif (current == '`') {\n\t\t\t\ttemplates.push(new TemplateFrame());\n\t\t\t\tcontext = Context.TEMPLATE_TEXT;\n\t\t\t\tcontinue;\n\t\t\t}\n""" + replacement = needle + """\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t}\n""" + if needle not in text: + raise SystemExit('Unable to locate template transition') + text = text.replace(needle, replacement, 1) + + text = text.replace( + 'private static boolean startsRegexLiteral(String script, int slashIndex) {', + 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) {') + text = text.replace( + "\t\tif (previous == ')' && closesControlHead(script, previousIndex)) {\n\t\t\treturn true;\n\t\t}\n", + "\t\tif (previous == ')' && previousIndex == lastControlHeadClose) {\n\t\t\treturn true;\n\t\t}\n") + + text, count = re.subn( + r'\n\tprivate static boolean closesControlHead\(String script, int closeParenIndex\) \{.*?\n\t\}\n\n\tprivate static boolean closesStatementBlock', + '\n\tprivate static boolean closesStatementBlock', + text, + count=1, + flags=re.DOTALL) + if count != 1: + raise SystemExit('Unable to remove closesControlHead') + + parser.write_text(text) + + tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') + t = tests.read_text() + marker = """\t@Test\n\tvoid objectLiteralFollowedByDivisionIsNotTreatedAsRegex() {\n""" + additions = """\t@Test\n\tvoid controlHeadIgnoresParenthesesInsideStrings() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"Bukkit.dispatchCommand(Console, \\\"op attacker\\\")\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"if (fn(\\\")\\\")) /[']/.test('x'); %untrusted%\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"if (fn(\\\")\\\")) /[']/.test('x'); __advancedCorePlaceholder0\", script);\n\t\tassertEquals(injection, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n\t@Test\n\tvoid controlHeadIgnoresParenthesesInsideRegexLiterals() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"Bukkit.dispatchCommand(Console, \\\"op attacker\\\")\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"if (/\\\\)/.test(value)) /[']/.test('x'); %untrusted%\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"if (/\\\\)/.test(value)) /[']/.test('x'); __advancedCorePlaceholder0\", script);\n\t\tassertEquals(injection, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n\t@Test\n\tvoid objectLiteralDoesNotConsumeNestedPercentPlaceholder() {\n\t\tHashMap bindings = new HashMap<>();\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"({allowed: %permission_result%}).allowed\",\n\t\t\t\tplaceholder -> placeholder.equals(\"%permission_result%\") ? \"true\" : placeholder, bindings::put);\n\n\t\tassertEquals(\"({allowed: __advancedCorePlaceholder0}).allowed\", script);\n\t\tassertEquals(Boolean.TRUE, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n""" + if marker not in t: + raise SystemExit('Unable to locate parser test insertion point') + t = t.replace(marker, additions + marker, 1) + tests.write_text(t) + PY + - name: Commit fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java + git rm .github/workflows/fix-codex-293-lexical.yml + git commit -m "Harden JavaScript lexical control tracking" + git push origin HEAD:security/javascript-placeholder-hardening From 82852179cd6c23d537e0403afa98a9d94d2adda1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:30:42 +0000 Subject: [PATCH 34/54] Harden JavaScript lexical control tracking --- .github/workflows/fix-codex-293-lexical.yml | 84 ------------------- .../JavascriptPlaceholderParser.java | 35 ++++---- .../JavascriptPlaceholderParserTest.java | 35 ++++++++ 3 files changed, 49 insertions(+), 105 deletions(-) delete mode 100644 .github/workflows/fix-codex-293-lexical.yml diff --git a/.github/workflows/fix-codex-293-lexical.yml b/.github/workflows/fix-codex-293-lexical.yml deleted file mode 100644 index f73f5947a..000000000 --- a/.github/workflows/fix-codex-293-lexical.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Apply Codex 293 lexical fixes - -on: - push: - branches: - - security/javascript-placeholder-hardening - paths: - - .github/workflows/fix-codex-293-lexical.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - fetch-depth: 0 - - name: Patch parser and tests - run: | - python3 - <<'PY' - from pathlib import Path - import re - - parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') - text = parser.read_text() - - text = text.replace( - 'Pattern.compile("%([^%]+)%|(? controlParens = new ArrayDeque<>();\n' - '\t\tint lastControlHeadClose = -1;\n') - - text = text.replace( - "\t\t\tif (current == '/' && startsRegexLiteral(script, i)) {", - "\t\t\tif (current == '/' && startsRegexLiteral(script, i, lastControlHeadClose)) {") - - needle = """\t\t\tif (current == '`') {\n\t\t\t\ttemplates.push(new TemplateFrame());\n\t\t\t\tcontext = Context.TEMPLATE_TEXT;\n\t\t\t\tcontinue;\n\t\t\t}\n""" - replacement = needle + """\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t}\n""" - if needle not in text: - raise SystemExit('Unable to locate template transition') - text = text.replace(needle, replacement, 1) - - text = text.replace( - 'private static boolean startsRegexLiteral(String script, int slashIndex) {', - 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) {') - text = text.replace( - "\t\tif (previous == ')' && closesControlHead(script, previousIndex)) {\n\t\t\treturn true;\n\t\t}\n", - "\t\tif (previous == ')' && previousIndex == lastControlHeadClose) {\n\t\t\treturn true;\n\t\t}\n") - - text, count = re.subn( - r'\n\tprivate static boolean closesControlHead\(String script, int closeParenIndex\) \{.*?\n\t\}\n\n\tprivate static boolean closesStatementBlock', - '\n\tprivate static boolean closesStatementBlock', - text, - count=1, - flags=re.DOTALL) - if count != 1: - raise SystemExit('Unable to remove closesControlHead') - - parser.write_text(text) - - tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') - t = tests.read_text() - marker = """\t@Test\n\tvoid objectLiteralFollowedByDivisionIsNotTreatedAsRegex() {\n""" - additions = """\t@Test\n\tvoid controlHeadIgnoresParenthesesInsideStrings() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"Bukkit.dispatchCommand(Console, \\\"op attacker\\\")\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"if (fn(\\\")\\\")) /[']/.test('x'); %untrusted%\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"if (fn(\\\")\\\")) /[']/.test('x'); __advancedCorePlaceholder0\", script);\n\t\tassertEquals(injection, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n\t@Test\n\tvoid controlHeadIgnoresParenthesesInsideRegexLiterals() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"Bukkit.dispatchCommand(Console, \\\"op attacker\\\")\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"if (/\\\\)/.test(value)) /[']/.test('x'); %untrusted%\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"if (/\\\\)/.test(value)) /[']/.test('x'); __advancedCorePlaceholder0\", script);\n\t\tassertEquals(injection, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n\t@Test\n\tvoid objectLiteralDoesNotConsumeNestedPercentPlaceholder() {\n\t\tHashMap bindings = new HashMap<>();\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"({allowed: %permission_result%}).allowed\",\n\t\t\t\tplaceholder -> placeholder.equals(\"%permission_result%\") ? \"true\" : placeholder, bindings::put);\n\n\t\tassertEquals(\"({allowed: __advancedCorePlaceholder0}).allowed\", script);\n\t\tassertEquals(Boolean.TRUE, bindings.get(\"__advancedCorePlaceholder0\"));\n\t}\n\n""" - if marker not in t: - raise SystemExit('Unable to locate parser test insertion point') - t = t.replace(marker, additions + marker, 1) - tests.write_text(t) - PY - - name: Commit fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java - git rm .github/workflows/fix-codex-293-lexical.yml - git commit -m "Harden JavaScript lexical control tracking" - git push origin HEAD:security/javascript-placeholder-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 914e3bc82..4fca885f8 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -9,7 +9,7 @@ import java.util.regex.Pattern; final class JavascriptPlaceholderParser { - private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%|(? controlParens = new ArrayDeque<>(); + int lastControlHeadClose = -1; for (int i = 0; i < end; i++) { char current = script.charAt(i); @@ -158,7 +160,7 @@ private static Context contextAt(String script, int end) { i++; continue; } - if (current == '/' && startsRegexLiteral(script, i)) { + if (current == '/' && startsRegexLiteral(script, i, lastControlHeadClose)) { context = Context.REGEX; escaped = false; regexCharacterClass = false; @@ -179,6 +181,14 @@ private static Context contextAt(String script, int end) { context = Context.TEMPLATE_TEXT; continue; } + if (current == '(') { + int keywordEnd = previousNonWhitespace(script, i - 1); + controlParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd))); + } else if (current == ')' && !controlParens.isEmpty()) { + if (controlParens.pop()) { + lastControlHeadClose = i; + } + } if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { if (current == '{') { templates.peek().expressionDepth++; @@ -199,7 +209,7 @@ private static Context contextAt(String script, int end) { return context; } - private static boolean startsRegexLiteral(String script, int slashIndex) { + private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) { int previousIndex = previousNonWhitespace(script, slashIndex - 1); if (previousIndex < 0) { return true; @@ -211,7 +221,7 @@ private static boolean startsRegexLiteral(String script, int slashIndex) { if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { return true; } - if (previous == ')' && closesControlHead(script, previousIndex)) { + if (previous == ')' && previousIndex == lastControlHeadClose) { return true; } if (previous == '}' && closesStatementBlock(script, previousIndex)) { @@ -235,23 +245,6 @@ private static boolean isPostfixIncrementOrDecrement(String script, int operator || operand == '}'; } - private static boolean closesControlHead(String script, int closeParenIndex) { - int depth = 1; - for (int i = closeParenIndex - 1; i >= 0; i--) { - char current = script.charAt(i); - if (current == ')') { - depth++; - } else if (current == '(') { - depth--; - if (depth == 0) { - int keywordEnd = previousNonWhitespace(script, i - 1); - return CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)); - } - } - } - return false; - } - private static boolean closesStatementBlock(String script, int closeBraceIndex) { int depth = 1; for (int i = closeBraceIndex - 1; i >= 0; i--) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 9c563e556..5b0af3f91 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -133,6 +133,41 @@ void postfixIncrementBeforeDivisionDoesNotOpenRegexContext() { assertTrue(bindings.isEmpty()); } + @Test + void controlHeadIgnoresParenthesesInsideStrings() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("if (fn(\")\")) /[']/.test('x'); %untrusted%", + ignored -> injection, bindings::put); + + assertEquals("if (fn(\")\")) /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void controlHeadIgnoresParenthesesInsideRegexLiterals() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("if (/\\)/.test(value)) /[']/.test('x'); %untrusted%", + ignored -> injection, bindings::put); + + assertEquals("if (/\\)/.test(value)) /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void objectLiteralDoesNotConsumeNestedPercentPlaceholder() { + HashMap bindings = new HashMap<>(); + + String script = JavascriptPlaceholderParser.replace("({allowed: %permission_result%}).allowed", + placeholder -> placeholder.equals("%permission_result%") ? "true" : placeholder, bindings::put); + + assertEquals("({allowed: __advancedCorePlaceholder0}).allowed", script); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + @Test void objectLiteralFollowedByDivisionIsNotTreatedAsRegex() { HashMap bindings = new HashMap<>(); From 2d45d1726090a704937d447580c0734df72ab7a2 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:33:31 -0600 Subject: [PATCH 35/54] Test authored marker preservation with empty placeholders --- .../api/messages/PlaceholderUtilsSecurityTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java index a94ea06c0..aeee97eed 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java @@ -56,6 +56,13 @@ void operatorAuthoredJavascriptMarkerRemainsAvailable() { assertTrue(result.contains("Ben")); } + @Test + void emptyPlaceholderMapLeavesOperatorAuthoredMarkerUntouched() { + String result = PlaceholderUtils.replacePlaceHolder("[Javascript=1+1]", new HashMap<>()); + + assertTrue(result.contains("[Javascript=1+1]")); + } + @Test void normalPlaceholderFormattingIsPreserved() { HashMap placeholders = new HashMap<>(); From ba40d32a3d043c3259f67ff4095599f942b2c275 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:42:31 -0600 Subject: [PATCH 36/54] Apply Codex block-state fixes for PR 293 --- .../workflows/fix-codex-293-block-state.yml | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/fix-codex-293-block-state.yml diff --git a/.github/workflows/fix-codex-293-block-state.yml b/.github/workflows/fix-codex-293-block-state.yml new file mode 100644 index 000000000..d3edd97d1 --- /dev/null +++ b/.github/workflows/fix-codex-293-block-state.yml @@ -0,0 +1,123 @@ +name: Apply Codex 293 block-state fixes + +on: + push: + branches: + - security/javascript-placeholder-hardening + paths: + - .github/workflows/fix-codex-293-block-state.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + fetch-depth: 0 + - name: Patch parser and tests + run: | + python3 - <<'PY' + from pathlib import Path + import re + + parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') + text = parser.read_text() + + text = text.replace( + '\t\tDeque controlParens = new ArrayDeque<>();\n\t\tint lastControlHeadClose = -1;\n', + '\t\tDeque controlParens = new ArrayDeque<>();\n' + '\t\tDeque statementBraces = new ArrayDeque<>();\n' + '\t\tint lastControlHeadClose = -1;\n' + '\t\tint lastStatementBlockClose = -1;\n') + + text = text.replace( + 'startsRegexLiteral(script, i, lastControlHeadClose)', + 'startsRegexLiteral(script, i, lastControlHeadClose, lastStatementBlockClose)') + + text = text.replace( + '\t\t\tif (current == \'(\') {\n' + '\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n' + '\t\t\t\tcontrolParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)));\n' + '\t\t\t} else if (current == \')\' && !controlParens.isEmpty()) {\n' + '\t\t\t\tif (controlParens.pop()) {\n' + '\t\t\t\t\tlastControlHeadClose = i;\n' + '\t\t\t\t}\n' + '\t\t\t}\n', + '\t\t\tif (current == \'(\') {\n' + '\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n' + '\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd));\n' + '\t\t\t} else if (current == \')\' && !controlParens.isEmpty()) {\n' + '\t\t\t\tif (controlParens.pop()) {\n' + '\t\t\t\t\tlastControlHeadClose = i;\n' + '\t\t\t\t}\n' + '\t\t\t}\n' + '\t\t\tif (current == \'{\') {\n' + '\t\t\t\tstatementBraces.push(opensStatementBlock(script, i));\n' + '\t\t\t} else if (current == \'}\' && !statementBraces.isEmpty()) {\n' + '\t\t\t\tif (statementBraces.pop()) {\n' + '\t\t\t\t\tlastStatementBlockClose = i;\n' + '\t\t\t\t}\n' + '\t\t\t}\n') + + text = text.replace( + 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) {', + 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose,\n' + '\t\t\tint lastStatementBlockClose) {') + text = text.replace( + "\t\tif (previous == '}' && closesStatementBlock(script, previousIndex)) {\n\t\t\treturn true;\n\t\t}\n", + "\t\tif (previous == '}' && previousIndex == lastStatementBlockClose) {\n\t\t\treturn true;\n\t\t}\n") + + old_block = re.compile(r'\n\tprivate static boolean closesStatementBlock\(String script, int closeBraceIndex\) \{.*?\n\t\}\n\n\tprivate static int previousNonWhitespace', re.DOTALL) + replacement = ''' +\tprivate static boolean isStandaloneControlHead(String script, int keywordEnd) { +\t\tString keyword = previousIdentifier(script, keywordEnd); +\t\tif (!CONTROL_HEAD_KEYWORDS.contains(keyword)) { +\t\t\treturn false; +\t\t} +\t\tint keywordStart = keywordEnd - keyword.length() + 1; +\t\tint prefixIndex = previousNonWhitespace(script, keywordStart - 1); +\t\treturn prefixIndex < 0 || script.charAt(prefixIndex) != '.'; +\t} + +\tprivate static boolean opensStatementBlock(String script, int openBraceIndex) { +\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1); +\t\tif (prefixIndex < 0) { +\t\t\treturn true; +\t\t} +\t\tchar prefix = script.charAt(prefixIndex); +\t\tif (prefix == ')' || prefix == '}' || prefix == ';' || prefix == '{') { +\t\t\treturn true; +\t\t} +\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { +\t\t\treturn true; +\t\t} +\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); +\t} + +\tprivate static int previousNonWhitespace''' + text, count = old_block.subn(replacement, text, count=1) + if count != 1: + raise SystemExit('Unable to replace statement-block scanner') + parser.write_text(text) + + tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') + t = tests.read_text() + marker = '''\t@Test\n\tvoid objectLiteralFollowedByDivisionIsNotTreatedAsRegex() {\n''' + additions = '''\t@Test\n\tvoid keywordNamedMemberCallIsNotTreatedAsControlHead() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"'; allowed=true; '\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"obj.if(true) / 2; '%untrusted%'\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"obj.if(true) / 2; '\\\\'; allowed=true; \\\\''\", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid bracesInsideObjectLiteralStringsDoNotCreateStatementBlockContext() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"'; allowed=true; '\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"var x={a:\\\"){\\\"} / 2; '%untrusted%'\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"var x={a:\\\"){\\\"} / 2; '\\\\'; allowed=true; \\\\''\", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n''' + if marker not in t: + raise SystemExit('Unable to locate parser test insertion point') + t = t.replace(marker, additions + marker, 1) + tests.write_text(t) + PY + - name: Commit fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java + git rm .github/workflows/fix-codex-293-block-state.yml + git commit -m "Track JavaScript block context lexically" + git push origin HEAD:security/javascript-placeholder-hardening From d5f96cb599151b4b6570ee3dd97bf1ee94f39c7c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:44:31 -0600 Subject: [PATCH 37/54] Track statement blocks during JavaScript lexing --- .../JavascriptPlaceholderParser.java | 94 ++++++++++++------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 4fca885f8..76032bc5f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -16,7 +16,7 @@ final class JavascriptPlaceholderParser { private static final Set REGEX_PREFIX_KEYWORDS = Set.of("return", "throw", "case", "delete", "void", "typeof", "instanceof", "in", "of", "new", "yield", "await", "else", "do"); private static final Set CONTROL_HEAD_KEYWORDS = Set.of("if", "while", "for", "with", "switch", "catch"); - private static final Set BLOCK_PREFIX_KEYWORDS = Set.of("else", "do", "try", "finally"); + private static final Set BLOCK_PREFIX_KEYWORDS = Set.of("else", "do", "try", "finally", "static"); private JavascriptPlaceholderParser() { } @@ -72,11 +72,13 @@ private static Object coercePrimitive(String value) { private static Context contextAt(String script, int end) { Deque templates = new ArrayDeque<>(); + Deque controlParens = new ArrayDeque<>(); + Deque statementBraces = new ArrayDeque<>(); Context context = Context.CODE; boolean escaped = false; boolean regexCharacterClass = false; - Deque controlParens = new ArrayDeque<>(); int lastControlHeadClose = -1; + int lastStatementBlockClose = -1; for (int i = 0; i < end; i++) { char current = script.charAt(i); @@ -160,7 +162,7 @@ private static Context contextAt(String script, int end) { i++; continue; } - if (current == '/' && startsRegexLiteral(script, i, lastControlHeadClose)) { + if (current == '/' && startsRegexLiteral(script, i, lastControlHeadClose, lastStatementBlockClose)) { context = Context.REGEX; escaped = false; regexCharacterClass = false; @@ -183,16 +185,24 @@ private static Context contextAt(String script, int end) { } if (current == '(') { int keywordEnd = previousNonWhitespace(script, i - 1); - controlParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd))); + controlParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces)); } else if (current == ')' && !controlParens.isEmpty()) { if (controlParens.pop()) { lastControlHeadClose = i; } } - if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { - if (current == '{') { + + if (current == '{') { + statementBraces.push(opensStatementBlock(script, i)); + if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { templates.peek().expressionDepth++; - } else if (current == '}') { + } + } else if (current == '}') { + boolean closesTemplateExpression = !templates.isEmpty() && templates.peek().expressionDepth == 1; + if (!closesTemplateExpression && !statementBraces.isEmpty() && statementBraces.pop()) { + lastStatementBlockClose = i; + } + if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { templates.peek().expressionDepth--; if (templates.peek().expressionDepth == 0) { context = Context.TEMPLATE_TEXT; @@ -209,7 +219,8 @@ private static Context contextAt(String script, int end) { return context; } - private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) { + private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose, + int lastStatementBlockClose) { int previousIndex = previousNonWhitespace(script, slashIndex - 1); if (previousIndex < 0) { return true; @@ -224,13 +235,30 @@ private static boolean startsRegexLiteral(String script, int slashIndex, int las if (previous == ')' && previousIndex == lastControlHeadClose) { return true; } - if (previous == '}' && closesStatementBlock(script, previousIndex)) { + if (previous == '}' && previousIndex == lastStatementBlockClose) { return true; } String previousWord = previousIdentifier(script, previousIndex); return REGEX_PREFIX_KEYWORDS.contains(previousWord); } + private static boolean isStandaloneControlHead(String script, int keywordEnd, Deque statementBraces) { + String keyword = previousIdentifier(script, keywordEnd); + if (!CONTROL_HEAD_KEYWORDS.contains(keyword)) { + return false; + } + int keywordStart = identifierStart(script, keywordEnd); + int beforeKeyword = previousNonWhitespace(script, keywordStart - 1); + if (beforeKeyword >= 0 && script.charAt(beforeKeyword) == '.') { + return false; + } + // A control statement cannot occur directly at object-literal/class-member level. + // This prevents method/property names such as { if() {} } from being mistaken + // for statement keywords while still allowing control statements inside a + // nested method/function body (whose brace is classified as a statement block). + return statementBraces.isEmpty() || statementBraces.peek(); + } + private static boolean isPostfixIncrementOrDecrement(String script, int operatorEndIndex) { char operator = script.charAt(operatorEndIndex); if (operatorEndIndex == 0 || script.charAt(operatorEndIndex - 1) != operator) { @@ -245,31 +273,19 @@ private static boolean isPostfixIncrementOrDecrement(String script, int operator || operand == '}'; } - private static boolean closesStatementBlock(String script, int closeBraceIndex) { - int depth = 1; - for (int i = closeBraceIndex - 1; i >= 0; i--) { - char current = script.charAt(i); - if (current == '}') { - depth++; - } else if (current == '{') { - depth--; - if (depth == 0) { - int prefixIndex = previousNonWhitespace(script, i - 1); - if (prefixIndex < 0) { - return true; - } - char prefix = script.charAt(prefixIndex); - if (prefix == ')' || prefix == '}' || prefix == ';') { - return true; - } - if (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { - return true; - } - return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); - } - } + private static boolean opensStatementBlock(String script, int openBraceIndex) { + int prefixIndex = previousNonWhitespace(script, openBraceIndex - 1); + if (prefixIndex < 0) { + return true; + } + char prefix = script.charAt(prefixIndex); + if (prefix == ')' || prefix == '}' || prefix == ';') { + return true; + } + if (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { + return true; } - return false; + return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); } private static int previousNonWhitespace(String script, int index) { @@ -281,14 +297,22 @@ private static int previousNonWhitespace(String script, int index) { return -1; } - private static String previousIdentifier(String script, int endIndex) { + private static int identifierStart(String script, int endIndex) { if (endIndex < 0 || !Character.isJavaIdentifierPart(script.charAt(endIndex))) { - return ""; + return endIndex + 1; } int start = endIndex; while (start > 0 && Character.isJavaIdentifierPart(script.charAt(start - 1))) { start--; } + return start; + } + + private static String previousIdentifier(String script, int endIndex) { + if (endIndex < 0 || !Character.isJavaIdentifierPart(script.charAt(endIndex))) { + return ""; + } + int start = identifierStart(script, endIndex); return script.substring(start, endIndex + 1); } From de7ddf8458a8eef2fe7f010224a039e1c2fc80a8 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:45:07 -0600 Subject: [PATCH 38/54] Test lexical statement block detection --- .../JavascriptPlaceholderParserTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 5b0af3f91..d80285b4c 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -157,6 +157,30 @@ void controlHeadIgnoresParenthesesInsideRegexLiterals() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void memberNamedIfDoesNotCreateControlHeadRegexContext() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace("obj.if(true) / 2; '%untrusted%'", ignored -> injection, + bindings::put); + + assertEquals("obj.if(true) / 2; '\\'; allowed=true; \\''", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void objectLiteralStringBraceDoesNotBecomeStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace("var x={a:\"){\"} / 2; '%untrusted%'", ignored -> injection, + bindings::put); + + assertEquals("var x={a:\"){\"} / 2; '\\'; allowed=true; \\''", script); + assertTrue(bindings.isEmpty()); + } + @Test void objectLiteralDoesNotConsumeNestedPercentPlaceholder() { HashMap bindings = new HashMap<>(); From b457a19729256b29789669f3a51376fbd6ee10f6 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:47:05 -0600 Subject: [PATCH 39/54] Remove temporary Codex patch workflow --- .../workflows/fix-codex-293-block-state.yml | 123 ------------------ 1 file changed, 123 deletions(-) delete mode 100644 .github/workflows/fix-codex-293-block-state.yml diff --git a/.github/workflows/fix-codex-293-block-state.yml b/.github/workflows/fix-codex-293-block-state.yml deleted file mode 100644 index d3edd97d1..000000000 --- a/.github/workflows/fix-codex-293-block-state.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Apply Codex 293 block-state fixes - -on: - push: - branches: - - security/javascript-placeholder-hardening - paths: - - .github/workflows/fix-codex-293-block-state.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - fetch-depth: 0 - - name: Patch parser and tests - run: | - python3 - <<'PY' - from pathlib import Path - import re - - parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') - text = parser.read_text() - - text = text.replace( - '\t\tDeque controlParens = new ArrayDeque<>();\n\t\tint lastControlHeadClose = -1;\n', - '\t\tDeque controlParens = new ArrayDeque<>();\n' - '\t\tDeque statementBraces = new ArrayDeque<>();\n' - '\t\tint lastControlHeadClose = -1;\n' - '\t\tint lastStatementBlockClose = -1;\n') - - text = text.replace( - 'startsRegexLiteral(script, i, lastControlHeadClose)', - 'startsRegexLiteral(script, i, lastControlHeadClose, lastStatementBlockClose)') - - text = text.replace( - '\t\t\tif (current == \'(\') {\n' - '\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n' - '\t\t\t\tcontrolParens.push(CONTROL_HEAD_KEYWORDS.contains(previousIdentifier(script, keywordEnd)));\n' - '\t\t\t} else if (current == \')\' && !controlParens.isEmpty()) {\n' - '\t\t\t\tif (controlParens.pop()) {\n' - '\t\t\t\t\tlastControlHeadClose = i;\n' - '\t\t\t\t}\n' - '\t\t\t}\n', - '\t\t\tif (current == \'(\') {\n' - '\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n' - '\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd));\n' - '\t\t\t} else if (current == \')\' && !controlParens.isEmpty()) {\n' - '\t\t\t\tif (controlParens.pop()) {\n' - '\t\t\t\t\tlastControlHeadClose = i;\n' - '\t\t\t\t}\n' - '\t\t\t}\n' - '\t\t\tif (current == \'{\') {\n' - '\t\t\t\tstatementBraces.push(opensStatementBlock(script, i));\n' - '\t\t\t} else if (current == \'}\' && !statementBraces.isEmpty()) {\n' - '\t\t\t\tif (statementBraces.pop()) {\n' - '\t\t\t\t\tlastStatementBlockClose = i;\n' - '\t\t\t\t}\n' - '\t\t\t}\n') - - text = text.replace( - 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose) {', - 'private static boolean startsRegexLiteral(String script, int slashIndex, int lastControlHeadClose,\n' - '\t\t\tint lastStatementBlockClose) {') - text = text.replace( - "\t\tif (previous == '}' && closesStatementBlock(script, previousIndex)) {\n\t\t\treturn true;\n\t\t}\n", - "\t\tif (previous == '}' && previousIndex == lastStatementBlockClose) {\n\t\t\treturn true;\n\t\t}\n") - - old_block = re.compile(r'\n\tprivate static boolean closesStatementBlock\(String script, int closeBraceIndex\) \{.*?\n\t\}\n\n\tprivate static int previousNonWhitespace', re.DOTALL) - replacement = ''' -\tprivate static boolean isStandaloneControlHead(String script, int keywordEnd) { -\t\tString keyword = previousIdentifier(script, keywordEnd); -\t\tif (!CONTROL_HEAD_KEYWORDS.contains(keyword)) { -\t\t\treturn false; -\t\t} -\t\tint keywordStart = keywordEnd - keyword.length() + 1; -\t\tint prefixIndex = previousNonWhitespace(script, keywordStart - 1); -\t\treturn prefixIndex < 0 || script.charAt(prefixIndex) != '.'; -\t} - -\tprivate static boolean opensStatementBlock(String script, int openBraceIndex) { -\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1); -\t\tif (prefixIndex < 0) { -\t\t\treturn true; -\t\t} -\t\tchar prefix = script.charAt(prefixIndex); -\t\tif (prefix == ')' || prefix == '}' || prefix == ';' || prefix == '{') { -\t\t\treturn true; -\t\t} -\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { -\t\t\treturn true; -\t\t} -\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); -\t} - -\tprivate static int previousNonWhitespace''' - text, count = old_block.subn(replacement, text, count=1) - if count != 1: - raise SystemExit('Unable to replace statement-block scanner') - parser.write_text(text) - - tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') - t = tests.read_text() - marker = '''\t@Test\n\tvoid objectLiteralFollowedByDivisionIsNotTreatedAsRegex() {\n''' - additions = '''\t@Test\n\tvoid keywordNamedMemberCallIsNotTreatedAsControlHead() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"'; allowed=true; '\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"obj.if(true) / 2; '%untrusted%'\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"obj.if(true) / 2; '\\\\'; allowed=true; \\\\''\", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid bracesInsideObjectLiteralStringsDoNotCreateStatementBlockContext() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = \"'; allowed=true; '\";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\"var x={a:\\\"){\\\"} / 2; '%untrusted%'\",\n\t\t\t\tignored -> injection, bindings::put);\n\n\t\tassertEquals(\"var x={a:\\\"){\\\"} / 2; '\\\\'; allowed=true; \\\\''\", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n''' - if marker not in t: - raise SystemExit('Unable to locate parser test insertion point') - t = t.replace(marker, additions + marker, 1) - tests.write_text(t) - PY - - name: Commit fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java - git rm .github/workflows/fix-codex-293-block-state.yml - git commit -m "Track JavaScript block context lexically" - git push origin HEAD:security/javascript-placeholder-hardening From 7128d79b3d4aae1f4d30722613eb9ed15bcb08a5 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:47:39 -0600 Subject: [PATCH 40/54] Isolate statement block regression resolver --- .../api/javascript/JavascriptPlaceholderParserTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index d80285b4c..7bb56ba25 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -174,8 +174,8 @@ void objectLiteralStringBraceDoesNotBecomeStatementBlock() { HashMap bindings = new HashMap<>(); String injection = "'; allowed=true; '"; - String script = JavascriptPlaceholderParser.replace("var x={a:\"){\"} / 2; '%untrusted%'", ignored -> injection, - bindings::put); + String script = JavascriptPlaceholderParser.replace("var x={a:\"){\"} / 2; '%untrusted%'", + placeholder -> placeholder.equals("%untrusted%") ? injection : placeholder, bindings::put); assertEquals("var x={a:\"){\"} / 2; '\\'; allowed=true; \\''", script); assertTrue(bindings.isEmpty()); From fb81b86d93a72194f8d1283fccdf3a0b94d3f991 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:19:25 -0600 Subject: [PATCH 41/54] Encode map values passed through JavaScript markers --- .../api/javascript/JavascriptSafeValue.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptSafeValue.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptSafeValue.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptSafeValue.java new file mode 100644 index 000000000..6e631c2aa --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptSafeValue.java @@ -0,0 +1,37 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Encodes values that must survive an earlier placeholder pass but still be + * treated as data by the JavaScript placeholder parser. + */ +public final class JavascriptSafeValue { + private static final String PREFIX = "__advancedcore_js_value:"; + + private JavascriptSafeValue() { + } + + public static String encodePlaceholder(String value) { + String encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString((value == null ? "" : value).getBytes(StandardCharsets.UTF_8)); + return "{" + PREFIX + encoded + "}"; + } + + static String decodePlaceholder(String placeholder) { + if (placeholder == null || placeholder.length() < 2 || placeholder.charAt(0) != '{' + || placeholder.charAt(placeholder.length() - 1) != '}') { + return null; + } + String value = placeholder.substring(1, placeholder.length() - 1); + if (!value.startsWith(PREFIX)) { + return null; + } + try { + return new String(Base64.getUrlDecoder().decode(value.substring(PREFIX.length())), StandardCharsets.UTF_8); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} From b60eb4248da861f7faaa7d6cf6ae2f1cbcbf5840 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:20:17 -0600 Subject: [PATCH 42/54] Treat labeled JavaScript blocks as statement context --- .../JavascriptPlaceholderParser.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 76032bc5f..99639fd05 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -27,7 +27,10 @@ static String replace(String script, Function resolver, BiConsum int index = 0; while (matcher.find()) { String placeholder = matcher.group(); - String value = resolver.apply(placeholder); + String value = JavascriptSafeValue.decodePlaceholder(placeholder); + if (value == null) { + value = resolver.apply(placeholder); + } if (value == null || value.equals(placeholder)) { matcher.appendReplacement(result, Matcher.quoteReplacement(placeholder)); continue; @@ -193,7 +196,7 @@ private static Context contextAt(String script, int end) { } if (current == '{') { - statementBraces.push(opensStatementBlock(script, i)); + statementBraces.push(opensStatementBlock(script, i, statementBraces)); if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { templates.peek().expressionDepth++; } @@ -252,10 +255,6 @@ private static boolean isStandaloneControlHead(String script, int keywordEnd, De if (beforeKeyword >= 0 && script.charAt(beforeKeyword) == '.') { return false; } - // A control statement cannot occur directly at object-literal/class-member level. - // This prevents method/property names such as { if() {} } from being mistaken - // for statement keywords while still allowing control statements inside a - // nested method/function body (whose brace is classified as a statement block). return statementBraces.isEmpty() || statementBraces.peek(); } @@ -273,7 +272,7 @@ private static boolean isPostfixIncrementOrDecrement(String script, int operator || operand == '}'; } - private static boolean opensStatementBlock(String script, int openBraceIndex) { + private static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) { int prefixIndex = previousNonWhitespace(script, openBraceIndex - 1); if (prefixIndex < 0) { return true; @@ -282,12 +281,36 @@ private static boolean opensStatementBlock(String script, int openBraceIndex) { if (prefix == ')' || prefix == '}' || prefix == ';') { return true; } + if (prefix == ':') { + return followsStatementLabel(script, prefixIndex, statementBraces); + } if (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { return true; } return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); } + private static boolean followsStatementLabel(String script, int colonIndex, Deque statementBraces) { + int labelEnd = previousNonWhitespace(script, colonIndex - 1); + if (labelEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(labelEnd))) { + return false; + } + int labelStart = identifierStart(script, labelEnd); + int beforeLabel = previousNonWhitespace(script, labelStart - 1); + if (beforeLabel < 0) { + return true; + } + char previous = script.charAt(beforeLabel); + if (previous == ';' || previous == '}') { + return true; + } + if (previous == '{') { + return statementBraces.isEmpty() || statementBraces.peek(); + } + String previousWord = previousIdentifier(script, beforeLabel); + return previousWord.equals("case"); + } + private static int previousNonWhitespace(String script, int index) { for (int i = index; i >= 0; i--) { if (!Character.isWhitespace(script.charAt(i))) { From 457ebb5f88c619fcd65ad7744bd7555a2845de30 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:20:27 -0600 Subject: [PATCH 43/54] Add focused Codex 293 patch workflow --- .github/workflows/fix-codex-293-map-label.yml | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/fix-codex-293-map-label.yml diff --git a/.github/workflows/fix-codex-293-map-label.yml b/.github/workflows/fix-codex-293-map-label.yml new file mode 100644 index 000000000..df5007508 --- /dev/null +++ b/.github/workflows/fix-codex-293-map-label.yml @@ -0,0 +1,59 @@ +name: Apply Codex 293 map and label fixes + +on: + push: + branches: + - security/javascript-placeholder-hardening + paths: + - .github/workflows/fix-codex-293-map-label.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + fetch-depth: 0 + - name: Patch safe map Javascript flow and labeled blocks + run: | + python3 - <<'PY' + from pathlib import Path + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java') + text = p.read_text() + old = '''\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list) {\n\t\tArrayList msg = new ArrayList<>();\n\t\tfor (String str : list) {\n\t\t\tmsg.add(replaceJavascript(player, str));\n\t\t}\n\t\treturn msg;\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text) {\n\t\tif (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) {\n\t\t\tif (player.isOnline()) {\n\t\t\t\treturn replaceJavascript(player.getPlayer(), text);\n\t\t\t}\n\t\t\tJavascriptEngine engine = new JavascriptEngine().addPlayer(player);\n\t\t\tString parsed = replaceJavascript(text, engine, player, null);\n\t\t\treturn replacePlaceHolders(player, parsed);\n\t\t}\n\t\treturn replacePlaceHolders(player, text);\n\t}\n''' + new = '''\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list) {\n\t\treturn replaceJavascript(player, list, null);\n\t}\n\n\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list,\n\t\t\tHashMap placeholders) {\n\t\tArrayList msg = new ArrayList<>();\n\t\tfor (String str : list) {\n\t\t\tmsg.add(replaceJavascript(player, str, placeholders));\n\t\t}\n\t\treturn msg;\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text) {\n\t\treturn replaceJavascript(player, text, null);\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text, HashMap placeholders) {\n\t\tString parsed = text;\n\t\tif (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) {\n\t\t\tJavascriptEngine engine = player.isOnline() ? new JavascriptEngine().addPlayer(player.getPlayer())\n\t\t\t\t\t: new JavascriptEngine().addPlayer(player);\n\t\t\tparsed = replaceJavascript(text, engine, player, placeholders);\n\t\t}\n\t\tparsed = replacePlaceHolder(parsed, placeholders);\n\t\treturn replacePlaceHolders(player, parsed);\n\t}\n''' + if old not in text: + raise SystemExit('PlaceholderUtils offline Javascript block not found') + text = text.replace(old, new, 1) + text = text.replace('''\tprivate static String replaceJavascript(String text, JavascriptEngine engine, OfflinePlayer player,\n\t\t\tHashMap placeholders) {''', '''\tstatic String replaceJavascript(String text, JavascriptEngine engine, OfflinePlayer player,\n\t\t\tHashMap placeholders) {''', 1) + p.write_text(text) + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java') + text = p.read_text() + old = '''\t\tsetName(MessageAPI.colorize(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\tPlaceholderUtils.replacePlaceHolder(getName(), placeholders))));\n\t\tsetLore(ArrayUtils.colorize(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\tPlaceholderUtils.replacePlaceHolder(getLore(), placeholders))));\n\t\tif (skull.contains("%")) {\n\t\t\tsetSkullOwner(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\t\tPlaceholderUtils.replacePlaceHolder(skull, placeholders)));\n\t\t}\n''' + new = '''\t\tsetName(MessageAPI.colorize(PlaceholderUtils.replaceJavascript(player, getName(), placeholders)));\n\t\tsetLore(ArrayUtils.colorize(PlaceholderUtils.replaceJavascript(player, getLore(), placeholders)));\n\t\tif (skull.contains("%")) {\n\t\t\tsetSkullOwner(PlaceholderUtils.replaceJavascript(player, skull, placeholders));\n\t\t}\n''' + if old not in text: + raise SystemExit('ItemBuilder parsePlaceholders block not found') + p.write_text(text.replace(old, new, 1)) + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') + text = p.read_text() + text = text.replace('statementBraces.push(opensStatementBlock(script, i));', + 'statementBraces.push(opensStatementBlock(script, i, statementBraces));', 1) + old = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n''' + new = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn isStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean isStatementLabel(String script, int colonIndex, Deque statementBraces) {\n\t\tif (!statementBraces.isEmpty() && !statementBraces.peek()) {\n\t\t\treturn false;\n\t\t}\n\t\tint labelEnd = previousNonWhitespace(script, colonIndex - 1);\n\t\tif (labelEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(labelEnd))) {\n\t\t\treturn false;\n\t\t}\n\t\tint labelStart = identifierStart(script, labelEnd);\n\t\tint beforeLabel = previousNonWhitespace(script, labelStart - 1);\n\t\tif (beforeLabel < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar before = script.charAt(beforeLabel);\n\t\treturn before == '{' || before == '}' || before == ';' || before == ':';\n\t}\n''' + if old not in text: + raise SystemExit('opensStatementBlock block not found') + p.write_text(text.replace(old, new, 1)) + PY + rm .github/workflows/fix-codex-293-map-label.yml + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add -A + git commit -m "Route map values through safe Javascript parsing" + git push origin HEAD:security/javascript-placeholder-hardening From a4bbbc93a3ac174489caed469394c08369683f74 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:21:21 -0600 Subject: [PATCH 44/54] Keep map placeholder values out of JavaScript source --- .../api/messages/PlaceholderUtils.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 8937d5832..5cbfbbfe6 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 @@ -10,6 +10,7 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; +import com.bencodez.advancedcore.api.javascript.JavascriptSafeValue; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.simpleapi.messages.MessageAPI; @@ -297,6 +298,7 @@ public static String replacePlaceHolder(String str, HashMap plac if (str == null || placeholders == null || placeholders.isEmpty()) { return str; } + str = encodeJavascriptMarkerMapValues(str, placeholders, true); String protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); str = str.replace(JAVASCRIPT_MARKER, protectedMarker); for (Entry entry : placeholders.entrySet()) { @@ -310,6 +312,7 @@ public static String replacePlaceHolder(String str, HashMap plac if (str == null || placeholders == null || placeholders.isEmpty()) { return str; } + str = encodeJavascriptMarkerMapValues(str, placeholders, ignoreCase); String protectedMarker = createProtectedJavascriptMarkerToken(str, placeholders); str = str.replace(JAVASCRIPT_MARKER, protectedMarker); for (Entry entry : placeholders.entrySet()) { @@ -346,6 +349,39 @@ static String neutralizeJavascriptMarker(String value) { return value.replace(JAVASCRIPT_MARKER, SAFE_JAVASCRIPT_MARKER); } + private static String encodeJavascriptMarkerMapValues(String text, HashMap placeholders, + boolean ignoreCase) { + StringBuilder result = new StringBuilder(text.length()); + int index = 0; + while (index < text.length()) { + int start = text.indexOf(JAVASCRIPT_MARKER, index); + if (start < 0) { + result.append(text, index, text.length()); + break; + } + result.append(text, index, start); + int end = text.indexOf(']', start + JAVASCRIPT_MARKER.length()); + if (end < 0) { + result.append(text, start, text.length()); + break; + } + String script = text.substring(start + JAVASCRIPT_MARKER.length(), end); + for (Entry entry : placeholders.entrySet()) { + String safeValue = JavascriptSafeValue.encodePlaceholder(entry.getValue()); + if (ignoreCase) { + script = MessageAPI.replaceIgnoreCase(script, "%" + entry.getKey() + "%", safeValue); + script = MessageAPI.replaceIgnoreCase(script, "\\{" + entry.getKey() + "\\}", safeValue); + } else { + script = script.replace("%" + entry.getKey() + "%", safeValue); + script = script.replace("{" + entry.getKey() + "}", safeValue); + } + } + result.append(JAVASCRIPT_MARKER).append(script).append(']'); + index = end + 1; + } + return result.toString(); + } + private static String createProtectedJavascriptMarkerToken(String text, HashMap placeholders) { String token; boolean collision; From d733b0c5f6e300c6032830d8b9444c6ce90a90fc Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:21:40 -0600 Subject: [PATCH 45/54] Test map values inside JavaScript markers stay data --- .../api/messages/PlaceholderUtilsSecurityTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java index aeee97eed..780da6a4e 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsSecurityTest.java @@ -56,6 +56,20 @@ void operatorAuthoredJavascriptMarkerRemainsAvailable() { assertTrue(result.contains("Ben")); } + @Test + void mapValueInsideAuthoredJavascriptMarkerIsNotCopiedIntoSource() { + HashMap placeholders = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, \"op attacker\"); '"; + placeholders.put("displayname", injection); + + String result = PlaceholderUtils.replacePlaceHolder("[Javascript='%displayname%']", placeholders); + + assertTrue(result.startsWith("[Javascript='")); + assertFalse(result.contains("Bukkit.dispatchCommand")); + assertFalse(result.contains(injection)); + assertTrue(result.contains("__advancedcore_js_value:")); + } + @Test void emptyPlaceholderMapLeavesOperatorAuthoredMarkerUntouched() { String result = PlaceholderUtils.replacePlaceHolder("[Javascript=1+1]", new HashMap<>()); From 88f4090a496ee957634483c46b5e5e93961c8ca9 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:22:21 -0600 Subject: [PATCH 46/54] Test labeled blocks and encoded JavaScript values --- .../JavascriptPlaceholderParserTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 7bb56ba25..03137138b 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -121,6 +121,30 @@ void recognizesRegexLiteralAfterStatementBlock() { assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); } + @Test + void recognizesRegexLiteralAfterLabeledStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace("label: {} /[']/.test('x'); %untrusted%", ignored -> injection, + bindings::put); + + assertEquals("label: {} /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void nestedObjectPropertyBlockDoesNotBecomeStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace("var x={a: {}} / 2; '%untrusted%'", + ignored -> injection, bindings::put); + + assertEquals("var x={a: {}} / 2; '\\'; allowed=true; \\''", script); + assertTrue(bindings.isEmpty()); + } + @Test void postfixIncrementBeforeDivisionDoesNotOpenRegexContext() { HashMap bindings = new HashMap<>(); @@ -234,6 +258,18 @@ void escapesPlaceholderValuesInsideRegexCharacterClasses() { assertTrue(bindings.isEmpty()); } + @Test + void encodedMapValueIsDecodedAndBoundAsData() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, \"op attacker\"); '"; + String encoded = JavascriptSafeValue.encodePlaceholder(injection); + + String script = JavascriptPlaceholderParser.replace(encoded, value -> value, bindings::put); + + assertEquals("__advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + @Test void preservesBooleanAndNumericPlaceholderTypes() { HashMap bindings = new HashMap<>(); From ddd05e2a5a649e6aa6f6cbd925199944cd676fb3 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:23:18 -0600 Subject: [PATCH 47/54] Remove obsolete Codex 293 patch workflow --- .github/workflows/fix-codex-293-map-label.yml | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 .github/workflows/fix-codex-293-map-label.yml diff --git a/.github/workflows/fix-codex-293-map-label.yml b/.github/workflows/fix-codex-293-map-label.yml deleted file mode 100644 index df5007508..000000000 --- a/.github/workflows/fix-codex-293-map-label.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Apply Codex 293 map and label fixes - -on: - push: - branches: - - security/javascript-placeholder-hardening - paths: - - .github/workflows/fix-codex-293-map-label.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - fetch-depth: 0 - - name: Patch safe map Javascript flow and labeled blocks - run: | - python3 - <<'PY' - from pathlib import Path - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java') - text = p.read_text() - old = '''\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list) {\n\t\tArrayList msg = new ArrayList<>();\n\t\tfor (String str : list) {\n\t\t\tmsg.add(replaceJavascript(player, str));\n\t\t}\n\t\treturn msg;\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text) {\n\t\tif (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) {\n\t\t\tif (player.isOnline()) {\n\t\t\t\treturn replaceJavascript(player.getPlayer(), text);\n\t\t\t}\n\t\t\tJavascriptEngine engine = new JavascriptEngine().addPlayer(player);\n\t\t\tString parsed = replaceJavascript(text, engine, player, null);\n\t\t\treturn replacePlaceHolders(player, parsed);\n\t\t}\n\t\treturn replacePlaceHolders(player, text);\n\t}\n''' - new = '''\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list) {\n\t\treturn replaceJavascript(player, list, null);\n\t}\n\n\tpublic static ArrayList replaceJavascript(OfflinePlayer player, ArrayList list,\n\t\t\tHashMap placeholders) {\n\t\tArrayList msg = new ArrayList<>();\n\t\tfor (String str : list) {\n\t\t\tmsg.add(replaceJavascript(player, str, placeholders));\n\t\t}\n\t\treturn msg;\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text) {\n\t\treturn replaceJavascript(player, text, null);\n\t}\n\n\tpublic static String replaceJavascript(OfflinePlayer player, String text, HashMap placeholders) {\n\t\tString parsed = text;\n\t\tif (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) {\n\t\t\tJavascriptEngine engine = player.isOnline() ? new JavascriptEngine().addPlayer(player.getPlayer())\n\t\t\t\t\t: new JavascriptEngine().addPlayer(player);\n\t\t\tparsed = replaceJavascript(text, engine, player, placeholders);\n\t\t}\n\t\tparsed = replacePlaceHolder(parsed, placeholders);\n\t\treturn replacePlaceHolders(player, parsed);\n\t}\n''' - if old not in text: - raise SystemExit('PlaceholderUtils offline Javascript block not found') - text = text.replace(old, new, 1) - text = text.replace('''\tprivate static String replaceJavascript(String text, JavascriptEngine engine, OfflinePlayer player,\n\t\t\tHashMap placeholders) {''', '''\tstatic String replaceJavascript(String text, JavascriptEngine engine, OfflinePlayer player,\n\t\t\tHashMap placeholders) {''', 1) - p.write_text(text) - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java') - text = p.read_text() - old = '''\t\tsetName(MessageAPI.colorize(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\tPlaceholderUtils.replacePlaceHolder(getName(), placeholders))));\n\t\tsetLore(ArrayUtils.colorize(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\tPlaceholderUtils.replacePlaceHolder(getLore(), placeholders))));\n\t\tif (skull.contains("%")) {\n\t\t\tsetSkullOwner(PlaceholderUtils.replaceJavascript(player,\n\t\t\t\t\tPlaceholderUtils.replacePlaceHolder(skull, placeholders)));\n\t\t}\n''' - new = '''\t\tsetName(MessageAPI.colorize(PlaceholderUtils.replaceJavascript(player, getName(), placeholders)));\n\t\tsetLore(ArrayUtils.colorize(PlaceholderUtils.replaceJavascript(player, getLore(), placeholders)));\n\t\tif (skull.contains("%")) {\n\t\t\tsetSkullOwner(PlaceholderUtils.replaceJavascript(player, skull, placeholders));\n\t\t}\n''' - if old not in text: - raise SystemExit('ItemBuilder parsePlaceholders block not found') - p.write_text(text.replace(old, new, 1)) - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') - text = p.read_text() - text = text.replace('statementBraces.push(opensStatementBlock(script, i));', - 'statementBraces.push(opensStatementBlock(script, i, statementBraces));', 1) - old = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n''' - new = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn isStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean isStatementLabel(String script, int colonIndex, Deque statementBraces) {\n\t\tif (!statementBraces.isEmpty() && !statementBraces.peek()) {\n\t\t\treturn false;\n\t\t}\n\t\tint labelEnd = previousNonWhitespace(script, colonIndex - 1);\n\t\tif (labelEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(labelEnd))) {\n\t\t\treturn false;\n\t\t}\n\t\tint labelStart = identifierStart(script, labelEnd);\n\t\tint beforeLabel = previousNonWhitespace(script, labelStart - 1);\n\t\tif (beforeLabel < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar before = script.charAt(beforeLabel);\n\t\treturn before == '{' || before == '}' || before == ';' || before == ':';\n\t}\n''' - if old not in text: - raise SystemExit('opensStatementBlock block not found') - p.write_text(text.replace(old, new, 1)) - PY - rm .github/workflows/fix-codex-293-map-label.yml - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git add -A - git commit -m "Route map values through safe Javascript parsing" - git push origin HEAD:security/javascript-placeholder-hardening From 84fe2486bc55f3e42bcdfcdf766afb2e9c120023 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 17:48:41 -0600 Subject: [PATCH 48/54] Apply Codex function-expression lexer fix --- ...ix-codex-293-function-expression-block.yml | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/fix-codex-293-function-expression-block.yml diff --git a/.github/workflows/fix-codex-293-function-expression-block.yml b/.github/workflows/fix-codex-293-function-expression-block.yml new file mode 100644 index 000000000..f666d0cc0 --- /dev/null +++ b/.github/workflows/fix-codex-293-function-expression-block.yml @@ -0,0 +1,63 @@ +name: Apply Codex 293 function-expression lexer fix + +on: + push: + branches: + - security/javascript-placeholder-hardening + paths: + - .github/workflows/fix-codex-293-function-expression-block.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + fetch-depth: 0 + - name: Distinguish expression bodies from statement blocks + run: | + python3 - <<'PY' + from pathlib import Path + + parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') + text = parser.read_text() + + text = text.replace( + 'Deque controlParens = new ArrayDeque<>();\n\t\tDeque statementBraces = new ArrayDeque<>();', + 'Deque controlParens = new ArrayDeque<>();\n\t\tDeque functionExpressionParens = new ArrayDeque<>();\n\t\tDeque statementBraces = new ArrayDeque<>();', 1) + text = text.replace( + 'int lastControlHeadClose = -1;\n\t\tint lastStatementBlockClose = -1;', + 'int lastControlHeadClose = -1;\n\t\tint lastFunctionExpressionParenClose = -1;\n\t\tint lastStatementBlockClose = -1;', 1) + + old = '''\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (current == '{') {\n\t\t\t\tstatementBraces.push(opensStatementBlock(script, i, statementBraces));''' + new = '''\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces));\n\t\t\t\tfunctionExpressionParens.push(opensFunctionExpressionParameters(script, i, statementBraces));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t\tif (!functionExpressionParens.isEmpty() && functionExpressionParens.pop()) {\n\t\t\t\t\tlastFunctionExpressionParenClose = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (current == '{') {\n\t\t\t\tstatementBraces.push(opensStatementBlock(script, i, statementBraces, lastFunctionExpressionParenClose));''' + if old not in text: + raise SystemExit('paren/brace block not found') + text = text.replace(old, new, 1) + + old = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn followsStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n''' + new = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces,\n\t\t\tint lastFunctionExpressionParenClose) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')') {\n\t\t\treturn prefixIndex != lastFunctionExpressionParenClose;\n\t\t}\n\t\tif (prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn followsStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters(String script, int openParenIndex,\n\t\t\tDeque statementBraces) {\n\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString token = previousIdentifier(script, tokenEnd);\n\t\tint functionEnd;\n\t\tboolean anonymous;\n\t\tif (token.equals("function")) {\n\t\t\tfunctionEnd = tokenEnd;\n\t\t\tanonymous = true;\n\t\t} else {\n\t\t\tint tokenStart = identifierStart(script, tokenEnd);\n\t\t\tint beforeName = previousNonWhitespace(script, tokenStart - 1);\n\t\t\tif (beforeName >= 0 && script.charAt(beforeName) == '*') {\n\t\t\t\tbeforeName = previousNonWhitespace(script, beforeName - 1);\n\t\t\t}\n\t\t\tif (beforeName < 0 || !previousIdentifier(script, beforeName).equals("function")) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfunctionEnd = beforeName;\n\t\t\tanonymous = false;\n\t\t}\n\n\t\tint functionStart = identifierStart(script, functionEnd);\n\t\tint beforeFunction = previousNonWhitespace(script, functionStart - 1);\n\t\tif (beforeFunction >= 0 && script.charAt(beforeFunction) == '.') {\n\t\t\treturn false;\n\t\t}\n\t\tif (anonymous) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Include a preceding async keyword when deciding whether a named function is\n\t\t// a declaration or an expression.\n\t\tif (beforeFunction >= 0 && Character.isJavaIdentifierPart(script.charAt(beforeFunction))\n\t\t\t\t&& previousIdentifier(script, beforeFunction).equals("async")) {\n\t\t\tint asyncStart = identifierStart(script, beforeFunction);\n\t\t\tbeforeFunction = previousNonWhitespace(script, asyncStart - 1);\n\t\t}\n\n\t\treturn !isStatementBoundary(script, beforeFunction, statementBraces);\n\t}\n\n\tprivate static boolean isStatementBoundary(String script, int index, Deque statementBraces) {\n\t\tif (index < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar previous = script.charAt(index);\n\t\tif (previous == ';' || previous == '}') {\n\t\t\treturn true;\n\t\t}\n\t\tif (previous == '{') {\n\t\t\treturn statementBraces.isEmpty() || statementBraces.peek();\n\t\t}\n\t\treturn false;\n\t}\n''' + if old not in text: + raise SystemExit('opensStatementBlock block not found') + text = text.replace(old, new, 1) + parser.write_text(text) + + tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') + text = tests.read_text() + anchor = '''\n\t@Test\n\tvoid objectLiteralStringBraceDoesNotBecomeStatementBlock() {''' + insert = '''\n\t@Test\n\tvoid functionExpressionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=function() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=function() {} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid namedFunctionExpressionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=function named() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=function named() {} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid arrowFunctionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=()=>{} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=()=>{} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid functionDeclarationBodyStillAllowsRegexStatement() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "Bukkit.dispatchCommand(Console, \\\"op attacker\\\")";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"function named() {} /[']/.test('x'); %untrusted%", ignored -> injection, bindings::put);\n\n\t\tassertEquals("function named() {} /[']/.test('x'); __advancedCorePlaceholder0", script);\n\t\tassertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n\t}\n''' + if anchor not in text: + raise SystemExit('test anchor not found') + text = text.replace(anchor, insert + anchor, 1) + tests.write_text(text) + PY + rm .github/workflows/fix-codex-293-function-expression-block.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Distinguish function expression bodies from statement blocks" + git push origin HEAD:security/javascript-placeholder-hardening From 42d73922921122df769646eae56386fcf5938f17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:48:52 +0000 Subject: [PATCH 49/54] Distinguish function expression bodies from statement blocks --- ...ix-codex-293-function-expression-block.yml | 63 --------------- .../JavascriptPlaceholderParser.java | 80 ++++++++++++++++++- .../JavascriptPlaceholderParserTest.java | 48 +++++++++++ 3 files changed, 124 insertions(+), 67 deletions(-) delete mode 100644 .github/workflows/fix-codex-293-function-expression-block.yml diff --git a/.github/workflows/fix-codex-293-function-expression-block.yml b/.github/workflows/fix-codex-293-function-expression-block.yml deleted file mode 100644 index f666d0cc0..000000000 --- a/.github/workflows/fix-codex-293-function-expression-block.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Apply Codex 293 function-expression lexer fix - -on: - push: - branches: - - security/javascript-placeholder-hardening - paths: - - .github/workflows/fix-codex-293-function-expression-block.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - fetch-depth: 0 - - name: Distinguish expression bodies from statement blocks - run: | - python3 - <<'PY' - from pathlib import Path - - parser = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') - text = parser.read_text() - - text = text.replace( - 'Deque controlParens = new ArrayDeque<>();\n\t\tDeque statementBraces = new ArrayDeque<>();', - 'Deque controlParens = new ArrayDeque<>();\n\t\tDeque functionExpressionParens = new ArrayDeque<>();\n\t\tDeque statementBraces = new ArrayDeque<>();', 1) - text = text.replace( - 'int lastControlHeadClose = -1;\n\t\tint lastStatementBlockClose = -1;', - 'int lastControlHeadClose = -1;\n\t\tint lastFunctionExpressionParenClose = -1;\n\t\tint lastStatementBlockClose = -1;', 1) - - old = '''\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (current == '{') {\n\t\t\t\tstatementBraces.push(opensStatementBlock(script, i, statementBraces));''' - new = '''\t\t\tif (current == '(') {\n\t\t\t\tint keywordEnd = previousNonWhitespace(script, i - 1);\n\t\t\t\tcontrolParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces));\n\t\t\t\tfunctionExpressionParens.push(opensFunctionExpressionParameters(script, i, statementBraces));\n\t\t\t} else if (current == ')' && !controlParens.isEmpty()) {\n\t\t\t\tif (controlParens.pop()) {\n\t\t\t\t\tlastControlHeadClose = i;\n\t\t\t\t}\n\t\t\t\tif (!functionExpressionParens.isEmpty() && functionExpressionParens.pop()) {\n\t\t\t\t\tlastFunctionExpressionParenClose = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (current == '{') {\n\t\t\t\tstatementBraces.push(opensStatementBlock(script, i, statementBraces, lastFunctionExpressionParenClose));''' - if old not in text: - raise SystemExit('paren/brace block not found') - text = text.replace(old, new, 1) - - old = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')' || prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn followsStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n''' - new = '''\tprivate static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces,\n\t\t\tint lastFunctionExpressionParenClose) {\n\t\tint prefixIndex = previousNonWhitespace(script, openBraceIndex - 1);\n\t\tif (prefixIndex < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar prefix = script.charAt(prefixIndex);\n\t\tif (prefix == ')') {\n\t\t\treturn prefixIndex != lastFunctionExpressionParenClose;\n\t\t}\n\t\tif (prefix == '}' || prefix == ';') {\n\t\t\treturn true;\n\t\t}\n\t\tif (prefix == ':') {\n\t\t\treturn followsStatementLabel(script, prefixIndex, statementBraces);\n\t\t}\n\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters(String script, int openParenIndex,\n\t\t\tDeque statementBraces) {\n\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString token = previousIdentifier(script, tokenEnd);\n\t\tint functionEnd;\n\t\tboolean anonymous;\n\t\tif (token.equals("function")) {\n\t\t\tfunctionEnd = tokenEnd;\n\t\t\tanonymous = true;\n\t\t} else {\n\t\t\tint tokenStart = identifierStart(script, tokenEnd);\n\t\t\tint beforeName = previousNonWhitespace(script, tokenStart - 1);\n\t\t\tif (beforeName >= 0 && script.charAt(beforeName) == '*') {\n\t\t\t\tbeforeName = previousNonWhitespace(script, beforeName - 1);\n\t\t\t}\n\t\t\tif (beforeName < 0 || !previousIdentifier(script, beforeName).equals("function")) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfunctionEnd = beforeName;\n\t\t\tanonymous = false;\n\t\t}\n\n\t\tint functionStart = identifierStart(script, functionEnd);\n\t\tint beforeFunction = previousNonWhitespace(script, functionStart - 1);\n\t\tif (beforeFunction >= 0 && script.charAt(beforeFunction) == '.') {\n\t\t\treturn false;\n\t\t}\n\t\tif (anonymous) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Include a preceding async keyword when deciding whether a named function is\n\t\t// a declaration or an expression.\n\t\tif (beforeFunction >= 0 && Character.isJavaIdentifierPart(script.charAt(beforeFunction))\n\t\t\t\t&& previousIdentifier(script, beforeFunction).equals("async")) {\n\t\t\tint asyncStart = identifierStart(script, beforeFunction);\n\t\t\tbeforeFunction = previousNonWhitespace(script, asyncStart - 1);\n\t\t}\n\n\t\treturn !isStatementBoundary(script, beforeFunction, statementBraces);\n\t}\n\n\tprivate static boolean isStatementBoundary(String script, int index, Deque statementBraces) {\n\t\tif (index < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tchar previous = script.charAt(index);\n\t\tif (previous == ';' || previous == '}') {\n\t\t\treturn true;\n\t\t}\n\t\tif (previous == '{') {\n\t\t\treturn statementBraces.isEmpty() || statementBraces.peek();\n\t\t}\n\t\treturn false;\n\t}\n''' - if old not in text: - raise SystemExit('opensStatementBlock block not found') - text = text.replace(old, new, 1) - parser.write_text(text) - - tests = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java') - text = tests.read_text() - anchor = '''\n\t@Test\n\tvoid objectLiteralStringBraceDoesNotBecomeStatementBlock() {''' - insert = '''\n\t@Test\n\tvoid functionExpressionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=function() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=function() {} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid namedFunctionExpressionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=function named() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=function named() {} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid arrowFunctionBodyDoesNotBecomeStatementBlock() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "'; allowed=true; '";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"var allowed=false,x=()=>{} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put);\n\n\t\tassertEquals("var allowed=false,x=()=>{} / 2; '\\\\'; allowed=true; \\\\''; allowed", script);\n\t\tassertTrue(bindings.isEmpty());\n\t}\n\n\t@Test\n\tvoid functionDeclarationBodyStillAllowsRegexStatement() {\n\t\tHashMap bindings = new HashMap<>();\n\t\tString injection = "Bukkit.dispatchCommand(Console, \\\"op attacker\\\")";\n\n\t\tString script = JavascriptPlaceholderParser.replace(\n\t\t\t\t"function named() {} /[']/.test('x'); %untrusted%", ignored -> injection, bindings::put);\n\n\t\tassertEquals("function named() {} /[']/.test('x'); __advancedCorePlaceholder0", script);\n\t\tassertEquals(injection, bindings.get("__advancedCorePlaceholder0"));\n\t}\n''' - if anchor not in text: - raise SystemExit('test anchor not found') - text = text.replace(anchor, insert + anchor, 1) - tests.write_text(text) - PY - rm .github/workflows/fix-codex-293-function-expression-block.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "Distinguish function expression bodies from statement blocks" - git push origin HEAD:security/javascript-placeholder-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 99639fd05..36baf427c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -76,11 +76,13 @@ private static Object coercePrimitive(String value) { private static Context contextAt(String script, int end) { Deque templates = new ArrayDeque<>(); Deque controlParens = new ArrayDeque<>(); + Deque functionExpressionParens = new ArrayDeque<>(); Deque statementBraces = new ArrayDeque<>(); Context context = Context.CODE; boolean escaped = false; boolean regexCharacterClass = false; int lastControlHeadClose = -1; + int lastFunctionExpressionParenClose = -1; int lastStatementBlockClose = -1; for (int i = 0; i < end; i++) { @@ -189,14 +191,18 @@ private static Context contextAt(String script, int end) { if (current == '(') { int keywordEnd = previousNonWhitespace(script, i - 1); controlParens.push(isStandaloneControlHead(script, keywordEnd, statementBraces)); + functionExpressionParens.push(opensFunctionExpressionParameters(script, i, statementBraces)); } else if (current == ')' && !controlParens.isEmpty()) { if (controlParens.pop()) { lastControlHeadClose = i; } + if (!functionExpressionParens.isEmpty() && functionExpressionParens.pop()) { + lastFunctionExpressionParenClose = i; + } } if (current == '{') { - statementBraces.push(opensStatementBlock(script, i, statementBraces)); + statementBraces.push(opensStatementBlock(script, i, statementBraces, lastFunctionExpressionParenClose)); if (!templates.isEmpty() && templates.peek().expressionDepth > 0) { templates.peek().expressionDepth++; } @@ -272,24 +278,90 @@ private static boolean isPostfixIncrementOrDecrement(String script, int operator || operand == '}'; } - private static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces) { + private static boolean opensStatementBlock(String script, int openBraceIndex, Deque statementBraces, + int lastFunctionExpressionParenClose) { int prefixIndex = previousNonWhitespace(script, openBraceIndex - 1); if (prefixIndex < 0) { return true; } char prefix = script.charAt(prefixIndex); - if (prefix == ')' || prefix == '}' || prefix == ';') { + if (prefix == ')') { + return prefixIndex != lastFunctionExpressionParenClose; + } + if (prefix == '}' || prefix == ';') { return true; } if (prefix == ':') { return followsStatementLabel(script, prefixIndex, statementBraces); } if (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') { - return true; + // Arrow-function bodies are part of an expression. Their closing brace is an + // operand and must not make a following slash look like a regex statement. + return false; } return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); } + private static boolean opensFunctionExpressionParameters(String script, int openParenIndex, + Deque statementBraces) { + int tokenEnd = previousNonWhitespace(script, openParenIndex - 1); + if (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) { + return false; + } + + String token = previousIdentifier(script, tokenEnd); + int functionEnd; + boolean anonymous; + if (token.equals("function")) { + functionEnd = tokenEnd; + anonymous = true; + } else { + int tokenStart = identifierStart(script, tokenEnd); + int beforeName = previousNonWhitespace(script, tokenStart - 1); + if (beforeName >= 0 && script.charAt(beforeName) == '*') { + beforeName = previousNonWhitespace(script, beforeName - 1); + } + if (beforeName < 0 || !previousIdentifier(script, beforeName).equals("function")) { + return false; + } + functionEnd = beforeName; + anonymous = false; + } + + int functionStart = identifierStart(script, functionEnd); + int beforeFunction = previousNonWhitespace(script, functionStart - 1); + if (beforeFunction >= 0 && script.charAt(beforeFunction) == '.') { + return false; + } + if (anonymous) { + return true; + } + + // Include a preceding async keyword when deciding whether a named function is + // a declaration or an expression. + if (beforeFunction >= 0 && Character.isJavaIdentifierPart(script.charAt(beforeFunction)) + && previousIdentifier(script, beforeFunction).equals("async")) { + int asyncStart = identifierStart(script, beforeFunction); + beforeFunction = previousNonWhitespace(script, asyncStart - 1); + } + + return !isStatementBoundary(script, beforeFunction, statementBraces); + } + + private static boolean isStatementBoundary(String script, int index, Deque statementBraces) { + if (index < 0) { + return true; + } + char previous = script.charAt(index); + if (previous == ';' || previous == '}') { + return true; + } + if (previous == '{') { + return statementBraces.isEmpty() || statementBraces.peek(); + } + return false; + } + private static boolean followsStatementLabel(String script, int colonIndex, Deque statementBraces) { int labelEnd = previousNonWhitespace(script, colonIndex - 1); if (labelEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(labelEnd))) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java index 03137138b..1fe27427b 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserTest.java @@ -193,6 +193,54 @@ void memberNamedIfDoesNotCreateControlHeadRegexContext() { assertTrue(bindings.isEmpty()); } + @Test + void functionExpressionBodyDoesNotBecomeStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=function() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=function() {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void namedFunctionExpressionBodyDoesNotBecomeStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=function named() {} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=function named() {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void arrowFunctionBodyDoesNotBecomeStatementBlock() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=()=>{} / 2; '%untrusted%'; allowed", ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=()=>{} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void functionDeclarationBodyStillAllowsRegexStatement() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace( + "function named() {} /[']/.test('x'); %untrusted%", ignored -> injection, bindings::put); + + assertEquals("function named() {} /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + @Test void objectLiteralStringBraceDoesNotBecomeStatementBlock() { HashMap bindings = new HashMap<>(); From 17fc31bc9232483b110094ccb165b08ac8cdd932 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 17:49:40 -0600 Subject: [PATCH 50/54] Test additional function expression block contexts --- ...avascriptFunctionExpressionParserTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptFunctionExpressionParserTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptFunctionExpressionParserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptFunctionExpressionParserTest.java new file mode 100644 index 000000000..36df0d627 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptFunctionExpressionParserTest.java @@ -0,0 +1,37 @@ +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.Test; + +class JavascriptFunctionExpressionParserTest { + + @Test + void asyncNamedFunctionExpressionFollowedByDivisionStaysDivision() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=async function named() {} / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=async function named() {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void generatorFunctionExpressionFollowedByDivisionStaysDivision() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=function* named() {} / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=function* named() {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } +} From 30bee81f426bfb74711bf672abb52fdb0f8cef5f Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 17:56:09 -0600 Subject: [PATCH 51/54] Apply latest Codex lexer fixes --- .../workflows/fix-codex-293-lexer-round2.yml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/fix-codex-293-lexer-round2.yml diff --git a/.github/workflows/fix-codex-293-lexer-round2.yml b/.github/workflows/fix-codex-293-lexer-round2.yml new file mode 100644 index 000000000..41155715a --- /dev/null +++ b/.github/workflows/fix-codex-293-lexer-round2.yml @@ -0,0 +1,57 @@ +name: Apply latest Codex 293 lexer fixes +on: + push: + paths: + - '.github/workflows/fix-codex-293-lexer-round2.yml' +permissions: + contents: write +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/javascript-placeholder-hardening + - name: Patch lexer + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') + s = p.read_text() + + old = '''\t\tString previousWord = previousIdentifier(script, previousIndex);\n\t\treturn REGEX_PREFIX_KEYWORDS.contains(previousWord);''' + new = '''\t\treturn isStandaloneRegexPrefixKeyword(script, previousIndex);''' + if old not in s: + raise SystemExit('regex-prefix replacement target not found') + s = s.replace(old, new, 1) + + marker = '''\tprivate static boolean isStandaloneControlHead(String script, int keywordEnd, Deque statementBraces) {''' + helper = '''\tprivate static boolean isStandaloneRegexPrefixKeyword(String script, int keywordEnd) {\n\t\tString keyword = previousIdentifier(script, keywordEnd);\n\t\tif (!REGEX_PREFIX_KEYWORDS.contains(keyword)) {\n\t\t\treturn false;\n\t\t}\n\t\tint keywordStart = identifierStart(script, keywordEnd);\n\t\tint beforeKeyword = previousNonWhitespace(script, keywordStart - 1);\n\t\treturn beforeKeyword < 0 || script.charAt(beforeKeyword) != '.';\n\t}\n\n''' + if marker not in s: + raise SystemExit('standalone-control marker not found') + s = s.replace(marker, helper + marker, 1) + + old = '''\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}''' + new = '''\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd >= 0 && script.charAt(tokenEnd) == '*') {\n\t\t\ttokenEnd = previousNonWhitespace(script, tokenEnd - 1);\n\t\t}\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}''' + if old not in s: + raise SystemExit('function-parameter replacement target not found') + s = s.replace(old, new, 1) + + old = '''\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters''' + new = '''\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\tif (opensClassDeclarationBody(script, prefixIndex, statementBraces)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensClassDeclarationBody(String script, int prefixIndex, Deque statementBraces) {\n\t\tint cursor = prefixIndex;\n\t\tint parenDepth = 0;\n\t\tint bracketDepth = 0;\n\t\twhile (cursor >= 0) {\n\t\t\tchar current = script.charAt(cursor);\n\t\t\tif (Character.isWhitespace(current)) {\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == ')') {\n\t\t\t\tparenDepth++;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == '(' && parenDepth > 0) {\n\t\t\t\tparenDepth--;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == ']') {\n\t\t\t\tbracketDepth++;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == '[' && bracketDepth > 0) {\n\t\t\t\tbracketDepth--;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (parenDepth > 0 || bracketDepth > 0) {\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (Character.isJavaIdentifierPart(current)) {\n\t\t\t\tint start = identifierStart(script, cursor);\n\t\t\t\tString word = script.substring(start, cursor + 1);\n\t\t\t\tif (word.equals("class")) {\n\t\t\t\t\tint beforeClass = previousNonWhitespace(script, start - 1);\n\t\t\t\t\treturn isClassDeclarationBoundary(script, beforeClass, statementBraces);\n\t\t\t\t}\n\t\t\t\tcursor = previousNonWhitespace(script, start - 1);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ("=?:,;".indexOf(current) >= 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcursor--;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate static boolean isClassDeclarationBoundary(String script, int beforeClass, Deque statementBraces) {\n\t\tif (isStatementBoundary(script, beforeClass, statementBraces)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (beforeClass < 0 || !Character.isJavaIdentifierPart(script.charAt(beforeClass))) {\n\t\t\treturn false;\n\t\t}\n\t\tString previousWord = previousIdentifier(script, beforeClass);\n\t\tif (previousWord.equals("export")) {\n\t\t\tint exportStart = identifierStart(script, beforeClass);\n\t\t\treturn isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces);\n\t\t}\n\t\tif (previousWord.equals("default")) {\n\t\t\tint defaultStart = identifierStart(script, beforeClass);\n\t\t\tint beforeDefault = previousNonWhitespace(script, defaultStart - 1);\n\t\t\tif (beforeDefault >= 0 && previousIdentifier(script, beforeDefault).equals("export")) {\n\t\t\t\tint exportStart = identifierStart(script, beforeDefault);\n\t\t\t\treturn isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters''' + if old not in s: + raise SystemExit('statement-block replacement target not found') + s = s.replace(old, new, 1) + + p.write_text(s) + PY + - name: Commit patch and remove helper workflow + shell: bash + run: | + git config user.name "Ben" + git config user.email "benbergen12@gmail.com" + git rm .github/workflows/fix-codex-293-lexer-round2.yml + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java + git commit -m "Harden JavaScript lexer keyword and class contexts" + git push origin HEAD:security/javascript-placeholder-hardening From a732494a2ee7acf87616758b82d4e20da097c42e Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 23:56:18 +0000 Subject: [PATCH 52/54] Harden JavaScript lexer keyword and class contexts --- .../workflows/fix-codex-293-lexer-round2.yml | 57 ----------- .../JavascriptPlaceholderParser.java | 94 ++++++++++++++++++- 2 files changed, 92 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/fix-codex-293-lexer-round2.yml diff --git a/.github/workflows/fix-codex-293-lexer-round2.yml b/.github/workflows/fix-codex-293-lexer-round2.yml deleted file mode 100644 index 41155715a..000000000 --- a/.github/workflows/fix-codex-293-lexer-round2.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Apply latest Codex 293 lexer fixes -on: - push: - paths: - - '.github/workflows/fix-codex-293-lexer-round2.yml' -permissions: - contents: write -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/javascript-placeholder-hardening - - name: Patch lexer - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java') - s = p.read_text() - - old = '''\t\tString previousWord = previousIdentifier(script, previousIndex);\n\t\treturn REGEX_PREFIX_KEYWORDS.contains(previousWord);''' - new = '''\t\treturn isStandaloneRegexPrefixKeyword(script, previousIndex);''' - if old not in s: - raise SystemExit('regex-prefix replacement target not found') - s = s.replace(old, new, 1) - - marker = '''\tprivate static boolean isStandaloneControlHead(String script, int keywordEnd, Deque statementBraces) {''' - helper = '''\tprivate static boolean isStandaloneRegexPrefixKeyword(String script, int keywordEnd) {\n\t\tString keyword = previousIdentifier(script, keywordEnd);\n\t\tif (!REGEX_PREFIX_KEYWORDS.contains(keyword)) {\n\t\t\treturn false;\n\t\t}\n\t\tint keywordStart = identifierStart(script, keywordEnd);\n\t\tint beforeKeyword = previousNonWhitespace(script, keywordStart - 1);\n\t\treturn beforeKeyword < 0 || script.charAt(beforeKeyword) != '.';\n\t}\n\n''' - if marker not in s: - raise SystemExit('standalone-control marker not found') - s = s.replace(marker, helper + marker, 1) - - old = '''\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}''' - new = '''\t\tint tokenEnd = previousNonWhitespace(script, openParenIndex - 1);\n\t\tif (tokenEnd >= 0 && script.charAt(tokenEnd) == '*') {\n\t\t\ttokenEnd = previousNonWhitespace(script, tokenEnd - 1);\n\t\t}\n\t\tif (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) {\n\t\t\treturn false;\n\t\t}''' - if old not in s: - raise SystemExit('function-parameter replacement target not found') - s = s.replace(old, new, 1) - - old = '''\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters''' - new = '''\t\tif (prefix == '>' && prefixIndex > 0 && script.charAt(prefixIndex - 1) == '=') {\n\t\t\t// Arrow-function bodies are part of an expression. Their closing brace is an\n\t\t\t// operand and must not make a following slash look like a regex statement.\n\t\t\treturn false;\n\t\t}\n\t\tif (opensClassDeclarationBody(script, prefixIndex, statementBraces)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex));\n\t}\n\n\tprivate static boolean opensClassDeclarationBody(String script, int prefixIndex, Deque statementBraces) {\n\t\tint cursor = prefixIndex;\n\t\tint parenDepth = 0;\n\t\tint bracketDepth = 0;\n\t\twhile (cursor >= 0) {\n\t\t\tchar current = script.charAt(cursor);\n\t\t\tif (Character.isWhitespace(current)) {\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == ')') {\n\t\t\t\tparenDepth++;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == '(' && parenDepth > 0) {\n\t\t\t\tparenDepth--;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == ']') {\n\t\t\t\tbracketDepth++;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (current == '[' && bracketDepth > 0) {\n\t\t\t\tbracketDepth--;\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (parenDepth > 0 || bracketDepth > 0) {\n\t\t\t\tcursor--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (Character.isJavaIdentifierPart(current)) {\n\t\t\t\tint start = identifierStart(script, cursor);\n\t\t\t\tString word = script.substring(start, cursor + 1);\n\t\t\t\tif (word.equals("class")) {\n\t\t\t\t\tint beforeClass = previousNonWhitespace(script, start - 1);\n\t\t\t\t\treturn isClassDeclarationBoundary(script, beforeClass, statementBraces);\n\t\t\t\t}\n\t\t\t\tcursor = previousNonWhitespace(script, start - 1);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ("=?:,;".indexOf(current) >= 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcursor--;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate static boolean isClassDeclarationBoundary(String script, int beforeClass, Deque statementBraces) {\n\t\tif (isStatementBoundary(script, beforeClass, statementBraces)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (beforeClass < 0 || !Character.isJavaIdentifierPart(script.charAt(beforeClass))) {\n\t\t\treturn false;\n\t\t}\n\t\tString previousWord = previousIdentifier(script, beforeClass);\n\t\tif (previousWord.equals("export")) {\n\t\t\tint exportStart = identifierStart(script, beforeClass);\n\t\t\treturn isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces);\n\t\t}\n\t\tif (previousWord.equals("default")) {\n\t\t\tint defaultStart = identifierStart(script, beforeClass);\n\t\t\tint beforeDefault = previousNonWhitespace(script, defaultStart - 1);\n\t\t\tif (beforeDefault >= 0 && previousIdentifier(script, beforeDefault).equals("export")) {\n\t\t\t\tint exportStart = identifierStart(script, beforeDefault);\n\t\t\t\treturn isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate static boolean opensFunctionExpressionParameters''' - if old not in s: - raise SystemExit('statement-block replacement target not found') - s = s.replace(old, new, 1) - - p.write_text(s) - PY - - name: Commit patch and remove helper workflow - shell: bash - run: | - git config user.name "Ben" - git config user.email "benbergen12@gmail.com" - git rm .github/workflows/fix-codex-293-lexer-round2.yml - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java - git commit -m "Harden JavaScript lexer keyword and class contexts" - git push origin HEAD:security/javascript-placeholder-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java index 36baf427c..b1528b30a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParser.java @@ -247,8 +247,17 @@ private static boolean startsRegexLiteral(String script, int slashIndex, int las if (previous == '}' && previousIndex == lastStatementBlockClose) { return true; } - String previousWord = previousIdentifier(script, previousIndex); - return REGEX_PREFIX_KEYWORDS.contains(previousWord); + return isStandaloneRegexPrefixKeyword(script, previousIndex); + } + + private static boolean isStandaloneRegexPrefixKeyword(String script, int keywordEnd) { + String keyword = previousIdentifier(script, keywordEnd); + if (!REGEX_PREFIX_KEYWORDS.contains(keyword)) { + return false; + } + int keywordStart = identifierStart(script, keywordEnd); + int beforeKeyword = previousNonWhitespace(script, keywordStart - 1); + return beforeKeyword < 0 || script.charAt(beforeKeyword) != '.'; } private static boolean isStandaloneControlHead(String script, int keywordEnd, Deque statementBraces) { @@ -299,12 +308,93 @@ private static boolean opensStatementBlock(String script, int openBraceIndex, De // operand and must not make a following slash look like a regex statement. return false; } + if (opensClassDeclarationBody(script, prefixIndex, statementBraces)) { + return true; + } return BLOCK_PREFIX_KEYWORDS.contains(previousIdentifier(script, prefixIndex)); } + private static boolean opensClassDeclarationBody(String script, int prefixIndex, Deque statementBraces) { + int cursor = prefixIndex; + int parenDepth = 0; + int bracketDepth = 0; + while (cursor >= 0) { + char current = script.charAt(cursor); + if (Character.isWhitespace(current)) { + cursor--; + continue; + } + if (current == ')') { + parenDepth++; + cursor--; + continue; + } + if (current == '(' && parenDepth > 0) { + parenDepth--; + cursor--; + continue; + } + if (current == ']') { + bracketDepth++; + cursor--; + continue; + } + if (current == '[' && bracketDepth > 0) { + bracketDepth--; + cursor--; + continue; + } + if (parenDepth > 0 || bracketDepth > 0) { + cursor--; + continue; + } + if (Character.isJavaIdentifierPart(current)) { + int start = identifierStart(script, cursor); + String word = script.substring(start, cursor + 1); + if (word.equals("class")) { + int beforeClass = previousNonWhitespace(script, start - 1); + return isClassDeclarationBoundary(script, beforeClass, statementBraces); + } + cursor = previousNonWhitespace(script, start - 1); + continue; + } + if ("=?:,;".indexOf(current) >= 0) { + return false; + } + cursor--; + } + return false; + } + + private static boolean isClassDeclarationBoundary(String script, int beforeClass, Deque statementBraces) { + if (isStatementBoundary(script, beforeClass, statementBraces)) { + return true; + } + if (beforeClass < 0 || !Character.isJavaIdentifierPart(script.charAt(beforeClass))) { + return false; + } + String previousWord = previousIdentifier(script, beforeClass); + if (previousWord.equals("export")) { + int exportStart = identifierStart(script, beforeClass); + return isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces); + } + if (previousWord.equals("default")) { + int defaultStart = identifierStart(script, beforeClass); + int beforeDefault = previousNonWhitespace(script, defaultStart - 1); + if (beforeDefault >= 0 && previousIdentifier(script, beforeDefault).equals("export")) { + int exportStart = identifierStart(script, beforeDefault); + return isStatementBoundary(script, previousNonWhitespace(script, exportStart - 1), statementBraces); + } + } + return false; + } + private static boolean opensFunctionExpressionParameters(String script, int openParenIndex, Deque statementBraces) { int tokenEnd = previousNonWhitespace(script, openParenIndex - 1); + if (tokenEnd >= 0 && script.charAt(tokenEnd) == '*') { + tokenEnd = previousNonWhitespace(script, tokenEnd - 1); + } if (tokenEnd < 0 || !Character.isJavaIdentifierPart(script.charAt(tokenEnd))) { return false; } From b3c1d413270a51d26f7e5fc9f1c13210a0def85a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 17:56:56 -0600 Subject: [PATCH 53/54] Test latest JavaScript lexer edge cases --- ...riptPlaceholderParserKeywordClassTest.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java new file mode 100644 index 000000000..de08ecf59 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java @@ -0,0 +1,74 @@ +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.Test; + +class JavascriptPlaceholderParserKeywordClassTest { + + @Test + void qualifiedReturnMemberBeforeDivisionDoesNotOpenRegexContext() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,obj={return:4}; obj.return / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,obj={return:4}; obj.return / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void anonymousGeneratorExpressionBeforeDivisionDoesNotOpenRegexContext() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,x=function*() {} / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,x=function*() {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } + + @Test + void classDeclarationBodyAllowsFollowingRegexStatement() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace( + "class X {} /[']/.test('x'); %untrusted%", ignored -> injection, bindings::put); + + assertEquals("class X {} /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void classDeclarationWithExtendsAllowsFollowingRegexStatement() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, \"op attacker\")"; + + String script = JavascriptPlaceholderParser.replace( + "class X extends Base {} /[']/.test('x'); %untrusted%", ignored -> injection, bindings::put); + + assertEquals("class X extends Base {} /[']/.test('x'); __advancedCorePlaceholder0", script); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void classExpressionBodyBeforeDivisionDoesNotOpenRegexContext() { + HashMap bindings = new HashMap<>(); + String injection = "'; allowed=true; '"; + + String script = JavascriptPlaceholderParser.replace( + "var allowed=false,C=class X {} / 2; '%untrusted%'; allowed", + ignored -> injection, bindings::put); + + assertEquals("var allowed=false,C=class X {} / 2; '\\'; allowed=true; \\''; allowed", script); + assertTrue(bindings.isEmpty()); + } +} From 1abf4d51e8940db7cdd61580bb617a4c601337d7 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Aug 2026 17:58:06 -0600 Subject: [PATCH 54/54] Fix qualified-member regression resolver --- .../javascript/JavascriptPlaceholderParserKeywordClassTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java index de08ecf59..d2c77f091 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderParserKeywordClassTest.java @@ -16,7 +16,7 @@ void qualifiedReturnMemberBeforeDivisionDoesNotOpenRegexContext() { String script = JavascriptPlaceholderParser.replace( "var allowed=false,obj={return:4}; obj.return / 2; '%untrusted%'; allowed", - ignored -> injection, bindings::put); + placeholder -> placeholder.equals("%untrusted%") ? injection : placeholder, bindings::put); assertEquals("var allowed=false,obj={return:4}; obj.return / 2; '\\'; allowed=true; \\''; allowed", script); assertTrue(bindings.isEmpty());