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.