This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 0c8ebc8b31 fix(rewrite): reject unusable rule handles at the admin
write boundary (#7873) (#7874)
0c8ebc8b31 is described below
commit 0c8ebc8b319c4dbde324829a75953a8a6e9869ac
Author: build <[email protected]>
AuthorDate: Wed Oct 21 11:58:01 2026 +0800
fix(rewrite): reject unusable rule handles at the admin write boundary
(#7873) (#7874)
* fix(rewrite): validate rule handles at the admin write boundary (#7873)
- Extract the rewrite applicability check from RewritePluginDataHandler into
shenyu-common's RewriteHandleUtils, so Admin and the gateway validate a
rule
handle with exactly the same code.
- Add RewriteRuleHandleValidator for the admin write boundary and call it
from
RuleServiceImpl#validateRuleHandle, so a rule whose handle the gateway
could
never apply is rejected when it is created, updated or imported instead of
being stored as enabled and silently ignored by the gateway.
- Tests: RewriteHandleUtilsTest, RewriteRuleHandleValidatorTest and new
RuleServiceTest cases, all of which fail before the change.
* fix(rewrite): validate raw patterns and address review feedback
---
.../shenyu/admin/service/impl/RuleServiceImpl.java | 3 +
.../validator/RewriteRuleHandleValidator.java | 62 ++++++++++++++++++
.../shenyu/admin/service/RuleServiceTest.java | 54 ++++++++++++++++
.../validator/RewriteRuleHandleValidatorTest.java | 71 +++++++++++++++++++++
.../shenyu/common/utils/RewriteHandleUtils.java | 73 ++++++++++++++++++++++
.../common/utils/RewriteHandleUtilsTest.java | 67 ++++++++++++++++++++
.../rewrite/handler/RewritePluginDataHandler.java | 23 ++-----
7 files changed, 336 insertions(+), 17 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
index 40e8125b92..a7e2838b4e 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
@@ -54,6 +54,7 @@ import org.apache.shenyu.admin.transfer.ConditionTransfer;
import org.apache.shenyu.admin.utils.Assert;
import org.apache.shenyu.admin.utils.SessionUtil;
import
org.apache.shenyu.admin.validation.validator.AgentGatewayRuleHandleValidator;
+import org.apache.shenyu.admin.validation.validator.RewriteRuleHandleValidator;
import org.apache.shenyu.common.constant.AdminConstants;
import org.apache.shenyu.common.dto.ConditionData;
import org.apache.shenyu.common.dto.RuleData;
@@ -415,6 +416,8 @@ public class RuleServiceImpl implements RuleService {
}
if (PluginEnum.AGENT_GATEWAY.getName().equals(plugin.getName())) {
AgentGatewayRuleHandleValidator.validate(handle);
+ } else if (PluginEnum.REWRITE.getName().equals(plugin.getName())) {
+ RewriteRuleHandleValidator.validate(handle);
}
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidator.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidator.java
new file mode 100644
index 0000000000..1ac4a189b0
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidator.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.validation.validator;
+
+import com.google.gson.JsonSyntaxException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.exception.ShenyuAdminException;
+import org.apache.shenyu.common.dto.convert.rule.RewriteHandle;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.RewriteHandleUtils;
+
+import java.util.Objects;
+
+/**
+ * Validates rewrite rule handles before Admin stores them.
+ */
+public final class RewriteRuleHandleValidator {
+
+ private RewriteRuleHandleValidator() {
+ }
+
+ /**
+ * Validate a rewrite rule handle, rejecting the rule before it is
persisted when the gateway
+ * could never apply it.
+ *
+ * @param handle the rule handle supplied to Admin
+ */
+ public static void validate(final String handle) {
+ if (StringUtils.isBlank(handle)) {
+ // an empty handle means the rule does not rewrite, which the
plugin tolerates as well
+ return;
+ }
+ final RewriteHandle rewriteHandle;
+ try {
+ rewriteHandle = GsonUtils.getInstance().fromJson(handle,
RewriteHandle.class);
+ } catch (JsonSyntaxException ex) {
+ throw new ShenyuAdminException("rewrite rule handle must be a
valid JSON object");
+ }
+ if (Objects.isNull(rewriteHandle)) {
+ throw new ShenyuAdminException("rewrite rule handle must be a
valid JSON object");
+ }
+ final String problem =
RewriteHandleUtils.applicabilityProblem(rewriteHandle.getRegex(),
rewriteHandle.getReplace());
+ if (Objects.nonNull(problem)) {
+ throw new ShenyuAdminException("rewrite rule handle can not be
applied, " + problem);
+ }
+ }
+}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java
index 6999eae704..1cfc2ad1ae 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java
@@ -321,6 +321,60 @@ public final class RuleServiceTest {
verify(ruleMapper, never()).insertSelective(any());
}
+ @Test
+ public void testRewriteCreateRejectsUnapplicableHandleBeforeWrite() {
+ mockRewritePlugin();
+ RuleDTO ruleDTO = buildRuleDTO("");
+ ruleDTO.setHandle("{\"regex\":\"test\",\"replace\":\"/v1/$$\"}");
+
+ assertThrows(ShenyuAdminException.class, () ->
ruleService.createOrUpdate(ruleDTO));
+
+ verify(ruleMapper, never()).insertSelective(any());
+ verify(ruleEventPublisher, never()).onCreated(any(), any());
+ }
+
+ @Test
+ public void testRewriteUpdateRejectsUnapplicableHandleBeforeWrite() {
+ mockRewritePlugin();
+ RuleDTO ruleDTO = buildRuleDTO("123");
+ ruleDTO.setHandle("{\"regex\":\"test\",\"replace\":\"/new/$\"}");
+
+ assertThrows(ShenyuAdminException.class, () ->
ruleService.createOrUpdate(ruleDTO));
+
+ verify(ruleMapper, never()).updateSelective(any());
+ verify(ruleEventPublisher, never()).onUpdated(any(), any(), any(),
any());
+ }
+
+ @Test
+ public void testRewriteCreateAcceptsApplicableHandle() {
+ mockRewritePlugin();
+ RuleDTO ruleDTO = buildRuleDTO("");
+ ruleDTO.setHandle("{\"regex\":\"(test)\",\"replace\":\"/new/$1\"}");
+ given(ruleMapper.insertSelective(any())).willReturn(1);
+
+ assertEquals(1, ruleService.createOrUpdate(ruleDTO));
+ verify(ruleEventPublisher).onCreated(any(), any());
+ }
+
+ @Test
+ public void testRewriteImportRejectsUnapplicableHandleBeforeWrite() {
+ mockRewritePlugin();
+ given(ruleMapper.selectAll()).willReturn(Collections.emptyList());
+ RuleDTO ruleDTO = buildRuleDTO("");
+ ruleDTO.setHandle("{\"regex\":\"test\",\"replace\":\"/new/$x\"}");
+
+ assertThrows(ShenyuAdminException.class, () ->
ruleService.importData(Collections.singletonList(ruleDTO)));
+ verify(ruleMapper, never()).insertSelective(any());
+ }
+
+ private void mockRewritePlugin() {
+ given(selectorMapper.selectById("456")).willReturn(buildSelectorDO());
+ given(pluginMapper.selectById("789")).willReturn(PluginDO.builder()
+ .id("789")
+ .name(PluginEnum.REWRITE.getName())
+ .build());
+ }
+
private void mockAgentGatewayPlugin() {
given(selectorMapper.selectById("456")).willReturn(buildSelectorDO());
given(pluginMapper.selectById("789")).willReturn(PluginDO.builder()
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidatorTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidatorTest.java
new file mode 100644
index 0000000000..95c93957f0
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/RewriteRuleHandleValidatorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.validation.validator;
+
+import org.apache.shenyu.admin.exception.ShenyuAdminException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for rewrite rule handle validation at the Admin write boundary.
+ */
+public final class RewriteRuleHandleValidatorTest {
+
+ @Test
+ public void testApplicableHandles() {
+ assertDoesNotThrow(() -> RewriteRuleHandleValidator.validate(null));
+ assertDoesNotThrow(() -> RewriteRuleHandleValidator.validate(""));
+ assertDoesNotThrow(() -> RewriteRuleHandleValidator.validate("{}"));
+ assertDoesNotThrow(() ->
RewriteRuleHandleValidator.validate("{\"regex\":\"(test)\",\"replace\":\"/new/$1\"}"));
+ assertDoesNotThrow(() ->
RewriteRuleHandleValidator.validate("{\"regex\":\"/shenyu/test\",\"replace\":\"/new\"}"));
+ assertDoesNotThrow(() ->
RewriteRuleHandleValidator.validate("{\"regex\":\"/shenyu/{id}\",\"replace\":\"/target/{id}\"}"));
+ assertDoesNotThrow(() ->
RewriteRuleHandleValidator.validate("{\"regex\":\"test\",\"replace\":\"/new\",\"percentage\":30}"));
+ // an incomplete expression means the plugin skips the rewrite
+ assertDoesNotThrow(() ->
RewriteRuleHandleValidator.validate("{\"regex\":\"test\"}"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "not-json",
+ "[]",
+ "\"test\"",
+ "null",
+ "{\"regex\":\"test\",\"replace\":\"/new/$1\"}",
+ "{\"regex\":\"test\",\"replace\":\"/v1/$$\"}",
+ "{\"regex\":\"test\",\"replace\":\"/new/$\"}",
+ "{\"regex\":\"test\",\"replace\":\"/new/${name\"}",
+ "{\"regex\":\"(test\",\"replace\":\"/new\"}",
+ "{\"regex\":\"[a-\",\"replace\":\"/new\"}",
+ "{\"regex\":\"test\\\\\",\"replace\":\"/new\"}"
+ })
+ public void testUnapplicableHandles(final String handle) {
+ assertThrows(ShenyuAdminException.class, () ->
RewriteRuleHandleValidator.validate(handle));
+ }
+
+ @Test
+ public void testMessageNamesTheProblem() {
+ final ShenyuAdminException error =
assertThrows(ShenyuAdminException.class,
+ () ->
RewriteRuleHandleValidator.validate("{\"regex\":\"test\",\"replace\":\"/new/$1\"}"));
+ assertTrue(error.getMessage().contains("invalid replacement
template"));
+ }
+}
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/utils/RewriteHandleUtils.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/RewriteHandleUtils.java
new file mode 100644
index 0000000000..b795c4b9e5
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/RewriteHandleUtils.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.common.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * The utils that tell whether a rewrite expression can be applied.
+ */
+public final class RewriteHandleUtils {
+
+ private static final String INVALID_REGEX = "invalid regular expression: ";
+
+ private static final String INVALID_REPLACEMENT = "invalid replacement
template: ";
+
+ private RewriteHandleUtils() {
+ }
+
+ /**
+ * Explain why a rewrite expression cannot be applied.
+ *
+ * <p>The plugin expands the replacement with {@link
String#replaceAll(String, String)}, so the
+ * replacement is a template in which a dollar sign references a regex
group. The configured
+ * expression is validated on its own first: appending the empty
alternative that makes the
+ * dry-run match would turn a trailing backslash into an escaped pipe and
hide an invalid
+ * expression. The dry-run that follows only validates the replacement
template. An incomplete
+ * expression and the placeholder mode, where both values contain a left
brace, are always
+ * applicable because the plugin skips them or substitutes them
literally.</p>
+ *
+ * @param regex the configured regular expression
+ * @param replace the configured replacement template
+ * @return {@code null} when the plugin can apply the expression,
otherwise the reason why it cannot
+ */
+ public static String applicabilityProblem(final String regex, final String
replace) {
+ if (isSkipped(regex, replace)) {
+ return null;
+ }
+ try {
+ Pattern.compile(regex);
+ } catch (PatternSyntaxException ex) {
+ return INVALID_REGEX + ex.getDescription();
+ }
+ try {
+ // the trailing empty alternative makes the empty input match, so
the replacement is parsed too
+ Pattern.compile(regex + "|").matcher("").replaceAll(replace);
+ return null;
+ } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
+ return INVALID_REPLACEMENT + ex.getMessage();
+ }
+ }
+
+ private static boolean isSkipped(final String regex, final String replace)
{
+ return StringUtils.isAnyBlank(regex, replace) || replace.contains("{")
&& regex.contains("{");
+ }
+}
diff --git
a/shenyu-common/src/test/java/org/apache/shenyu/common/utils/RewriteHandleUtilsTest.java
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/RewriteHandleUtilsTest.java
new file mode 100644
index 0000000000..fae5e269ed
--- /dev/null
+++
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/RewriteHandleUtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.common.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for {@link RewriteHandleUtils}.
+ */
+public final class RewriteHandleUtilsTest {
+
+ @Test
+ public void testApplicableExpressions() {
+ assertNull(RewriteHandleUtils.applicabilityProblem(null, null));
+ assertNull(RewriteHandleUtils.applicabilityProblem("", "/new"));
+ assertNull(RewriteHandleUtils.applicabilityProblem("test", null));
+ assertNull(RewriteHandleUtils.applicabilityProblem("(test)",
"/new/$1"));
+ assertNull(RewriteHandleUtils.applicabilityProblem("/shenyu/test",
"/new"));
+ assertNull(RewriteHandleUtils.applicabilityProblem("/shenyu/{id}",
"/target/{id}"));
+ }
+
+ @Test
+ public void testMissingGroupReferenceIsReported() {
+ final String problem = RewriteHandleUtils.applicabilityProblem("test",
"/new/$1");
+ assertNotNull(problem);
+ assertTrue(problem.startsWith("invalid replacement template"));
+ }
+
+ @Test
+ public void testBareDollarIsReported() {
+ assertNotNull(RewriteHandleUtils.applicabilityProblem("test",
"/v1/$$"));
+ }
+
+ @Test
+ public void testUnclosedGroupIsReported() {
+ final String problem =
RewriteHandleUtils.applicabilityProblem("(test", "/new");
+ assertNotNull(problem);
+ assertTrue(problem.startsWith("invalid regular expression"));
+ }
+
+ @Test
+ public void testTrailingBackslashExpressionIsReported() {
+ // appending the empty alternative would turn the trailing backslash
into an escaped pipe
+ final String problem =
RewriteHandleUtils.applicabilityProblem("test\\", "/new");
+ assertNotNull(problem);
+ assertTrue(problem.startsWith("invalid regular expression"));
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-rewrite/src/main/java/org/apache/shenyu/plugin/rewrite/handler/RewritePluginDataHandler.java
b/shenyu-plugin/shenyu-plugin-rewrite/src/main/java/org/apache/shenyu/plugin/rewrite/handler/RewritePluginDataHandler.java
index 3d203b42bd..8d6d30fc49 100644
---
a/shenyu-plugin/shenyu-plugin-rewrite/src/main/java/org/apache/shenyu/plugin/rewrite/handler/RewritePluginDataHandler.java
+++
b/shenyu-plugin/shenyu-plugin-rewrite/src/main/java/org/apache/shenyu/plugin/rewrite/handler/RewritePluginDataHandler.java
@@ -22,6 +22,7 @@ import org.apache.shenyu.common.dto.RuleData;
import org.apache.shenyu.common.dto.convert.rule.RewriteHandle;
import org.apache.shenyu.common.enums.PluginEnum;
import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.RewriteHandleUtils;
import org.apache.shenyu.plugin.base.cache.CommonHandleCache;
import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
import org.apache.shenyu.plugin.base.utils.BeanHolder;
@@ -32,7 +33,6 @@ import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
-import java.util.regex.Pattern;
/**
* The type redirect plugin data subscriber.
@@ -78,23 +78,12 @@ public class RewritePluginDataHandler implements
PluginDataHandler {
* @return true when the plugin can apply the handle
*/
private static boolean canRewrite(final RewriteHandle rewriteHandle, final
RuleData ruleData) {
- final String regex = rewriteHandle.getRegex();
- final String replace = rewriteHandle.getReplace();
- if (StringUtils.isAnyBlank(regex, replace)) {
- // an incomplete expression means the plugin does not rewrite,
which is always safe.
+ final String problem =
RewriteHandleUtils.applicabilityProblem(rewriteHandle.getRegex(),
rewriteHandle.getReplace());
+ if (Objects.isNull(problem)) {
return true;
}
- if (replace.contains("{") && regex.contains("{")) {
- // in placeholder mode both values are substituted literally, the
regex is never compiled.
- return true;
- }
- try {
- // the empty alternative makes the empty input match, which also
validates the replacement.
- Pattern.compile(regex + "|").matcher("").replaceAll(replace);
- return true;
- } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
- LOG.error("invalid rewrite rule [{}] is ignored, regex: {},
replace: {}", ruleData.getId(), regex, replace, ex);
- return false;
- }
+ LOG.error("invalid rewrite rule [{}] is ignored, regex: {}, replace:
{}, reason: {}",
+ ruleData.getId(), rewriteHandle.getRegex(),
rewriteHandle.getReplace(), problem);
+ return false;
}
}