This is an automated email from the ASF dual-hosted git repository.
jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new 94a44ef7b6 bugfix: fix error parsing application/x-www-form-urlencoded
requests in Http2HttpHandler (#7749)
94a44ef7b6 is described below
commit 94a44ef7b6c7507700fe0a8136f96162cd24f43f
Author: xiaoyu <[email protected]>
AuthorDate: Fri Oct 31 14:08:15 2025 +0800
bugfix: fix error parsing application/x-www-form-urlencoded requests in
Http2HttpHandler (#7749)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
.../core/protocol/detector/Http2Detector.java | 1 +
.../core/rpc/netty/http/Http2HttpHandler.java | 27 +++-
.../netty/http/filter/HttpRequestParamWrapper.java | 3 +-
server/pom.xml | 6 +
.../server/controller/ClusterControllerTest.java | 169 ++++++++++++++++++++-
7 files changed, 197 insertions(+), 11 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index f77d017b7b..70878588dc 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -47,6 +47,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7662](https://github.com/apache/incubator-seata/pull/7662)] ensure
visibility of rm and The methods in MockTest are executed in order
- [[#7683](https://github.com/apache/incubator-seata/pull/7683)] Override
XABranchXid equals() and hashCode() to fix memory leak in mysql driver
- [[#7643](https://github.com/apache/incubator-seata/pull/7643)] fix DM
transaction rollback not using database auto-increment primary keys
+- [[#7749](https://github.com/apache/incubator-seata/pull/7749)] fix error
parsing application/x-www-form-urlencoded requests in Http2HttpHandler
### optimize:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 3e66207881..9d707fb830 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -47,6 +47,7 @@
- [[#7662](https://github.com/apache/incubator-seata/pull/7662)] 确保 rm 的可见性,并且
MockTest 中的方法按顺序执行
- [[#7683](https://github.com/apache/incubator-seata/pull/7683)] 重写
XABranchXid的equals和hashCode,解决mysql driver内存泄漏问题
- [[#7643](https://github.com/apache/incubator-seata/pull/7643)] 修复 DM
事务回滚不使用数据库自动增量主键
+- [[#7749](https://github.com/apache/incubator-seata/pull/7749)] 修复
Http2HttpHandler 解析 application/x-www-form-urlencoded 请求失败的问题
### optimize:
diff --git
a/core/src/main/java/org/apache/seata/core/protocol/detector/Http2Detector.java
b/core/src/main/java/org/apache/seata/core/protocol/detector/Http2Detector.java
index 9099c82977..24ae99a7c8 100644
---
a/core/src/main/java/org/apache/seata/core/protocol/detector/Http2Detector.java
+++
b/core/src/main/java/org/apache/seata/core/protocol/detector/Http2Detector.java
@@ -33,6 +33,7 @@ import org.apache.seata.core.rpc.netty.grpc.GrpcEncoder;
import org.apache.seata.core.rpc.netty.http.Http2HttpHandler;
public class Http2Detector implements ProtocolDetector {
+ // HTTP/2 connection preface for detecting h2c (plaintext HTTP/2, not
encrypted HTTPS)
private static final byte[] HTTP2_PREFIX_BYTES = "PRI *
HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(CharsetUtil.UTF_8);
private final ChannelHandler[] serverHandlers;
diff --git
a/core/src/main/java/org/apache/seata/core/rpc/netty/http/Http2HttpHandler.java
b/core/src/main/java/org/apache/seata/core/rpc/netty/http/Http2HttpHandler.java
index feb8deda67..07172a3ad7 100644
---
a/core/src/main/java/org/apache/seata/core/rpc/netty/http/Http2HttpHandler.java
+++
b/core/src/main/java/org/apache/seata/core/rpc/netty/http/Http2HttpHandler.java
@@ -39,7 +39,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
+import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
/**
* The http2 http handler.
@@ -111,12 +114,28 @@ public class Http2HttpHandler extends
BaseHttpChannelHandler<Http2StreamFrame> {
if (request.getMethod() == HttpMethod.POST
&& request.getBody() != null
&& !request.getBody().isEmpty()) {
- // assume body is json
+ CharSequence contentTypeSeq =
request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE);
+ String contentType = contentTypeSeq != null ?
contentTypeSeq.toString() : "";
try {
- ObjectNode bodyDataNode = (ObjectNode)
OBJECT_MAPPER.readTree(request.getBody());
- requestDataNode.set("body", bodyDataNode);
+ if (contentType.contains("application/json")) {
+ ObjectNode bodyDataNode = (ObjectNode)
OBJECT_MAPPER.readTree(request.getBody());
+ requestDataNode.set("body", bodyDataNode);
+ } else if
(contentType.contains("application/x-www-form-urlencoded")) {
+ Map<String, String> formParams = new HashMap<>();
+ String[] pairs = request.getBody().split("&");
+ for (String pair : pairs) {
+ String[] kv = pair.split("=", 2);
+ if (kv.length == 2) {
+ String key = URLDecoder.decode(kv[0],
StandardCharsets.UTF_8.name());
+ String value = URLDecoder.decode(kv[1],
StandardCharsets.UTF_8.name());
+ formParams.put(key, value);
+ }
+ }
+ ObjectNode formDataNode =
OBJECT_MAPPER.valueToTree(formParams);
+ requestDataNode.set("body", formDataNode);
+ }
} catch (Exception e) {
- LOGGER.warn("Failed to parse http2 body as json: {}",
e.getMessage());
+ LOGGER.warn("Failed to parse http2 body: {}",
e.getMessage());
}
}
Object httpController = httpInvocation.getController();
diff --git
a/core/src/main/java/org/apache/seata/core/rpc/netty/http/filter/HttpRequestParamWrapper.java
b/core/src/main/java/org/apache/seata/core/rpc/netty/http/filter/HttpRequestParamWrapper.java
index 89fc7079e7..50af6437ff 100644
---
a/core/src/main/java/org/apache/seata/core/rpc/netty/http/filter/HttpRequestParamWrapper.java
+++
b/core/src/main/java/org/apache/seata/core/rpc/netty/http/filter/HttpRequestParamWrapper.java
@@ -66,7 +66,8 @@ public class HttpRequestParamWrapper {
parseQueryParams(request.getPath());
parseHeaders(request.getHeaders());
- String contentType = (String)
request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE);
+ CharSequence contentTypeSeq =
request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE);
+ String contentType = contentTypeSeq != null ?
contentTypeSeq.toString() : null;
if (contentType == null) {
return;
}
diff --git a/server/pom.xml b/server/pom.xml
index 15bce29717..ae6dc4930a 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -297,6 +297,12 @@
<artifactId>bucket4j_jdk8-core</artifactId>
<version>${bucket4j.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/server/src/test/java/org/apache/seata/server/controller/ClusterControllerTest.java
b/server/src/test/java/org/apache/seata/server/controller/ClusterControllerTest.java
index 5c3b360db2..478358783e 100644
---
a/server/src/test/java/org/apache/seata/server/controller/ClusterControllerTest.java
+++
b/server/src/test/java/org/apache/seata/server/controller/ClusterControllerTest.java
@@ -16,11 +16,14 @@
*/
package org.apache.seata.server.controller;
+import okhttp3.Protocol;
+import okhttp3.Response;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ContentType;
import org.apache.http.protocol.HTTP;
+import org.apache.seata.common.executor.HttpCallback;
import org.apache.seata.common.holder.ObjectHolder;
import org.apache.seata.common.util.HttpClientUtil;
import org.apache.seata.server.BaseSpringBootTest;
@@ -39,9 +42,14 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import static
org.apache.seata.common.ConfigurationKeys.SERVER_SERVICE_PORT_CAMEL;
import static
org.apache.seata.common.Constants.OBJECT_KEY_SPRING_APPLICATION_CONTEXT;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ClusterControllerTest extends BaseSpringBootTest {
@@ -76,6 +84,44 @@ class ClusterControllerTest extends BaseSpringBootTest {
@Test
@Order(2)
+ void watchTimeoutTest_withHttp2() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+
+ Map<String, String> params = new HashMap<>();
+ params.put("default-test", "1");
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(Protocol.H2_PRIOR_KNOWLEDGE,
response.protocol());
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED,
response.code());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Assertions.fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ Assertions.fail("Should not be cancelled");
+ }
+ };
+
+ HttpClientUtil.doPostWithHttp2(
+ "http://127.0.0.1:" + port +
"/metadata/v1/watch?timeout=3000", params, headers, callback);
+ // Currently, the server side does not have the ability to send http2
responses,
+ // so if no response is received here, it will definitely time out
+ Assertions.assertFalse(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ @Order(3)
void watch() throws Exception {
Map<String, String> header = new HashMap<>();
header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
@@ -106,7 +152,7 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(3)
+ @Order(4)
void testXssFilterBlocked_queryParam() throws Exception {
String malicious = "<script>alert('xss')</script>";
Map<String, String> header = new HashMap<>();
@@ -123,7 +169,118 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(4)
+ @Order(5)
+ void testXssFilterBlocked_queryParam_withGetHttp2() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ String malicious = "<script>alert('xss')</script>";
+ Map<String, String> header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ assertNotNull(response);
+ Assertions.assertEquals(Protocol.H2_PRIOR_KNOWLEDGE,
response.protocol());
+ Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST,
response.code());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ HttpClientUtil.doGetWithHttp2(
+ "http://127.0.0.1:" + port +
"/metadata/v1/watch?timeout=3000&testParam="
+ + URLEncoder.encode(malicious,
String.valueOf(StandardCharsets.UTF_8)),
+ header,
+ callback,
+ 5000);
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ @Order(6)
+ void testXssFilterBlocked_formParam_withPostHttp2() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ String malicious = "<script>alert('xss')</script>";
+ Map<String, String> header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+
+ Map<String, String> params = new HashMap<>();
+ params.put("key", malicious);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ assertNotNull(response);
+ Assertions.assertEquals(Protocol.H2_PRIOR_KNOWLEDGE,
response.protocol());
+ Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST,
response.code());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ HttpClientUtil.doPostWithHttp2("http://127.0.0.1:" + port + "/random",
params, header, callback, 5000);
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ @Order(7)
+ void testXssFilterBlocked_bodyParam_withPostHttp2() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ String malicious = "<script>alert('xss')</script>";
+ Map<String, String> header = new HashMap<>();
+
+ String jsonBody = "{\"key\":\"" + malicious + "\"}";
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ assertNotNull(response);
+ Assertions.assertEquals(Protocol.H2_PRIOR_KNOWLEDGE,
response.protocol());
+ Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST,
response.code());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ HttpClientUtil.doPostWithHttp2("http://127.0.0.1:" + port + "/random",
jsonBody, header, callback, 5000);
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ @Order(8)
void testXssFilterBlocked_formParam() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
@@ -139,7 +296,7 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(5)
+ @Order(9)
void testXssFilterBlocked_jsonBody() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_JSON.getMimeType());
@@ -154,7 +311,7 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(6)
+ @Order(10)
void testXssFilterBlocked_headerParam() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
@@ -171,7 +328,7 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(7)
+ @Order(11)
void testXssFilterBlocked_multiSource() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_JSON.getMimeType());
@@ -191,7 +348,7 @@ class ClusterControllerTest extends BaseSpringBootTest {
}
@Test
- @Order(8)
+ @Order(12)
void testXssFilterBlocked_formParamWithUserCustomKeyWords() throws
Exception {
Map<String, String> headers = new HashMap<>();
headers.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]