diff --git a/appserver/tests/payara-samples/samples/pom.xml b/appserver/tests/payara-samples/samples/pom.xml
index aafdcf5c943..c27d35790a7 100644
--- a/appserver/tests/payara-samples/samples/pom.xml
+++ b/appserver/tests/payara-samples/samples/pom.xml
@@ -2,7 +2,7 @@
+
+
+ 4.0.0
+
+ payara-samples-profiled-tests
+ fish.payara.samples
+ 6.2024.11-SNAPSHOT
+
+
+ rfc-9110
+ Payara Samples - Payara - rfc-9110
+ war
+
+
+ UTF-8
+ false
+ 1.9.1.Final
+ 5.11.1
+ 3.0.alpha4
+ 3.3.1
+
+
+
+
+
+ org.jboss.arquillian
+ arquillian-bom
+ ${arquillian-bom.version}
+ import
+ pom
+
+
+ org.junit
+ junit-bom
+ ${junit-jupiter.version}
+ import
+ pom
+
+
+
+
+
+
+ org.jboss.arquillian.junit5
+ arquillian-junit5-container
+ test
+
+
+ org.jboss.arquillian.protocol
+ arquillian-protocol-servlet
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.jboss.shrinkwrap.resolver
+ shrinkwrap-resolver-impl-maven
+ ${shrinkwrap.version}
+ test
+
+
+ org.jboss.shrinkwrap.resolver
+ shrinkwrap-resolver-api-maven
+ ${shrinkwrap.version}
+ test
+
+
+ fish.payara.samples
+ samples-test-utils
+ test
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ test
+
+
+ org.glassfish.jersey.core
+ jersey-client
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+
+
+
\ No newline at end of file
diff --git a/appserver/tests/payara-samples/samples/rfc-9110/src/main/java/fish/payara/samples/headers/SimpleServlet.java b/appserver/tests/payara-samples/samples/rfc-9110/src/main/java/fish/payara/samples/headers/SimpleServlet.java
new file mode 100644
index 00000000000..20a4ad99203
--- /dev/null
+++ b/appserver/tests/payara-samples/samples/rfc-9110/src/main/java/fish/payara/samples/headers/SimpleServlet.java
@@ -0,0 +1,60 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2024 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/main/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+
+package fish.payara.samples.headers;
+
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Alfonso Valdez
+ */
+
+@WebServlet(value = "/test")
+public class SimpleServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getOutputStream().print("Hello Servlet");
+ }
+}
diff --git a/appserver/tests/payara-samples/samples/rfc-9110/src/test/java/fish/payara/samples/headers/PayaraValidationRFCHeadersIT.java b/appserver/tests/payara-samples/samples/rfc-9110/src/test/java/fish/payara/samples/headers/PayaraValidationRFCHeadersIT.java
new file mode 100644
index 00000000000..747781197ac
--- /dev/null
+++ b/appserver/tests/payara-samples/samples/rfc-9110/src/test/java/fish/payara/samples/headers/PayaraValidationRFCHeadersIT.java
@@ -0,0 +1,175 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) [2024] Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/main/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+
+package fish.payara.samples.headers;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Response;
+import fish.payara.samples.PayaraArquillianTestRunner;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.jboss.arquillian.junit.InSequence;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.net.URL;
+
+
+import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN;
+import static jakarta.ws.rs.client.ClientBuilder.newClient;
+import org.junit.Assert;
+
+/**
+ * @author Alfonso Valdez
+ */
+
+@ExtendWith(ArquillianExtension.class)
+public class PayaraValidationRFCHeadersIT {
+
+ private static final Logger logger = Logger.getLogger(PayaraValidationRFCHeadersIT.class.getName());
+
+ @ArquillianResource
+ private URL base;
+
+ private Client client;
+
+ @Deployment(testable = false)
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class, "headers.war")
+ .addPackage(SimpleServlet.class.getPackage());
+ }
+
+ @BeforeEach
+ public void setup() {
+ logger.info("setting client");
+ this.client = newClient();
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidLFHeader() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\n hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidLFHeaderASCII() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\x0A hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidNULHeader() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\0 hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidNULHeaderASCII() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\x00 hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidCRHeader() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\r hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testInvalidCRHeaderASCII() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("InvalidValue", "\\x0D hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(400, headerResponse.getStatus());
+ }
+ }
+
+ @Test
+ @RunAsClient
+ public void testValidHeader() throws Exception {
+ logger.log(Level.INFO, " client: {0}, baseURL: {1}", new Object[]{client, base});
+ final var headersTarget = this.client.target(new URL(this.base, "test").toExternalForm());
+ try (final Response headerResponse = headersTarget.request()
+ .accept(TEXT_PLAIN).header("ValidValue", "hello").get()) {
+ logger.log(Level.INFO, " response: {0}", new Object[]{headerResponse});
+ Assert.assertEquals(200, headerResponse.getStatus());
+ }
+ }
+}
\ No newline at end of file
diff --git a/appserver/web/web-core/src/main/java/org/apache/catalina/LogFacade.java b/appserver/web/web-core/src/main/java/org/apache/catalina/LogFacade.java
index fbc5129c817..3bb1f3fa64c 100644
--- a/appserver/web/web-core/src/main/java/org/apache/catalina/LogFacade.java
+++ b/appserver/web/web-core/src/main/java/org/apache/catalina/LogFacade.java
@@ -56,6 +56,8 @@
* limitations under the License.
*/
+// Portions Copyright [2024] [Payara Foundation and/or its affiliates]
+
package org.apache.catalina;
import org.glassfish.logging.annotation.LogMessageInfo;
@@ -3635,5 +3637,11 @@ public static Logger getLogger() {
level = "WARNING"
)
public static final String NONCACHEABLE_UNSAFE_PUSH_METHOD_EXCEPTION = prefix + "00548";
+
+ @LogMessageInfo(
+ message = "Invalid Header value, you cannot include CR, LF or NUL characters, please refer to RFC-9110",
+ level = "WARNING"
+ )
+ public static final String INVALID_HEADER_VALUE_RFC_9110 = prefix + "00549";
}
diff --git a/appserver/web/web-core/src/main/java/org/apache/catalina/connector/CoyoteAdapter.java b/appserver/web/web-core/src/main/java/org/apache/catalina/connector/CoyoteAdapter.java
index e850b6e49a6..cbf65eb5327 100644
--- a/appserver/web/web-core/src/main/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/appserver/web/web-core/src/main/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -55,10 +55,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// Portions Copyright [2022] [Payara Foundation and/or its affiliates]
+// Portions Copyright [2022-2024] [Payara Foundation and/or its affiliates]
package org.apache.catalina.connector;
-import java.io.IOException;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -68,6 +67,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -132,6 +133,8 @@ public class CoyoteAdapter extends HttpHandler {
Boolean.valueOf(System.getProperty(
"com.sun.enterprise.web.collapseAdjacentSlashes", "true"));
+ private static String INVALID_PATTERNS_RFC_9110 = "(\\\\n)|(\\\\0)|(\\\\r)|(\\\\x00)|(\\\\x0A)|(\\\\x0D)";
+
/**
* When mod_jk is used, the adapter must be invoked the same way
* Tomcat does by invoking service(...) and the afterService(...). This
@@ -303,7 +306,15 @@ private void doService(final org.glassfish.grizzly.http.server.Request req,
// if (connector.isXpoweredBy()) {
// response.addHeader("X-Powered-By", POWERED_BY);
// }
-
+ //adding validation to reject request if invalid characters available RFC-9110
+ if (!validateHeaderValues(req)) {
+
+ if (log.isLoggable(Level.INFO)) {
+ log.log(Level.INFO, LogFacade.INVALID_HEADER_VALUE_RFC_9110);
+ }
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, LogFacade.INVALID_HEADER_VALUE_RFC_9110);
+ return;
+ }
// Parse and set Catalina and configuration specific
// request parameters
@@ -1031,6 +1042,27 @@ public int getPort() {
return connector.getPort();
}
+ /**
+ * Method to validate invalid characters on header values based on the specification RFC-9110
+ * @param req represents the Request that contains the Header values
+ * @return boolean false if any of the evaluated characters is available in any of the header values, true if the headers
+ * don't contain any of the problematic characters
+ */
+ private boolean validateHeaderValues(final org.glassfish.grizzly.http.server.Request req) {
+ Iterable headers = req.getHeaderNames();
+ Pattern p = Pattern.compile(INVALID_PATTERNS_RFC_9110);
+ if (headers != null) {
+ for (String nameHeader : headers) {
+ String headerValue = req.getHeader(nameHeader);
+ Matcher matcher = p.matcher(headerValue);
+ if (matcher.find()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
/**
* AfterServiceListener, which is responsible for recycle catalina request and response
* objects.