[Git][java-team/jetty9][bullseye] Import Debian changes 9.4.39-3+deb11u2

Markus Koschany (@apo) gitlab at salsa.debian.org
Sat Sep 30 13:22:14 BST 2023



Markus Koschany pushed to branch bullseye at Debian Java Maintainers / jetty9


Commits:
8648cca8 by Markus Koschany at 2023-09-30T14:19:09+02:00
Import Debian changes 9.4.39-3+deb11u2

jetty9 (9.4.39-3+deb11u2) bullseye-security; urgency=high
.
  * Team upload.
  * The org.eclipse.jetty.servlets.CGI has been deprecated. It is potentially
    unsafe to use it. The upstream developers of Jetty recommend to use Fast CGI
    instead. See also CVE-2023-36479.
  * Fix CVE-2023-26048:
    Jetty is a java based web server and servlet engine. In affected versions
    servlets with multipart support (e.g. annotated with `@MultipartConfig`)
    that call `HttpServletRequest.getParameter()` or
    `HttpServletRequest.getParts()` may cause `OutOfMemoryError` when the
    client sends a multipart request with a part that has a name but no
    filename and very large content. This happens even with the default
    settings of `fileSizeThreshold=0` which should stream the whole part
    content to disk.
  * Fix CVE-2023-26049:
    Nonstandard cookie parsing in Jetty may allow an attacker to smuggle
    cookies within other cookies, or otherwise perform unintended behavior by
    tampering with the cookie parsing mechanism.
  * Fix CVE-2023-40167:
    Prior to this version Jetty accepted the `+` character proceeding the
    content-length value in a HTTP/1 header field. This is more permissive than
    allowed by the RFC and other servers routinely reject such requests with
    400 responses. There is no known exploit scenario, but it is conceivable
    that request smuggling could result if jetty is used in combination with a
    server that does not close the connection after sending such a 400
    response.
  * CVE-2023-36479:
    Users of the CgiServlet with a very specific command structure may have the
    wrong command executed. If a user sends a request to a
    org.eclipse.jetty.servlets.CGI Servlet for a binary with a space in its
    name, the servlet will escape the command by wrapping it in quotation
    marks. This wrapped command, plus an optional command prefix, will then be
    executed through a call to Runtime.exec. If the original binary name
    provided by the user contains a quotation mark followed by a space, the
    resulting command line will contain multiple tokens instead of one.
  * Fix CVE-2023-41900:
    Jetty is vulnerable to weak authentication. If a Jetty
    `OpenIdAuthenticator` uses the optional nested `LoginService`, and that
    `LoginService` decides to revoke an already authenticated user, then the
    current request will still treat the user as authenticated. The
    authentication is then cleared from the session and subsequent requests
    will not be treated as authenticated. So a request on a previously
    authenticated session could be allowed to bypass authentication after it
    had been rejected by the `LoginService`. This impacts usages of the
    jetty-openid which have configured a nested `LoginService` and where that
    `LoginService` is capable of rejecting previously authenticated users.

- - - - -


10 changed files:

- debian/changelog
- debian/patches/CVE-2021-34429.patch
- debian/patches/CVE-2022-2047.patch
- debian/patches/CVE-2022-2048.patch
- + debian/patches/CVE-2023-26048.patch
- + debian/patches/CVE-2023-26049.patch
- + debian/patches/CVE-2023-36479.patch
- + debian/patches/CVE-2023-40167.patch
- + debian/patches/CVE-2023-41900.patch
- debian/patches/series


Changes:

=====================================
debian/changelog
=====================================
@@ -1,3 +1,53 @@
+jetty9 (9.4.39-3+deb11u2) bullseye-security; urgency=high
+
+  * Team upload.
+  * The org.eclipse.jetty.servlets.CGI has been deprecated. It is potentially
+    unsafe to use it. The upstream developers of Jetty recommend to use Fast CGI
+    instead. See also CVE-2023-36479.
+  * Fix CVE-2023-26048:
+    Jetty is a java based web server and servlet engine. In affected versions
+    servlets with multipart support (e.g. annotated with `@MultipartConfig`)
+    that call `HttpServletRequest.getParameter()` or
+    `HttpServletRequest.getParts()` may cause `OutOfMemoryError` when the
+    client sends a multipart request with a part that has a name but no
+    filename and very large content. This happens even with the default
+    settings of `fileSizeThreshold=0` which should stream the whole part
+    content to disk.
+  * Fix CVE-2023-26049:
+    Nonstandard cookie parsing in Jetty may allow an attacker to smuggle
+    cookies within other cookies, or otherwise perform unintended behavior by
+    tampering with the cookie parsing mechanism.
+  * Fix CVE-2023-40167:
+    Prior to this version Jetty accepted the `+` character proceeding the
+    content-length value in a HTTP/1 header field. This is more permissive than
+    allowed by the RFC and other servers routinely reject such requests with
+    400 responses. There is no known exploit scenario, but it is conceivable
+    that request smuggling could result if jetty is used in combination with a
+    server that does not close the connection after sending such a 400
+    response.
+  * CVE-2023-36479:
+    Users of the CgiServlet with a very specific command structure may have the
+    wrong command executed. If a user sends a request to a
+    org.eclipse.jetty.servlets.CGI Servlet for a binary with a space in its
+    name, the servlet will escape the command by wrapping it in quotation
+    marks. This wrapped command, plus an optional command prefix, will then be
+    executed through a call to Runtime.exec. If the original binary name
+    provided by the user contains a quotation mark followed by a space, the
+    resulting command line will contain multiple tokens instead of one.
+  * Fix CVE-2023-41900:
+    Jetty is vulnerable to weak authentication. If a Jetty
+    `OpenIdAuthenticator` uses the optional nested `LoginService`, and that
+    `LoginService` decides to revoke an already authenticated user, then the
+    current request will still treat the user as authenticated. The
+    authentication is then cleared from the session and subsequent requests
+    will not be treated as authenticated. So a request on a previously
+    authenticated session could be allowed to bypass authentication after it
+    had been rejected by the `LoginService`. This impacts usages of the
+    jetty-openid which have configured a nested `LoginService` and where that
+    `LoginService` is capable of rejecting previously authenticated users.
+
+ -- Markus Koschany <apo at debian.org>  Wed, 27 Sep 2023 18:12:02 +0200
+
 jetty9 (9.4.39-3+deb11u1) bullseye-security; urgency=high
 
   * Team upload.


=====================================
debian/patches/CVE-2021-34429.patch
=====================================
@@ -6,13 +6,13 @@ Bug-Debian: https://bugs.debian.org/991188
 Origin: https://github.com/eclipse/jetty.project/pull/6477
 ---
  .../main/java/org/eclipse/jetty/http/HttpURI.java  |  13 +-
- .../java/org/eclipse/jetty/http/HttpURITest.java   | 172 ++++++++-----
+ .../java/org/eclipse/jetty/http/HttpURITest.java   | 135 ++++++-----
  .../jetty/rewrite/handler/RedirectUtil.java        |   4 +-
  .../jetty/rewrite/handler/ValidUrlRuleTest.java    |  14 +-
  .../jetty/server/handler/ContextHandler.java       |  30 +--
  .../jetty/server/handler/ResourceHandler.java      |   2 +
  .../eclipse/jetty/server/HttpConnectionTest.java   |   6 +
- .../handler/ContextHandlerGetResourceTest.java     |  21 ++
+ .../handler/ContextHandlerGetResourceTest.java     |  22 ++
  .../org/eclipse/jetty/servlet/RequestURITest.java  |  42 +++-
  .../main/java/org/eclipse/jetty/util/URIUtil.java  | 266 +++++++++++----------
  .../eclipse/jetty/util/resource/FileResource.java  |   7 +-
@@ -21,7 +21,7 @@ Origin: https://github.com/eclipse/jetty.project/pull/6477
  .../eclipse/jetty/util/resource/URLResource.java   |   9 +-
  .../jetty/util/URIUtilCanonicalPathTest.java       |  20 ++
  .../eclipse/jetty/util/resource/ResourceTest.java  |  18 ++
- 16 files changed, 412 insertions(+), 237 deletions(-)
+ 16 files changed, 374 insertions(+), 239 deletions(-)
 
 diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
 index 74c04e0..9538468 100644
@@ -55,10 +55,10 @@ index 74c04e0..9538468 100644
      public boolean isAmbiguous()
      {
 diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
-index f6b95ba..3f3a27d 100644
+index f6b95ba..af56161 100644
 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
 +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
-@@ -230,84 +230,111 @@ public class HttpURITest
+@@ -230,84 +230,77 @@ public class HttpURITest
          assertEquals("", uri.toString());
  
          uri.setPath("/path/info");
@@ -170,6 +170,13 @@ index f6b95ba..3f3a27d 100644
 -                {"/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
 -                {"path/../info", "info", EnumSet.noneOf(Ambiguous.class)},
 -                {"path/./info", "path/info", EnumSet.noneOf(Ambiguous.class)},
+-
+-                // illegal paths
+-                {"//host/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
+-                {"/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
+-                {"../path/info", null, EnumSet.noneOf(Ambiguous.class)},
+-                {"/path/%XX/info", null, EnumSet.noneOf(Ambiguous.class)},
+-                {"/path/%2/F/info", null, EnumSet.noneOf(Ambiguous.class)},
 +public static Stream<Arguments> decodePathTests()
 +{
 +    return Arrays.stream(new Object[][]
@@ -189,63 +196,10 @@ index f6b95ba..3f3a27d 100644
 +            {"/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
 +            {"path/../info", "info", EnumSet.noneOf(Ambiguous.class)},
 +            {"path/./info", "path/info", EnumSet.noneOf(Ambiguous.class)},
-+
-+            // encoded paths
-+                {"/f%6f%6F/bar", "/foo/bar", EnumSet.noneOf(Violation.class)},
-+                {"/f%u006f%u006F/bar", "/foo/bar", EnumSet.of(Violation.UTF16)},
-+                {"/f%u0001%u0001/bar", "/f\001\001/bar", EnumSet.of(Violation.UTF16)},
-+                {"/foo/%u20AC/bar", "/foo/\u20AC/bar", EnumSet.of(Violation.UTF16)},
- 
-                 // illegal paths
--                {"//host/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
--                {"/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
--                {"../path/info", null, EnumSet.noneOf(Ambiguous.class)},
--                {"/path/%XX/info", null, EnumSet.noneOf(Ambiguous.class)},
--                {"/path/%2/F/info", null, EnumSet.noneOf(Ambiguous.class)},
-+                {"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/../path/info", null, EnumSet.noneOf(Violation.class)},
-+                {"../path/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/%XX/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/%2/F/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/%/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/%u000X/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/Fo%u0000/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/Fo%00/info", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/Foo/info%u0000", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/Foo/info%00", null, EnumSet.noneOf(Violation.class)},
-+                {"/path/%U20AC", null, EnumSet.noneOf(Violation.class)},
-+                {"%2e%2e/info", null, EnumSet.noneOf(Violation.class)},
-+                {"%u002e%u002e/info", null, EnumSet.noneOf(Violation.class)},
-+                {"%2e%2e;/info", null, EnumSet.noneOf(Violation.class)},
-+                {"%u002e%u002e;/info", null, EnumSet.noneOf(Violation.class)},
-+                {"%2e.", null, EnumSet.noneOf(Violation.class)},
-+                {"%u002e.", null, EnumSet.noneOf(Violation.class)},
-+                {".%2e", null, EnumSet.noneOf(Violation.class)},
-+                {".%u002e", null, EnumSet.noneOf(Violation.class)},
-+                {"%2e%2e", null, EnumSet.noneOf(Violation.class)},
-+                {"%u002e%u002e", null, EnumSet.noneOf(Violation.class)},
-+                {"%2e%u002e", null, EnumSet.noneOf(Violation.class)},
-+                {"%u002e%2e", null, EnumSet.noneOf(Violation.class)},
-+                {"..;/info", null, EnumSet.noneOf(Violation.class)},
-+                {"..;param/info", null, EnumSet.noneOf(Violation.class)},
  
                  // ambiguous dot encodings
                  {"scheme://host/path/%2e/info", "/path/./info", EnumSet.of(Ambiguous.SEGMENT)},
-@@ -342,6 +369,13 @@ public class HttpURITest
-                 {"%2F/info", "//info", EnumSet.of(Ambiguous.SEPARATOR)},
-                 {"/path/%2f../info", "/path//../info", EnumSet.of(Ambiguous.SEPARATOR)},
- 
-+                // ambiguous encoding
-+                {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
-+                {"/path/%u0025/info", "/path/%/info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
-+                {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
-+                {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
-+                {"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
-+
-                 // combinations
-                 {"/path/%2f/..;/info", "/path///../info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM)},
-                 {"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Ambiguous.SEPARATOR, Ambiguous.PARAM, Ambiguous.SEGMENT)},
-@@ -370,4 +404,20 @@ public class HttpURITest
+@@ -370,4 +363,20 @@ public class HttpURITest
              assertThat(decodedPath, nullValue());
          }
      }
@@ -471,10 +425,10 @@ index 72197e6..ada19a0 100644
              "Host: localhost\r\n" +
              "Connection: close\r\n" +
 diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
-index a6a471b..4f575bf 100644
+index a6a471b..8daa933 100644
 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
 +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
-@@ -245,6 +245,27 @@ public class ContextHandlerGetResourceTest
+@@ -245,6 +245,28 @@ public class ContextHandlerGetResourceTest
          assertNull(url);
      }
  
@@ -498,6 +452,7 @@ index a6a471b..4f575bf 100644
 +        assertNull(resource);
 +        resourceURL = context.getServletContext().getResource(path);
 +        assertNull(resourceURL);
++    }
 +
      @Test
      public void testDeep() throws Exception


=====================================
debian/patches/CVE-2022-2047.patch
=====================================
@@ -4,13 +4,13 @@ Subject: CVE-2022-2047
 
 Origin: https://github.com/eclipse/jetty.project/pull/8146
 ---
- .../java/org/eclipse/jetty/client/HttpRequest.java |  8 +-
- .../eclipse/jetty/client/HttpClientURITest.java    | 45 +++++++++++
- .../main/java/org/eclipse/jetty/http/HttpURI.java  | 22 +++++-
- .../java/org/eclipse/jetty/http/HttpURITest.java   | 86 ++++++++++++++++++++++
+ .../java/org/eclipse/jetty/client/HttpRequest.java |  8 ++++---
+ .../eclipse/jetty/client/HttpClientURITest.java    |  2 ++
+ .../main/java/org/eclipse/jetty/http/HttpURI.java  | 22 +++++++++++++++++-
+ .../java/org/eclipse/jetty/http/HttpURITest.java   | 26 ++++++++++++++++++++++
  .../org/eclipse/jetty/proxy/ConnectHandler.java    |  2 +-
- .../java/org/eclipse/jetty/server/Request.java     | 13 +++-
- 6 files changed, 169 insertions(+), 7 deletions(-)
+ .../java/org/eclipse/jetty/server/Request.java     | 13 +++++++++--
+ 6 files changed, 66 insertions(+), 7 deletions(-)
 
 diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java
 index 2b94c47..5419926 100644
@@ -44,7 +44,7 @@ index 2b94c47..5419926 100644
          }
          catch (URISyntaxException x)
 diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java
-index 13ca948..9766108 100644
+index 13ca948..aed6175 100644
 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java
 +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java
 @@ -29,8 +29,10 @@ import java.net.SocketException;
@@ -58,56 +58,6 @@ index 13ca948..9766108 100644
  import javax.servlet.ServletException;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
-@@ -82,6 +84,49 @@ public class HttpClientURITest extends AbstractHttpClientServerTest
-         assertEquals(HttpStatus.OK_200, request.send().getStatus());
-     }
- 
-+    @ParameterizedTest
-+    @ArgumentsSource(ScenarioProvider.class)
-+    public void testPathWithPathParameter(Scenario scenario) throws Exception
-+    {
-+        AtomicReference<CountDownLatch> serverLatchRef = new AtomicReference<>();
-+        start(scenario, new EmptyServerHandler()
-+        {
-+            @Override
-+            protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
-+            {
-+                if (jettyRequest.getHttpURI().hasAmbiguousEmptySegment())
-+                    response.setStatus(400);
-+                serverLatchRef.get().countDown();
-+            }
-+        });
-+
-+        serverLatchRef.set(new CountDownLatch(1));
-+        ContentResponse response1 = client.newRequest("localhost", connector.getLocalPort())
-+            .scheme(scenario.getScheme())
-+            .path("/url;p=v")
-+            .send();
-+        assertEquals(HttpStatus.OK_200, response1.getStatus());
-+        assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
-+
-+        // Ambiguous empty segment.
-+        serverLatchRef.set(new CountDownLatch(1));
-+        ContentResponse response2 = client.newRequest("localhost", connector.getLocalPort())
-+            .scheme(scenario.getScheme())
-+            .path(";p=v/url")
-+            .send();
-+        assertEquals(HttpStatus.BAD_REQUEST_400, response2.getStatus());
-+        assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
-+
-+        // Ambiguous empty segment.
-+        serverLatchRef.set(new CountDownLatch(1));
-+        ContentResponse response3 = client.newRequest("localhost", connector.getLocalPort())
-+            .scheme(scenario.getScheme())
-+            .path(";@host.org/url")
-+            .send();
-+        assertEquals(HttpStatus.BAD_REQUEST_400, response3.getStatus());
-+        assertTrue(serverLatchRef.get().await(5, TimeUnit.SECONDS));
-+    }
-+
-     @ParameterizedTest
-     @ArgumentsSource(ScenarioProvider.class)
-     public void testIDNHost(Scenario scenario) throws Exception
 diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
 index 9538468..2fe6420 100644
 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
@@ -173,7 +123,7 @@ index 9538468..2fe6420 100644
      {
          _query = query;
 diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
-index 3f3a27d..d8f80c8 100644
+index af56161..7060efd 100644
 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
 +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
 @@ -106,6 +106,32 @@ public class HttpURITest
@@ -209,71 +159,6 @@ index 3f3a27d..d8f80c8 100644
      @Test
      public void testExtB() throws Exception
      {
-@@ -420,4 +446,64 @@ public static Stream<Arguments> decodePathTests()
-         HttpURI httpURI = new HttpURI(input);
-         assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
-     }
-+
-+    @Test
-+    public void testRelativePathWithAuthority()
-+    {
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setAuthority("host", 0);
-+            httpURI.setPath("path");
-+        });
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setAuthority("host", 8080);
-+            httpURI.setPath(";p=v/url");
-+        });
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setAuthority("host", 0);
-+            httpURI.setPath(";");
-+        });
-+
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setPath("path");
-+            httpURI.setAuthority("host", 0);
-+        });
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setPath(";p=v/url");
-+            httpURI.setAuthority("host", 8080);
-+        });
-+        assertThrows(IllegalArgumentException.class, () ->
-+        {
-+            HttpURI httpURI = new HttpURI();
-+            httpURI.setPath(";");
-+            httpURI.setAuthority("host", 0);
-+        });
-+
-+        HttpURI uri = new HttpURI();
-+        uri.setPath("*");
-+        uri.setAuthority("host", 0);
-+        assertEquals("//host*", uri.toString());
-+        uri = new HttpURI();
-+        uri.setAuthority("host", 0);
-+        uri.setPath("*");
-+        assertEquals("//host*", uri.toString());
-+
-+        uri = new HttpURI();
-+        uri.setPath("");
-+        uri.setAuthority("host", 0);
-+        assertEquals("//host", uri.toString());
-+        uri = new HttpURI();
-+        uri.setAuthority("host", 0);
-+        uri.setPath("");
-+        assertEquals("//host", uri.toString());
-+    }
- }
 diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java
 index 12518ed..a463fef 100644
 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java


=====================================
debian/patches/CVE-2022-2048.patch
=====================================
@@ -5,8 +5,8 @@ Subject: CVE-2022-2048
 Origin: https://github.com/eclipse/jetty.project/issues/7935
 ---
  .../jetty/http2/server/HttpChannelOverHTTP2.java   |  12 +-
- .../org/eclipse/jetty/http2/server/BadURITest.java | 153 +++++++++++++++++++++
- 2 files changed, 157 insertions(+), 8 deletions(-)
+ .../org/eclipse/jetty/http2/server/BadURITest.java | 154 +++++++++++++++++++++
+ 2 files changed, 158 insertions(+), 8 deletions(-)
  create mode 100644 jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/BadURITest.java
 
 diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java
@@ -47,21 +47,26 @@ index 08db7f1..fbed608 100644
  
 diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/BadURITest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/BadURITest.java
 new file mode 100644
-index 0000000..1f19868
+index 0000000..066ce67
 --- /dev/null
 +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/BadURITest.java
-@@ -0,0 +1,153 @@
+@@ -0,0 +1,154 @@
 +//
-+// ========================================================================
-+// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
++//  ========================================================================
++//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
++//  ------------------------------------------------------------------------
++//  All rights reserved. This program and the accompanying materials
++//  are made available under the terms of the Eclipse Public License v1.0
++//  and Apache License v2.0 which accompanies this distribution.
 +//
-+// This program and the accompanying materials are made available under the
-+// terms of the Eclipse Public License v. 2.0 which is available at
-+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
-+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
++//      The Eclipse Public License is available at
++//      http://www.eclipse.org/legal/epl-v10.html
 +//
-+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-+// ========================================================================
++//      The Apache License v2.0 is available at
++//      http://www.opensource.org/licenses/apache2.0.php
++//
++//  You may elect to redistribute this code under either of these licenses.
++//  ========================================================================
 +//
 +
 +package org.eclipse.jetty.http2.server;
@@ -135,20 +140,16 @@ index 0000000..1f19868
 +        });
 +
 +        // Remove existing ErrorHandlers.
-+        while (true)
++        for (ErrorHandler errorHandler : server.getBeans(ErrorHandler.class))
 +        {
-+            ErrorHandler errorHandler = server.getBean(ErrorHandler.class);
-+            if (errorHandler == null)
-+                break;
 +            server.removeBean(errorHandler);
 +        }
 +
 +        server.addBean(new ErrorHandler()
 +        {
 +            @Override
-+            public ByteBuffer badMessageError(int status, String reason, HttpFields.Mutable fields)
++            public ByteBuffer badMessageError(int status, String reason, HttpFields fields)
 +            {
-+                // TODO: flow control should be enough.
 +                // Return a very large buffer that will cause HTTP/2 flow control exhaustion and/or TCP congestion.
 +                return ByteBuffer.allocateDirect(128 * 1024 * 1024);
 +            }
@@ -165,7 +166,7 @@ index 0000000..1f19868
 +            // Use an ambiguous path parameter so that the URI is invalid.
 +            "/foo/..;/bar",
 +            HttpVersion.HTTP_2,
-+            HttpFields.EMPTY,
++            new HttpFields(),
 +            -1
 +        );
 +        ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
@@ -192,7 +193,7 @@ index 0000000..1f19868
 +                new HostPortHttpField("localhost:" + connector.getLocalPort()),
 +                "/valid",
 +                HttpVersion.HTTP_2,
-+                HttpFields.EMPTY,
++                new HttpFields(),
 +                -1
 +            );
 +            generator.control(lease, new HeadersFrame(3, metaData2, null, true));


=====================================
debian/patches/CVE-2023-26048.patch
=====================================
@@ -0,0 +1,552 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 27 Sep 2023 17:45:03 +0200
+Subject: CVE-2023-26048
+
+Origin: https://github.com/eclipse/jetty.project/pull/9345
+---
+ .../jetty/http/MultiPartFormInputStream.java       |  22 ++-
+ .../java/org/eclipse/jetty/server/MultiParts.java  |  14 +-
+ .../java/org/eclipse/jetty/server/Request.java     |  27 ++-
+ .../jetty/servlet/MultiPartServletTest.java        | 195 ++++++++++++++++++++-
+ .../jetty/util/MultiPartInputStreamParser.java     |  23 ++-
+ .../jetty/http/client/HttpClientStreamTest.java    |   2 +-
+ .../eclipse/jetty/http/client/HttpClientTest.java  |   2 +-
+ 7 files changed, 268 insertions(+), 17 deletions(-)
+
+diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
+index a92c5a1..3420a9b 100644
+--- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
+@@ -60,8 +60,11 @@ import org.eclipse.jetty.util.log.Logger;
+ public class MultiPartFormInputStream
+ {
+     private static final Logger LOG = Log.getLogger(MultiPartFormInputStream.class);
++    private static final int DEFAULT_MAX_FORM_KEYS = 1000;
+     private static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
+     private final MultiMap<Part> _parts;
++    private final int _maxParts;
++    private int _numParts = 0;
+     private InputStream _in;
+     private MultipartConfigElement _config;
+     private String _contentType;
+@@ -323,18 +326,30 @@ public class MultiPartFormInputStream
+      * @param contextTmpDir javax.servlet.context.tempdir
+      */
+     public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
++    {
++        this(in, contentType, config, contextTmpDir, DEFAULT_MAX_FORM_KEYS);
++    }
++
++    /**
++     * @param in Request input stream
++     * @param contentType Content-Type header
++     * @param config MultipartConfigElement
++     * @param contextTmpDir javax.servlet.context.tempdir
++     * @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
++     */
++    public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
+     {
+         _contentType = contentType;
+         _config = config;
+         _contextTmpDir = contextTmpDir;
++        _maxParts = maxParts;
+         if (_contextTmpDir == null)
+             _contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
+ 
+         if (_config == null)
+             _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
+ 
+-        MultiMap parts = new MultiMap();
+-
++        MultiMap<Part> parts = new MultiMap<>();
+         if (in instanceof ServletInputStream)
+         {
+             if (((ServletInputStream)in).isFinished())
+@@ -721,6 +736,9 @@ public class MultiPartFormInputStream
+         public void startPart()
+         {
+             reset();
++            _numParts++;
++            if (_maxParts >= 0 && _numParts > _maxParts)
++                throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
+         }
+ 
+         @Override
+diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
+index 33c7f6d..720cbf7 100644
+--- a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
++++ b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
+@@ -57,7 +57,12 @@ public interface MultiParts extends Closeable
+ 
+         public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
+         {
+-            _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir);
++            this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
++        }
++
++        public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
++        {
++            _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir, maxParts);
+             _context = request.getContext();
+         }
+ 
+@@ -101,7 +106,12 @@ public interface MultiParts extends Closeable
+ 
+         public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
+         {
+-            _utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir);
++            this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
++        }
++
++        public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
++        {
++            _utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir, maxParts);
+             _context = request.getContext();
+             _request = request;
+         }
+diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
+index bc89c02..4e0ccb0 100644
+--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
++++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
+@@ -2441,7 +2441,21 @@ public class Request implements HttpServletRequest
+             if (config == null)
+                 throw new IllegalStateException("No multipart config for servlet");
+ 
+-            _multiParts = newMultiParts(config);
++            int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
++            int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
++            if (_context != null)
++            {
++                ContextHandler contextHandler = _context.getContextHandler();
++                maxFormContentSize = contextHandler.getMaxFormContentSize();
++                maxFormKeys = contextHandler.getMaxFormKeys();
++            }
++            else
++            {
++                maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
++                maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
++            }
++
++            _multiParts = newMultiParts(config, maxFormKeys);
+             setAttribute(MULTIPARTS, _multiParts);
+             Collection<Part> parts = _multiParts.getParts();
+ 
+@@ -2475,11 +2489,16 @@ public class Request implements HttpServletRequest
+             else
+                 defaultCharset = StandardCharsets.UTF_8;
+ 
++            long formContentSize = 0;
+             ByteArrayOutputStream os = null;
+             for (Part p : parts)
+             {
+                 if (p.getSubmittedFileName() == null)
+                 {
++                    formContentSize = Math.addExact(formContentSize, p.getSize());
++                    if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
++                        throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
++
+                     // Servlet Spec 3.0 pg 23, parts without filename must be put into params.
+                     String charset = null;
+                     if (p.getContentType() != null)
+@@ -2504,7 +2523,7 @@ public class Request implements HttpServletRequest
+         return _multiParts.getParts();
+     }
+ 
+-    private MultiParts newMultiParts(MultipartConfigElement config) throws IOException
++    private MultiParts newMultiParts(MultipartConfigElement config, int maxParts) throws IOException
+     {
+         MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
+         if (LOG.isDebugEnabled())
+@@ -2514,12 +2533,12 @@ public class Request implements HttpServletRequest
+         {
+             case RFC7578:
+                 return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config,
+-                    (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
++                    (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this, maxParts);
+ 
+             case LEGACY:
+             default:
+                 return new MultiParts.MultiPartsUtilParser(getInputStream(), getContentType(), config,
+-                    (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
++                    (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this, maxParts);
+         }
+     }
+ 
+diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/MultiPartServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/MultiPartServletTest.java
+index 472ae8f..96d9eba 100644
+--- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/MultiPartServletTest.java
++++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/MultiPartServletTest.java
+@@ -1,6 +1,6 @@
+ //
+ //  ========================================================================
+-//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
++//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
+ //  ------------------------------------------------------------------------
+ //  All rights reserved. This program and the accompanying materials
+ //  are made available under the terms of the Eclipse Public License v1.0
+@@ -19,10 +19,15 @@
+ package org.eclipse.jetty.servlet;
+ 
+ import java.io.IOException;
++import java.io.InputStream;
+ import java.nio.file.Files;
+ import java.nio.file.Path;
++import java.util.ArrayList;
+ import java.util.Arrays;
++import java.util.List;
++import java.util.concurrent.TimeUnit;
+ import java.util.stream.Stream;
++import java.util.zip.GZIPInputStream;
+ import javax.servlet.MultipartConfigElement;
+ import javax.servlet.ServletException;
+ import javax.servlet.http.HttpServlet;
+@@ -32,31 +37,45 @@ import javax.servlet.http.Part;
+ 
+ import org.eclipse.jetty.client.HttpClient;
+ import org.eclipse.jetty.client.api.ContentResponse;
++import org.eclipse.jetty.client.api.Response;
+ import org.eclipse.jetty.client.util.BytesContentProvider;
++import org.eclipse.jetty.client.util.InputStreamResponseListener;
+ import org.eclipse.jetty.client.util.MultiPartContentProvider;
++import org.eclipse.jetty.client.util.OutputStreamContentProvider;
++import org.eclipse.jetty.client.util.StringContentProvider;
++import org.eclipse.jetty.http.HttpFields;
++import org.eclipse.jetty.http.HttpHeader;
+ import org.eclipse.jetty.http.HttpMethod;
+ import org.eclipse.jetty.http.HttpScheme;
++import org.eclipse.jetty.http.HttpStatus;
+ import org.eclipse.jetty.http.MimeTypes;
+ import org.eclipse.jetty.http.MultiPartFormInputStream;
++import org.eclipse.jetty.io.EofException;
+ import org.eclipse.jetty.server.HttpChannel;
+ import org.eclipse.jetty.server.HttpConnectionFactory;
+ import org.eclipse.jetty.server.MultiPartFormDataCompliance;
+ import org.eclipse.jetty.server.Server;
+ import org.eclipse.jetty.server.ServerConnector;
++import org.eclipse.jetty.server.handler.gzip.GzipHandler;
+ import org.eclipse.jetty.util.IO;
+ import org.eclipse.jetty.util.log.Log;
+ import org.eclipse.jetty.util.log.Logger;
+ import org.eclipse.jetty.util.log.StacklessLogging;
+ import org.junit.jupiter.api.AfterEach;
+ import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Test;
+ import org.junit.jupiter.params.ParameterizedTest;
+ import org.junit.jupiter.params.provider.Arguments;
+ import org.junit.jupiter.params.provider.MethodSource;
+ 
+ import static org.hamcrest.MatcherAssert.assertThat;
+ import static org.hamcrest.Matchers.containsString;
++import static org.hamcrest.Matchers.equalTo;
++import static org.hamcrest.Matchers.instanceOf;
+ import static org.hamcrest.Matchers.is;
++import static org.hamcrest.Matchers.startsWith;
+ import static org.junit.jupiter.api.Assertions.assertEquals;
++import static org.junit.jupiter.api.Assertions.assertNotNull;
+ 
+ public class MultiPartServletTest
+ {
+@@ -68,6 +87,7 @@ public class MultiPartServletTest
+     private Path tmpDir;
+ 
+     private static final int MAX_FILE_SIZE = 512 * 1024;
++    private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 8;
+     private static final int LARGE_MESSAGE_SIZE = 1024 * 1024;
+ 
+     public static Stream<Arguments> data()
+@@ -75,6 +95,19 @@ public class MultiPartServletTest
+         return Arrays.asList(MultiPartFormDataCompliance.values()).stream().map(Arguments::of);
+     }
+ 
++    public static class RequestParameterServlet extends HttpServlet
++    {
++        @Override
++        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
++        {
++            req.getParameterMap();
++            req.getParts();
++            resp.setStatus(200);
++            resp.getWriter().print("success");
++            resp.getWriter().close();
++        }
++    }
++
+     public static class MultiPartServlet extends HttpServlet
+     {
+         @Override
+@@ -96,29 +129,63 @@ public class MultiPartServletTest
+         }
+     }
+ 
++    public static class MultiPartEchoServlet extends HttpServlet
++    {
++        @Override
++        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
++        {
++            if (!req.getContentType().contains(MimeTypes.Type.MULTIPART_FORM_DATA.asString()))
++            {
++                resp.sendError(400);
++                return;
++            }
++
++            resp.setContentType(req.getContentType());
++            IO.copy(req.getInputStream(), resp.getOutputStream());
++        }
++    }
++
+     @BeforeEach
+     public void start() throws Exception
+     {
+         tmpDir = Files.createTempDirectory(MultiPartServletTest.class.getSimpleName());
++        Files.deleteIfExists(tmpDir);
++        assertNotNull(tmpDir);
+ 
+         server = new Server();
+         connector = new ServerConnector(server);
+         server.addConnector(connector);
+ 
++        MultipartConfigElement config = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
++            MAX_FILE_SIZE, -1, 1);
++        MultipartConfigElement requestSizedConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
++            -1, MAX_REQUEST_SIZE, 1);
++        MultipartConfigElement defaultConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
++            -1, -1, 1);
++
+         ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+         contextHandler.setContextPath("/");
+         ServletHolder servletHolder = contextHandler.addServlet(MultiPartServlet.class, "/");
+-
+-        MultipartConfigElement config = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
+-            MAX_FILE_SIZE, -1, 1);
++        servletHolder.getRegistration().setMultipartConfig(config);
++        servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/defaultConfig");
++        servletHolder.getRegistration().setMultipartConfig(defaultConfig);
++        servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/requestSizeLimit");
++        servletHolder.getRegistration().setMultipartConfig(requestSizedConfig);
++        servletHolder = contextHandler.addServlet(MultiPartEchoServlet.class, "/echo");
+         servletHolder.getRegistration().setMultipartConfig(config);
+ 
+-        server.setHandler(contextHandler);
++        GzipHandler gzipHandler = new GzipHandler();
++        gzipHandler.addIncludedMethods(HttpMethod.POST.asString());
++        gzipHandler.addIncludedMimeTypes("multipart/form-data");
++        gzipHandler.setMinGzipSize(32);
++        gzipHandler.setHandler(contextHandler);
++        server.setHandler(gzipHandler);
+ 
+         server.start();
+ 
+         client = new HttpClient();
+         client.start();
++        client.getContentDecoderFactories().clear();
+     }
+ 
+     @AfterEach
+@@ -130,6 +197,119 @@ public class MultiPartServletTest
+         IO.delete(tmpDir.toFile());
+     }
+ 
++    @ParameterizedTest
++    @MethodSource("data")
++    public void testLargePart(MultiPartFormDataCompliance compliance) throws Exception
++    {
++        connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration()
++            .setMultiPartFormDataCompliance(compliance);
++
++        OutputStreamContentProvider content = new OutputStreamContentProvider();
++        MultiPartContentProvider multiPart = new MultiPartContentProvider();
++        multiPart.addFieldPart("param", content, null);
++        multiPart.close();
++
++        InputStreamResponseListener listener = new InputStreamResponseListener();
++        client.newRequest("localhost", connector.getLocalPort())
++            .path("/defaultConfig")
++            .scheme(HttpScheme.HTTP.asString())
++            .method(HttpMethod.POST)
++            .content(multiPart)
++            .send(listener);
++
++        // Write large amount of content to the part.
++        byte[] byteArray = new byte[1024 * 1024];
++        Arrays.fill(byteArray, (byte)1);
++        for (int i = 0; i < 128 * 2; i++)
++        {
++            content.getOutputStream().write(byteArray);
++        }
++        content.close();
++
++        Response response = listener.get(2, TimeUnit.MINUTES);
++        assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
++        String responseContent = IO.toString(listener.getInputStream());
++        assertThat(responseContent, containsString("Unable to parse form content"));
++        assertThat(responseContent, containsString("Form is larger than max length"));
++    }
++
++    @ParameterizedTest
++    @MethodSource("data")
++    public void testManyParts(MultiPartFormDataCompliance compliance) throws Exception
++    {
++        connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration()
++            .setMultiPartFormDataCompliance(compliance);
++
++        byte[] byteArray = new byte[1024];
++        Arrays.fill(byteArray, (byte)1);
++
++        MultiPartContentProvider multiPart = new MultiPartContentProvider();
++        for (int i = 0; i < 1024 * 1024; i++)
++        {
++            BytesContentProvider content = new BytesContentProvider(byteArray);
++            multiPart.addFieldPart("part" + i, content, null);
++        }
++        multiPart.close();
++
++        InputStreamResponseListener listener = new InputStreamResponseListener();
++        client.newRequest("localhost", connector.getLocalPort())
++            .path("/defaultConfig")
++            .scheme(HttpScheme.HTTP.asString())
++            .method(HttpMethod.POST)
++            .content(multiPart)
++            .send(listener);
++
++        Response response = listener.get(30, TimeUnit.SECONDS);
++        assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
++        String responseContent = IO.toString(listener.getInputStream());
++        assertThat(responseContent, containsString("Unable to parse form content"));
++        assertThat(responseContent, containsString("Form with too many parts"));
++    }
++
++    @ParameterizedTest
++    @MethodSource("data")
++    public void testMaxRequestSize(MultiPartFormDataCompliance compliance) throws Exception
++    {
++        connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration()
++            .setMultiPartFormDataCompliance(compliance);
++
++        OutputStreamContentProvider content = new OutputStreamContentProvider();
++        MultiPartContentProvider multiPart = new MultiPartContentProvider();
++        multiPart.addFieldPart("param", content, null);
++        multiPart.close();
++
++        InputStreamResponseListener listener = new InputStreamResponseListener();
++        client.newRequest("localhost", connector.getLocalPort())
++            .path("/requestSizeLimit")
++            .scheme(HttpScheme.HTTP.asString())
++            .method(HttpMethod.POST)
++            .content(multiPart)
++            .send(listener);
++
++        Throwable writeError = null;
++        try
++        {
++            // Write large amount of content to the part.
++            byte[] byteArray = new byte[1024 * 1024];
++            Arrays.fill(byteArray, (byte)1);
++            for (int i = 0; i < 512; i++)
++            {
++                content.getOutputStream().write(byteArray);
++            }
++        }
++        catch (Exception e)
++        {
++            writeError = e;
++        }
++
++        if (writeError != null)
++            assertThat(writeError, instanceOf(EofException.class));
++
++        // We should get 400 response.
++        Response response = listener.get(30, TimeUnit.SECONDS);
++        assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
++    }
++
+     @ParameterizedTest
+     @MethodSource("data")
+     public void testTempFilesDeletedOnError(MultiPartFormDataCompliance compliance) throws Exception
+@@ -161,6 +341,9 @@ public class MultiPartServletTest
+                 containsString("Multipart Mime part largePart exceeds max filesize"));
+         }
+ 
+-        assertThat(tmpDir.toFile().list().length, is(0));
++        String[] fileList = tmpDir.toFile().list();
++        assertNotNull(fileList);
++        assertThat(fileList.length, is(0));
+     }
++
+ }
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
+index cb4017d..6dedd7c 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
+@@ -64,8 +64,11 @@ import org.eclipse.jetty.util.log.Logger;
+ public class MultiPartInputStreamParser
+ {
+     private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.class);
++    private static final int DEFAULT_MAX_FORM_KEYS = 1000;
+     public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
+-    public static final MultiMap<Part> EMPTY_MAP = new MultiMap(Collections.emptyMap());
++    public static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
++    private final int _maxParts;
++    private int _numParts;
+     protected InputStream _in;
+     protected MultipartConfigElement _config;
+     protected String _contentType;
+@@ -394,10 +397,23 @@ public class MultiPartInputStreamParser
+      * @param contextTmpDir javax.servlet.context.tempdir
+      */
+     public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
++    {
++        this(in, contentType, config, contextTmpDir, DEFAULT_MAX_FORM_KEYS);
++    }
++
++    /**
++     * @param in Request input stream
++     * @param contentType Content-Type header
++     * @param config MultipartConfigElement
++     * @param contextTmpDir javax.servlet.context.tempdir
++     * @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
++     */
++    public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
+     {
+         _contentType = contentType;
+         _config = config;
+         _contextTmpDir = contextTmpDir;
++        _maxParts = maxParts;
+         if (_contextTmpDir == null)
+             _contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
+ 
+@@ -693,6 +709,11 @@ public class MultiPartInputStreamParser
+                     continue;
+                 }
+ 
++                // Check if we can create a new part.
++                _numParts++;
++                if (_maxParts >= 0 && _numParts > _maxParts)
++                    throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
++
+                 //Have a new Part
+                 MultiPart part = new MultiPart(name, filename);
+                 part.setHeaders(headers);
+diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientStreamTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientStreamTest.java
+index 4e38e0e..7da042f 100644
+--- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientStreamTest.java
++++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientStreamTest.java
+@@ -1,6 +1,6 @@
+ //
+ //  ========================================================================
+-//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
++//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
+ //  ------------------------------------------------------------------------
+ //  All rights reserved. This program and the accompanying materials
+ //  are made available under the terms of the Eclipse Public License v1.0
+diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
+index 407054f..91c8379 100644
+--- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
++++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
+@@ -1,6 +1,6 @@
+ //
+ //  ========================================================================
+-//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
++//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
+ //  ------------------------------------------------------------------------
+ //  All rights reserved. This program and the accompanying materials
+ //  are made available under the terms of the Eclipse Public License v1.0


=====================================
debian/patches/CVE-2023-26049.patch
=====================================
@@ -0,0 +1,367 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 27 Sep 2023 17:52:07 +0200
+Subject: CVE-2023-26049
+
+Origin: https://github.com/eclipse/jetty.project/pull/9352
+---
+ .../org/eclipse/jetty/http/CookieCompliance.java   |   1 +
+ .../org/eclipse/jetty/server/CookieCutter.java     | 102 ++++++++++++++-------
+ .../jetty/server/CookieCutterLenientTest.java      |   3 +-
+ .../org/eclipse/jetty/server/CookieCutterTest.java |  90 +++++++++++++++++-
+ .../java/org/eclipse/jetty/server/RequestTest.java |   2 +
+ 5 files changed, 157 insertions(+), 41 deletions(-)
+
+diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
+index b6b640b..734b029 100644
+--- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
+@@ -24,5 +24,6 @@ package org.eclipse.jetty.http;
+ public enum CookieCompliance
+ {
+     RFC6265,
++    RFC6265_LEGACY, // Forgiving of bad quotes.
+     RFC2965
+ }
+diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
+index 6d72c3e..c9678b9 100644
+--- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
++++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
+@@ -154,6 +154,13 @@ public class CookieCutter
+                 // Handle quoted values for name or value
+                 if (inQuoted)
+                 {
++                    boolean eol = c == 0 && i == hdr.length();
++                    if (!eol && _compliance != CookieCompliance.RFC2965 && isRFC6265RejectedCharacter(inQuoted, c))
++                    {
++                        reject = true;
++                        continue;
++                    }
++
+                     if (escaped)
+                     {
+                         escaped = false;
+@@ -182,15 +189,24 @@ public class CookieCutter
+                             continue;
+ 
+                         case 0:
+-                            // unterminated quote, let's ignore quotes
++                            // unterminated quote
++                            if (_compliance == CookieCompliance.RFC6265)
++                                continue;
++                            // let's ignore quotes
+                             unquoted.setLength(0);
+                             inQuoted = false;
+                             i--;
+                             continue;
+ 
++                        case ';':
++                            if (_compliance == CookieCompliance.RFC6265)
++                                reject = true;
++                            else
++                                unquoted.append(c);
++                            continue;
++
+                         default:
+                             unquoted.append(c);
+-                            continue;
+                     }
+                 }
+                 else
+@@ -198,6 +214,13 @@ public class CookieCutter
+                     // Handle name and value state machines
+                     if (invalue)
+                     {
++                        boolean eol = c == 0 && i == hdr.length();
++                        if (!eol && _compliance == CookieCompliance.RFC6265 && isRFC6265RejectedCharacter(inQuoted, c))
++                        {
++                            reject = true;
++                            continue;
++                        }
++
+                         // parse the cookie-value
+                         switch (c)
+                         {
+@@ -300,9 +323,20 @@ public class CookieCutter
+                                         unquoted = new StringBuilder();
+                                     break;
+                                 }
++                                else if (_compliance == CookieCompliance.RFC6265)
++                                {
++                                    reject = true;
++                                    continue;
++                                }
+                                 // fall through to default case
+ 
+                             default:
++                                if (_compliance == CookieCompliance.RFC6265 && quoted)
++                                {
++                                    reject = true;
++                                    continue;
++                                }
++
+                                 if (quoted)
+                                 {
+                                     // must have been a bad internal quote. let's fix as best we can
+@@ -312,18 +346,12 @@ public class CookieCutter
+                                     continue;
+                                 }
+ 
+-                                if (_compliance == CookieCompliance.RFC6265)
+-                                {
+-                                    if (isRFC6265RejectedCharacter(inQuoted, c))
+-                                    {
+-                                        reject = true;
+-                                    }
+-                                }
++                                if (_compliance == CookieCompliance.RFC6265_LEGACY && isRFC6265RejectedCharacter(inQuoted, c))
++                                    reject = true;
+ 
+                                 if (tokenstart < 0)
+                                     tokenstart = i;
+                                 tokenend = i;
+-                                continue;
+                         }
+                     }
+                     else
+@@ -366,13 +394,8 @@ public class CookieCutter
+                                     continue;
+                                 }
+ 
+-                                if (_compliance == CookieCompliance.RFC6265)
+-                                {
+-                                    if (isRFC6265RejectedCharacter(inQuoted, c))
+-                                    {
+-                                        reject = true;
+-                                    }
+-                                }
++                                if (_compliance != CookieCompliance.RFC2965 && isRFC6265RejectedCharacter(inQuoted, c))
++                                    reject = true;
+ 
+                                 if (tokenstart < 0)
+                                     tokenstart = i;
+@@ -390,28 +413,37 @@ public class CookieCutter
+ 
+     protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c)
+     {
+-        if (inQuoted)
++        // LEGACY test
++        if (_compliance == CookieCompliance.RFC6265_LEGACY)
+         {
+-            // We only reject if a Control Character is encountered
+-            if (Character.isISOControl(c))
++            if (inQuoted)
+             {
+-                return true;
++                // We only reject if a Control Character is encountered
++                if (Character.isISOControl(c))
++                    return true;
+             }
+-        }
+-        else
+-        {
+-            /* From RFC6265 - Section 4.1.1 - Syntax
+-             *  cookie-octet  = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+-             *                  ; US-ASCII characters excluding CTLs,
+-             *                  ; whitespace DQUOTE, comma, semicolon,
+-             *                  ; and backslash
+-             */
+-            return Character.isISOControl(c) || // control characters
+-                c > 127 || // 8-bit characters
+-                c == ',' || // comma
+-                c == ';'; // semicolon
++            else
++            {
++                return Character.isISOControl(c) || // control characters
++                    c > 127 || // 8-bit characters
++                    c == ',' || // comma
++                    c == ';'; // semicolon
++            }
++            return false;
+         }
+ 
+-        return false;
++        /* From RFC6265 - Section 4.1.1 - Syntax
++         *  cookie-octet  = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
++         *                  ; US-ASCII characters excluding CTLs,
++         *                  ; whitespace DQUOTE, comma, semicolon,
++         *                  ; and backslash
++         *
++         * Note: DQUOTE and semicolon are used as separator by the parser,
++         * so we can consider them authorized.
++         */
++        return c > 127 ||  // 8-bit characters
++            Character.isISOControl(c) || // control characters
++            c == ',' || // comma
++            c == '\\';  // backslash
+     }
+ }
+diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java
+index 516ab98..af7237e 100644
+--- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java
++++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java
+@@ -21,6 +21,7 @@ package org.eclipse.jetty.server;
+ import java.util.stream.Stream;
+ import javax.servlet.http.Cookie;
+ 
++import org.eclipse.jetty.http.CookieCompliance;
+ import org.junit.jupiter.params.ParameterizedTest;
+ import org.junit.jupiter.params.provider.Arguments;
+ import org.junit.jupiter.params.provider.MethodSource;
+@@ -162,7 +163,7 @@ public class CookieCutterLenientTest
+     @MethodSource("data")
+     public void testLenientBehavior(String rawHeader, String expectedName, String expectedValue)
+     {
+-        CookieCutter cutter = new CookieCutter();
++        CookieCutter cutter = new CookieCutter(CookieCompliance.RFC6265_LEGACY);
+         cutter.addCookieField(rawHeader);
+         Cookie[] cookies = cutter.getCookies();
+         if (expectedName == null)
+diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
+index 4ba3db1..9d18276 100644
+--- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
++++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
+@@ -19,10 +19,13 @@
+ package org.eclipse.jetty.server;
+ 
+ import java.util.Arrays;
++import java.util.List;
+ import javax.servlet.http.Cookie;
+ 
+ import org.eclipse.jetty.http.CookieCompliance;
+ import org.junit.jupiter.api.Test;
++import org.junit.jupiter.params.ParameterizedTest;
++import org.junit.jupiter.params.provider.MethodSource;
+ 
+ import static org.hamcrest.MatcherAssert.assertThat;
+ import static org.hamcrest.Matchers.is;
+@@ -162,15 +165,13 @@ public class CookieCutterTest
+             "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
+ 
+         Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+-
+         assertThat("Cookies.length", cookies.length, is(2));
+         assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
+         assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
+ 
+         cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+-        assertThat("Cookies.length", cookies.length, is(2));
+-        assertCookie("Cookies[0]", cookies[0], "session_id", "1234\", $Version=\"1", 0, null);
+-        assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 0, null);
++        assertThat("Cookies.length", cookies.length, is(1));
++        assertCookie("Cookies[0]", cookies[0], "session_id", "1111", 0, null);
+     }
+ 
+     /**
+@@ -266,7 +267,7 @@ public class CookieCutterTest
+     {
+         char[] excessive = new char[65535];
+         Arrays.fill(excessive, ';');
+-        String rawCookie = "foo=bar; " + excessive + "; xyz=pdq";
++        String rawCookie = "foo=bar; " + new String(excessive) + "; xyz=pdq";
+ 
+         Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+ 
+@@ -274,4 +275,83 @@ public class CookieCutterTest
+         assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
+         assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null);
+     }
++
++    @ParameterizedTest
++    @MethodSource("rfc6265Cookies")
++    public void testRFC6265CookieParsing(Param param)
++    {
++        Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, param.input);
++
++        assertThat("Cookies.length (" + dump(cookies) + ")", cookies.length, is(param.expected.size()));
++        for (int i = 0; i < cookies.length; i++)
++        {
++            Cookie cookie = cookies[i];
++            assertThat("Cookies[" + i + "] (" + dump(cookies) + ")", cookie.getName() + "=" + cookie.getValue(), is(param.expected.get(i)));
++        }
++    }
++
++    public static List<Param> rfc6265Cookies()
++    {
++        return Arrays.asList(
++            new Param("A=1; B=2; C=3", "A=1", "B=2", "C=3"),
++            new Param("A=\"1\"; B=2; C=3", "A=1", "B=2", "C=3"),
++            new Param("A=\"1\"; B=\"2\"; C=\"3\"", "A=1", "B=2", "C=3"),
++            new Param("A=1; B=2; C=\"3", "A=1", "B=2"),
++            new Param("A=1 ; B=2; C=3", "A=1", "B=2", "C=3"),
++            new Param("A= 1; B=2; C=3", "A=1", "B=2", "C=3"),
++            new Param("A=\"1; B=2\"; C=3", "C=3"),
++            new Param("A=\"1; B=2; C=3"),
++            new Param("A=\"1 B=2\"; C=3", "A=1 B=2", "C=3"),
++            new Param("A=\"\"1; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\"\" ; B=2; C=3", "A=", "B=2", "C=3"),
++            new Param("A=\"\"; B=2; C=3", "A=", "B=2", "C=3"),
++            new Param("A=1\"\"; B=2; C=3", "B=2", "C=3"),
++            new Param("A=1\"; B=2; C=3", "B=2", "C=3"),
++            new Param("A=1\"1; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\" 1\"; B=2; C=3", "A= 1", "B=2", "C=3"),
++            new Param("A=\"1 \"; B=2; C=3", "A=1 ", "B=2", "C=3"),
++            new Param("A=\" 1 \"; B=2; C=3", "A= 1 ", "B=2", "C=3"),
++            new Param("A=\" 1 1 \"; B=2; C=3", "A= 1 1 ", "B=2", "C=3"),
++            new Param("A=1,; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\"1,\"; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\\1; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\"\\1\"; B=2; C=3", "B=2", "C=3"),
++            new Param("A=1\u0007; B=2; C=3", "B=2", "C=3"),
++            new Param("A=\"1\u0007\"; B=2; C=3", "B=2", "C=3"),
++            new Param("€"),
++            new Param("@={}"),
++            new Param("$X=Y; N=V", "N=V"),
++            new Param("N=V; $X=Y", "N=V")
++        );
++    }
++
++    private static String dump(Cookie[] cookies)
++    {
++        StringBuilder sb = new StringBuilder();
++        for (Cookie cookie : cookies)
++        {
++            sb.append("<").append(cookie.getName()).append(">=<").append(cookie.getValue()).append("> | ");
++        }
++        if (sb.length() > 0)
++            sb.delete(sb.length() - 2, sb.length() - 1);
++        return sb.toString();
++    }
++
++    private static class Param
++    {
++        private final String input;
++        private final List<String> expected;
++
++        public Param(String input, String... expected)
++        {
++            this.input = input;
++            this.expected = Arrays.asList(expected);
++        }
++
++        @Override
++        public String toString()
++        {
++            return input + " -> " + expected.toString();
++        }
++    }
+ }
+diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
+index f33bbce..80cd7b0 100644
+--- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
++++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
+@@ -48,6 +48,7 @@ import javax.servlet.http.HttpServletResponse;
+ import javax.servlet.http.Part;
+ 
+ import org.eclipse.jetty.http.BadMessageException;
++import org.eclipse.jetty.http.CookieCompliance;
+ import org.eclipse.jetty.http.HttpCompliance;
+ import org.eclipse.jetty.http.HttpComplianceSection;
+ import org.eclipse.jetty.http.HttpTester;
+@@ -106,6 +107,7 @@ public class RequestTest
+         http.getHttpConfiguration().setRequestHeaderSize(512);
+         http.getHttpConfiguration().setResponseHeaderSize(512);
+         http.getHttpConfiguration().setOutputBufferSize(2048);
++        http.getHttpConfiguration().setRequestCookieCompliance(CookieCompliance.RFC6265_LEGACY);
+         http.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
+         _connector = new LocalConnector(_server, http);
+         _server.addConnector(_connector);


=====================================
debian/patches/CVE-2023-36479.patch
=====================================
@@ -0,0 +1,51 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 27 Sep 2023 17:52:28 +0200
+Subject: CVE-2023-36479
+
+The org.eclipse.jetty.servlets.CGI Servlet should not be used anymore.
+Upstream recommends to use Fast CGI instead.
+
+Origin: https://github.com/eclipse/jetty.project/pull/9888
+---
+ .../src/main/java/org/eclipse/jetty/servlets/CGI.java        |  3 +++
+ .../test-jetty-webapp/src/main/webapp/WEB-INF/web.xml        | 12 ------------
+ 2 files changed, 3 insertions(+), 12 deletions(-)
+
+diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
+index ecafb6a..b271030 100644
+--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
++++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
+@@ -67,7 +67,10 @@ import org.eclipse.jetty.util.log.Logger;
+  * <dt>ignoreExitState</dt>
+  * <dd>If true then do not act on a non-zero exec exit status")</dd>
+  * </dl>
++ *
++ * @deprecated do not use, no replacement, will be removed in a future release.
+  */
++ at Deprecated
+ public class CGI extends HttpServlet
+ {
+     private static final long serialVersionUID = -6182088932884791074L;
+diff --git a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
+index 05e4f1d..ef7e279 100644
+--- a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
++++ b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
+@@ -121,18 +121,6 @@
+     <url-pattern>/dispatch/*</url-pattern>
+   </servlet-mapping>
+   
+-  <servlet>
+-    <servlet-name>CGI</servlet-name>
+-    <servlet-class>org.eclipse.jetty.servlets.CGI</servlet-class>
+-    <load-on-startup>1</load-on-startup>
+-    <async-supported>true</async-supported>
+-  </servlet>
+-  
+-  <servlet-mapping>
+-    <servlet-name>CGI</servlet-name>
+-    <url-pattern>/cgi-bin/*</url-pattern>
+-  </servlet-mapping>
+-  
+   <servlet>
+     <servlet-name>Chat</servlet-name>
+     <servlet-class>com.acme.ChatServlet</servlet-class>


=====================================
debian/patches/CVE-2023-40167.patch
=====================================
@@ -0,0 +1,268 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 27 Sep 2023 17:52:50 +0200
+Subject: CVE-2023-40167
+
+Origin: https://github.com/eclipse/jetty.project/commit/e4d596eafc887bcd813ae6e28295b5ce327def47
+---
+ .../java/org/eclipse/jetty/http/HttpParser.java    | 48 ++++++------
+ .../org/eclipse/jetty/http/HttpParserTest.java     | 87 +++++++---------------
+ 2 files changed, 51 insertions(+), 84 deletions(-)
+
+diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
+index 4637006..00c714f 100644
+--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
+@@ -493,7 +493,7 @@ public class HttpParser
+     /* Quick lookahead for the start state looking for a request method or an HTTP version,
+      * otherwise skip white space until something else to parse.
+      */
+-    private boolean quickStart(ByteBuffer buffer)
++    private void quickStart(ByteBuffer buffer)
+     {
+         if (_requestHandler != null)
+         {
+@@ -504,7 +504,7 @@ public class HttpParser
+                 buffer.position(buffer.position() + _methodString.length() + 1);
+ 
+                 setState(State.SPACE1);
+-                return false;
++                return;
+             }
+         }
+         else if (_responseHandler != null)
+@@ -514,7 +514,7 @@ public class HttpParser
+             {
+                 buffer.position(buffer.position() + _version.asString().length() + 1);
+                 setState(State.SPACE1);
+-                return false;
++                return;
+             }
+         }
+ 
+@@ -535,7 +535,7 @@ public class HttpParser
+                     _string.setLength(0);
+                     _string.append(t.getChar());
+                     setState(_requestHandler != null ? State.METHOD : State.RESPONSE_VERSION);
+-                    return false;
++                    return;
+                 }
+                 case OTEXT:
+                 case SPACE:
+@@ -553,7 +553,6 @@ public class HttpParser
+                 throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
+             }
+         }
+-        return false;
+     }
+ 
+     private void setString(String s)
+@@ -968,23 +967,20 @@ public class HttpParser
+                     case CONTENT_LENGTH:
+                         if (_hasTransferEncoding && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
+                             throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length");
+-
++                        long contentLength = convertContentLength(_valueString);
+                         if (_hasContentLength)
+                         {
+                             if (complianceViolation(MULTIPLE_CONTENT_LENGTHS))
+                                 throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.description);
+-                            if (convertContentLength(_valueString) != _contentLength)
+-                                throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.description);
++                            if (contentLength != _contentLength)
++                                throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.getDescription());
+                         }
+                         _hasContentLength = true;
+ 
+                         if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
+                         {
+-                            _contentLength = convertContentLength(_valueString);
+-                            if (_contentLength <= 0)
+-                                _endOfContent = EndOfContent.NO_CONTENT;
+-                            else
+-                                _endOfContent = EndOfContent.CONTENT_LENGTH;
++                            _contentLength = contentLength;
++                            _endOfContent = EndOfContent.CONTENT_LENGTH;
+                         }
+                         break;
+ 
+@@ -1101,15 +1097,21 @@ public class HttpParser
+ 
+     private long convertContentLength(String valueString)
+     {
+-        try
+-        {
+-            return Long.parseLong(valueString);
+-        }
+-        catch (NumberFormatException e)
++        if (valueString == null || valueString.length() == 0)
++            throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
++
++        long value = 0;
++        int length = valueString.length();
++
++        for (int i = 0; i < length; i++)
+         {
+-            LOG.ignore(e);
+-            throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Invalid Content-Length Value", e);
++            char c = valueString.charAt(i);
++            if (c < '0' || c > '9')
++                throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
++
++            value = Math.addExact(Math.multiplyExact(value, 10L), c - '0');
+         }
++        return value;
+     }
+ 
+     /*
+@@ -1505,12 +1507,11 @@ public class HttpParser
+                 _methodString = null;
+                 _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+                 _header = null;
+-                if (quickStart(buffer))
+-                    return true;
++                quickStart(buffer);
+             }
+ 
+             // Request/response line
+-            if (_state.ordinal() >= State.START.ordinal() && _state.ordinal() < State.HEADER.ordinal())
++            if (_state.ordinal() < State.HEADER.ordinal())
+             {
+                 if (parseLine(buffer))
+                     return true;
+@@ -2030,7 +2031,6 @@ public class HttpParser
+         }
+     }
+ 
+-    @SuppressWarnings("serial")
+     private static class IllegalCharacterException extends BadMessageException
+     {
+         private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer)
+diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
+index f886ad6..d8a010b 100644
+--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
++++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
+@@ -41,6 +41,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
+ import static org.hamcrest.Matchers.contains;
+ import static org.hamcrest.Matchers.containsString;
+ import static org.hamcrest.Matchers.is;
++import static org.hamcrest.Matchers.notNullValue;
+ import static org.hamcrest.Matchers.nullValue;
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertFalse;
+@@ -1672,7 +1673,7 @@ public class HttpParserTest
+     }
+ 
+     @Test
+-    public void testUnknownReponseVersion()
++    public void testUnknownResponseVersion()
+     {
+         ByteBuffer buffer = BufferUtil.toBuffer(
+             "HPPT/7.7 200 OK\r\n" +
+@@ -1815,65 +1816,31 @@ public class HttpParserTest
+         assertEquals(HttpParser.State.CLOSED, parser.getState());
+     }
+ 
+-    @Test
+-    public void testBadContentLength0()
+-    {
+-        ByteBuffer buffer = BufferUtil.toBuffer(
+-            "GET / HTTP/1.0\r\n" +
+-                "Content-Length: abc\r\n" +
+-                "Connection: close\r\n" +
+-                "\r\n");
+-
+-        HttpParser.RequestHandler handler = new Handler();
+-        HttpParser parser = new HttpParser(handler);
+-
+-        parser.parseNext(buffer);
+-        assertEquals("GET", _methodOrVersion);
+-        assertEquals("Invalid Content-Length Value", _bad);
+-        assertFalse(buffer.hasRemaining());
+-        assertEquals(HttpParser.State.CLOSE, parser.getState());
+-        parser.atEOF();
+-        parser.parseNext(BufferUtil.EMPTY_BUFFER);
+-        assertEquals(HttpParser.State.CLOSED, parser.getState());
+-    }
+-
+-    @Test
+-    public void testBadContentLength1()
+-    {
+-        ByteBuffer buffer = BufferUtil.toBuffer(
+-            "GET / HTTP/1.0\r\n" +
+-                "Content-Length: 9999999999999999999999999999999999999999999999\r\n" +
+-                "Connection: close\r\n" +
+-                "\r\n");
+-
+-        HttpParser.RequestHandler handler = new Handler();
+-        HttpParser parser = new HttpParser(handler);
+-
+-        parser.parseNext(buffer);
+-        assertEquals("GET", _methodOrVersion);
+-        assertEquals("Invalid Content-Length Value", _bad);
+-        assertFalse(buffer.hasRemaining());
+-        assertEquals(HttpParser.State.CLOSE, parser.getState());
+-        parser.atEOF();
+-        parser.parseNext(BufferUtil.EMPTY_BUFFER);
+-        assertEquals(HttpParser.State.CLOSED, parser.getState());
+-    }
+-
+-    @Test
+-    public void testBadContentLength2()
+-    {
+-        ByteBuffer buffer = BufferUtil.toBuffer(
+-            "GET / HTTP/1.0\r\n" +
+-                "Content-Length: 1.5\r\n" +
+-                "Connection: close\r\n" +
+-                "\r\n");
++    @ParameterizedTest
++    @ValueSource(strings = {
++        "abc",
++        "1.5",
++        "9999999999999999999999999999999999999999999999",
++        "-10",
++        "+10",
++        "1.0",
++        "1,0",
++        "10,"
++    })
++    public void testBadContentLengths(String contentLength)
++    {
++        ByteBuffer buffer = BufferUtil.toBuffer(
++            "GET /test HTTP/1.1\r\n" +
++                "Host: localhost\r\n" +
++                "Content-Length: " + contentLength + "\r\n" +
++                "\r\n" +
++                "1234567890\r\n");
+ 
+         HttpParser.RequestHandler handler = new Handler();
+-        HttpParser parser = new HttpParser(handler);
++        HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
++        parseAll(parser, buffer);
+ 
+-        parser.parseNext(buffer);
+-        assertEquals("GET", _methodOrVersion);
+-        assertEquals("Invalid Content-Length Value", _bad);
++        assertThat(_bad, notNullValue());
+         assertFalse(buffer.hasRemaining());
+         assertEquals(HttpParser.State.CLOSE, parser.getState());
+         parser.atEOF();
+@@ -2084,7 +2051,7 @@ public class HttpParserTest
+     @Test
+     public void testBadIPv6Host()
+     {
+-        try (StacklessLogging s = new StacklessLogging(HttpParser.class))
++        try (StacklessLogging ignored = new StacklessLogging(HttpParser.class))
+         {
+             ByteBuffer buffer = BufferUtil.toBuffer(
+                 "GET / HTTP/1.1\r\n" +
+@@ -2930,8 +2897,8 @@ public class HttpParserTest
+     private String _methodOrVersion;
+     private String _uriOrStatus;
+     private String _versionOrReason;
+-    private List<HttpField> _fields = new ArrayList<>();
+-    private List<HttpField> _trailers = new ArrayList<>();
++    private final List<HttpField> _fields = new ArrayList<>();
++    private final List<HttpField> _trailers = new ArrayList<>();
+     private String[] _hdr;
+     private String[] _val;
+     private int _headers;


=====================================
debian/patches/CVE-2023-41900.patch
=====================================
@@ -0,0 +1,1164 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 27 Sep 2023 17:59:57 +0200
+Subject: CVE-2023-41900
+
+Origin: https://github.com/eclipse/jetty.project/pull/9660
+---
+ .../jetty/security/openid/OpenIdAuthenticator.java | 271 ++++++++++++++-------
+ .../jetty/security/openid/OpenIdCredentials.java   |  19 +-
+ .../security/openid/OpenIdAuthenticationTest.java  | 187 +++++++++++---
+ .../jetty/security/openid/OpenIdProvider.java      | 250 ++++++++++++++++---
+ 4 files changed, 562 insertions(+), 165 deletions(-)
+
+diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
+index aa0a5a3..13e5e0e 100644
+--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
++++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
+@@ -1,6 +1,6 @@
+ //
+ //  ========================================================================
+-//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
++//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
+ //  ------------------------------------------------------------------------
+ //  All rights reserved. This program and the accompanying materials
+ //  are made available under the terms of the Eclipse Public License v1.0
+@@ -19,9 +19,12 @@
+ package org.eclipse.jetty.security.openid;
+ 
+ import java.io.IOException;
++import java.io.Serializable;
+ import java.math.BigInteger;
+ import java.nio.charset.StandardCharsets;
+ import java.security.SecureRandom;
++import java.util.LinkedHashMap;
++import java.util.Map;
+ import java.util.Objects;
+ import javax.servlet.ServletRequest;
+ import javax.servlet.ServletResponse;
+@@ -72,10 +75,14 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+     public static final String J_URI = "org.eclipse.jetty.security.openid.URI";
+     public static final String J_POST = "org.eclipse.jetty.security.openid.POST";
+     public static final String J_METHOD = "org.eclipse.jetty.security.openid.METHOD";
+-    public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token";
+     public static final String J_SECURITY_CHECK = "/j_security_check";
+     public static final String ERROR_PARAMETER = "error_description_jetty";
++    private static final String CSRF_MAP = "org.eclipse.jetty.security.openid.csrf_map";
++
++    @Deprecated
++    public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token";
+ 
++    private final SecureRandom _secureRandom = new SecureRandom();
+     private OpenIdConfiguration _configuration;
+     private String _errorPage;
+     private String _errorPath;
+@@ -117,18 +124,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         return Constraint.__OPENID_AUTH;
+     }
+ 
+-    /**
+-     * If true, uris that cause a redirect to a login page will always
+-     * be remembered. If false, only the first uri that leads to a login
+-     * page redirect is remembered.
+-     *
+-     * @param alwaysSave true to always save the uri
+-     */
++    @Deprecated
+     public void setAlwaysSaveUri(boolean alwaysSave)
+     {
+         _alwaysSaveUri = alwaysSave;
+     }
+ 
++    @Deprecated
+     public boolean isAlwaysSaveUri()
+     {
+         return _alwaysSaveUri;
+@@ -172,9 +174,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         {
+             HttpSession session = ((HttpServletRequest)request).getSession();
+             Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials);
+-            session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
+-            session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
+-            session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
++            synchronized (session)
++            {
++                session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
++                session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
++                session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
++            }
+         }
+         return user;
+     }
+@@ -189,10 +194,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         if (session == null)
+             return;
+ 
+-        //clean up session
+-        session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
+-        session.removeAttribute(CLAIMS);
+-        session.removeAttribute(RESPONSE);
++        synchronized (session)
++        {
++            session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
++            session.removeAttribute(CLAIMS);
++            session.removeAttribute(RESPONSE);
++        }
+     }
+ 
+     @Override
+@@ -207,16 +214,24 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         //See Servlet Spec 3.1 sec 13.6.3
+         HttpServletRequest httpRequest = (HttpServletRequest)request;
+         HttpSession session = httpRequest.getSession(false);
+-        if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
++        if (session == null)
+             return; //not authenticated yet
+ 
+-        String juri = (String)session.getAttribute(J_URI);
+-        if (juri == null || juri.length() == 0)
+-            return; //no original uri saved
++        String juri;
++        String method;
++        synchronized (session)
++        {
++            if (session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
++                return; //not authenticated yet
++
++            juri = (String)session.getAttribute(J_URI);
++            if (juri == null || juri.length() == 0)
++                return; //no original uri saved
+ 
+-        String method = (String)session.getAttribute(J_METHOD);
+-        if (method == null || method.length() == 0)
+-            return; //didn't save original request method
++            method = (String)session.getAttribute(J_METHOD);
++            if (method == null || method.length() == 0)
++                return; //didn't save original request method
++        }
+ 
+         StringBuffer buf = httpRequest.getRequestURL();
+         if (httpRequest.getQueryString() != null)
+@@ -225,10 +240,10 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         if (!juri.equals(buf.toString()))
+             return; //this request is not for the same url as the original
+ 
+-        //restore the original request's method on this request
++        // Restore the original request's method on this request.
+         if (LOG.isDebugEnabled())
+             LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod());
+-        Request baseRequest = Request.getBaseRequest(request);
++        Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
+         baseRequest.setMethod(method);
+     }
+ 
+@@ -240,6 +255,9 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
+         final Response baseResponse = baseRequest.getResponse();
+ 
++        if (LOG.isDebugEnabled())
++            LOG.debug("validateRequest({},{},{})", req, res, mandatory);
++
+         String uri = request.getRequestURI();
+         if (uri == null)
+             uri = URIUtil.SLASH;
+@@ -265,48 +283,56 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+             if (isJSecurityCheck(uri))
+             {
+                 String authCode = request.getParameter("code");
+-                if (authCode != null)
++                if (authCode == null)
+                 {
+-                    // Verify anti-forgery state token
+-                    String state = request.getParameter("state");
+-                    String antiForgeryToken = (String)session.getAttribute(CSRF_TOKEN);
+-                    if (antiForgeryToken == null || !antiForgeryToken.equals(state))
+-                    {
+-                        sendError(request, response, "auth failed: invalid state parameter");
+-                        return Authentication.SEND_FAILURE;
+-                    }
++                    sendError(request, response, "auth failed: no code parameter");
++                    return Authentication.SEND_FAILURE;
++                }
+ 
+-                    // Attempt to login with the provided authCode
+-                    OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request));
+-                    UserIdentity user = login(null, credentials, request);
+-                    if (user != null)
+-                    {
+-                        // Redirect to original request
+-                        String nuri;
+-                        synchronized (session)
+-                        {
+-                            nuri = (String)session.getAttribute(J_URI);
++                String state = request.getParameter("state");
++                if (state == null)
++                {
++                    sendError(request, response, "auth failed: no state parameter");
++                    return Authentication.SEND_FAILURE;
++                }
+ 
+-                            if (nuri == null || nuri.length() == 0)
+-                            {
+-                                nuri = request.getContextPath();
+-                                if (nuri.length() == 0)
+-                                    nuri = URIUtil.SLASH;
+-                            }
+-                        }
+-                        OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user);
+-                        if (LOG.isDebugEnabled())
+-                            LOG.debug("authenticated {}->{}", openIdAuth, nuri);
++                // Verify anti-forgery state token.
++                UriRedirectInfo uriRedirectInfo;
++                synchronized (session)
++                {
++                    uriRedirectInfo = removeAndClearCsrfMap(session, state);
++                }
++                if (uriRedirectInfo == null)
++                {
++                    sendError(request, response, "auth failed: invalid state parameter");
++                    return Authentication.SEND_FAILURE;
++                }
+ 
+-                        response.setContentLength(0);
+-                        baseResponse.sendRedirect(nuri, true);
+-                        return openIdAuth;
+-                    }
++                // Attempt to login with the provided authCode.
++                OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request));
++                UserIdentity user = login(null, credentials, request);
++                if (user == null)
++                {
++                    sendError(request, response, null);
++                    return Authentication.SEND_FAILURE;
+                 }
+ 
+-                // Not authenticated.
+-                sendError(request, response, null);
+-                return Authentication.SEND_FAILURE;
++                OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user);
++                if (LOG.isDebugEnabled())
++                    LOG.debug("authenticated {}->{}", openIdAuth, uriRedirectInfo.getUri());
++
++                // Save redirect info in session so original request can be restored after redirect.
++                synchronized (session)
++                {
++                    session.setAttribute(J_URI, uriRedirectInfo.getUri());
++                    session.setAttribute(J_METHOD, uriRedirectInfo.getMethod());
++                    session.setAttribute(J_POST, uriRedirectInfo.getFormParameters());
++                }
++
++                // Redirect to the original URI.
++                response.setContentLength(0);
++                baseResponse.sendRedirect(uriRedirectInfo.getUri(), true);
++                return openIdAuth;
+             }
+ 
+             // Look for cached authentication in the Session.
+@@ -319,7 +345,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+                 {
+                     if (LOG.isDebugEnabled())
+                         LOG.debug("auth revoked {}", authentication);
+-                    session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
++                    logout(request);
+                 }
+                 else
+                 {
+@@ -328,8 +354,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+                         String jUri = (String)session.getAttribute(J_URI);
+                         if (jUri != null)
+                         {
+-                            //check if the request is for the same url as the original and restore
+-                            //params if it was a post
++                            // Check if the request is for the same url as the original and restore params if it was a post.
+                             if (LOG.isDebugEnabled())
+                                 LOG.debug("auth retry {}->{}", authentication, jUri);
+                             StringBuffer buf = request.getRequestURL();
+@@ -352,6 +377,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+                             }
+                         }
+                     }
++
+                     if (LOG.isDebugEnabled())
+                         LOG.debug("auth {}", authentication);
+                     return authentication;
+@@ -366,29 +392,8 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+                 return Authentication.UNAUTHENTICATED;
+             }
+ 
+-            // Remember the current URI.
+-            synchronized (session)
+-            {
+-                // But only if it is not set already, or we save every uri that leads to a login redirect
+-                if (session.getAttribute(J_URI) == null || isAlwaysSaveUri())
+-                {
+-                    StringBuffer buf = request.getRequestURL();
+-                    if (request.getQueryString() != null)
+-                        buf.append("?").append(request.getQueryString());
+-                    session.setAttribute(J_URI, buf.toString());
+-                    session.setAttribute(J_METHOD, request.getMethod());
+-
+-                    if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
+-                    {
+-                        MultiMap<String> formParameters = new MultiMap<>();
+-                        baseRequest.extractFormParameters(formParameters);
+-                        session.setAttribute(J_POST, formParameters);
+-                    }
+-                }
+-            }
+-
+             // Send the the challenge.
+-            String challengeUri = getChallengeUri(request);
++            String challengeUri = getChallengeUri(baseRequest);
+             if (LOG.isDebugEnabled())
+                 LOG.debug("challenge {}->{}", session.getId(), challengeUri);
+             baseResponse.sendRedirect(challengeUri, true);
+@@ -469,16 +474,15 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+         return redirectUri.toString();
+     }
+ 
+-    protected String getChallengeUri(HttpServletRequest request)
++    protected String getChallengeUri(Request request)
+     {
+         HttpSession session = request.getSession();
+         String antiForgeryToken;
+         synchronized (session)
+         {
+-            antiForgeryToken = (session.getAttribute(CSRF_TOKEN) == null)
+-                ? new BigInteger(130, new SecureRandom()).toString(32)
+-                : (String)session.getAttribute(CSRF_TOKEN);
+-            session.setAttribute(CSRF_TOKEN, antiForgeryToken);
++            Map<String, UriRedirectInfo> csrfMap = ensureCsrfMap(session);
++            antiForgeryToken = new BigInteger(130, _secureRandom).toString(32);
++            csrfMap.put(antiForgeryToken, new UriRedirectInfo(request));
+         }
+ 
+         // any custom scopes requested from configuration
+@@ -499,7 +503,90 @@ public class OpenIdAuthenticator extends LoginAuthenticator
+     @Override
+     public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser)
+     {
+-        return true;
++        return req.isSecure();
++    }
++
++    private UriRedirectInfo removeAndClearCsrfMap(HttpSession session, String csrf)
++    {
++        @SuppressWarnings("unchecked")
++        Map<String, UriRedirectInfo> csrfMap = (Map<String, UriRedirectInfo>)session.getAttribute(CSRF_MAP);
++        if (csrfMap == null)
++            return null;
++
++        UriRedirectInfo uriRedirectInfo = csrfMap.get(csrf);
++        csrfMap.clear();
++        return uriRedirectInfo;
++    }
++
++    private Map<String, UriRedirectInfo> ensureCsrfMap(HttpSession session)
++    {
++        @SuppressWarnings("unchecked")
++        Map<String, UriRedirectInfo> csrfMap = (Map<String, UriRedirectInfo>)session.getAttribute(CSRF_MAP);
++        if (csrfMap == null)
++        {
++            csrfMap = new MRUMap(64);
++            session.setAttribute(CSRF_MAP, csrfMap);
++        }
++        return csrfMap;
++    }
++
++    private static class MRUMap extends LinkedHashMap<String, UriRedirectInfo>
++    {
++        private static final long serialVersionUID = 5375723072014233L;
++
++        private final int _size;
++
++        private MRUMap(int size)
++        {
++            _size = size;
++        }
++
++        @Override
++        protected boolean removeEldestEntry(Map.Entry<String, UriRedirectInfo> eldest)
++        {
++            return size() > _size;
++        }
++    }
++
++    private static class UriRedirectInfo implements Serializable
++    {
++        private static final long serialVersionUID = 139567755844461433L;
++
++        private final String _uri;
++        private final String _method;
++        private final MultiMap<String> _formParameters;
++
++        public UriRedirectInfo(Request request)
++        {
++            _uri = request.getRequestURI();
++            _method = request.getMethod();
++
++            if (MimeTypes.Type.FORM_ENCODED.is(request.getContentType()) && HttpMethod.POST.is(request.getMethod()))
++            {
++                MultiMap<String> formParameters = new MultiMap<>();
++                request.extractFormParameters(formParameters);
++                _formParameters = formParameters;
++            }
++            else
++            {
++                _formParameters = null;
++            }
++        }
++
++        public String getUri()
++        {
++            return _uri;
++        }
++
++        public String getMethod()
++        {
++            return _method;
++        }
++
++        public MultiMap<String> getFormParameters()
++        {
++            return _formParameters;
++        }
+     }
+ 
+     /**
+diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
+index a22af81..c6ba8c4 100644
+--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
++++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
+@@ -19,6 +19,7 @@
+ package org.eclipse.jetty.security.openid;
+ 
+ import java.io.Serializable;
++import java.time.Instant;
+ import java.util.Arrays;
+ import java.util.Map;
+ import java.util.concurrent.TimeUnit;
+@@ -125,12 +126,24 @@ public class OpenIdCredentials implements Serializable
+             throw new AuthenticationException("Authorized party claim value should be the client_id");
+ 
+         // Check that the ID token has not expired by checking the exp claim.
+-        long expiry = (Long)claims.get("exp");
+-        long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
+-        if (currentTimeSeconds > expiry)
++        if (isExpired())
+             throw new AuthenticationException("ID Token has expired");
+     }
+ 
++    public boolean isExpired()
++    {
++        return checkExpiry(claims);
++    }
++
++    public static boolean checkExpiry(Map<String, Object> claims)
++    {
++        if (claims == null)
++            return true;
++
++        // Check that the ID token has not expired by checking the exp claim.
++        return Instant.ofEpochSecond((Long)claims.get("exp")).isBefore(Instant.now());
++    }
++
+     private void validateAudience(OpenIdConfiguration configuration) throws AuthenticationException
+     {
+         Object aud = claims.get("aud");
+diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
+index 4290c69..6ca228a 100644
+--- a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
++++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
+@@ -18,28 +18,41 @@
+ 
+ package org.eclipse.jetty.security.openid;
+ 
++import java.io.File;
+ import java.io.IOException;
+ import java.security.Principal;
+ import java.util.Map;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.function.Consumer;
++import javax.servlet.ServletException;
+ import javax.servlet.http.HttpServlet;
+ import javax.servlet.http.HttpServletRequest;
+ import javax.servlet.http.HttpServletResponse;
+ 
+ import org.eclipse.jetty.client.HttpClient;
+ import org.eclipse.jetty.client.api.ContentResponse;
++import org.eclipse.jetty.http.HttpHeader;
+ import org.eclipse.jetty.http.HttpStatus;
+-import org.eclipse.jetty.security.Authenticator;
++import org.eclipse.jetty.security.AbstractLoginService;
+ import org.eclipse.jetty.security.ConstraintMapping;
+ import org.eclipse.jetty.security.ConstraintSecurityHandler;
++import org.eclipse.jetty.security.LoginService;
+ import org.eclipse.jetty.server.Server;
+ import org.eclipse.jetty.server.ServerConnector;
++import org.eclipse.jetty.server.UserIdentity;
++import org.eclipse.jetty.server.session.FileSessionDataStoreFactory;
+ import org.eclipse.jetty.servlet.ServletContextHandler;
++import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
++import org.eclipse.jetty.util.IO;
+ import org.eclipse.jetty.util.security.Constraint;
++import org.eclipse.jetty.util.security.Password;
+ import org.junit.jupiter.api.AfterEach;
+-import org.junit.jupiter.api.BeforeEach;
+ import org.junit.jupiter.api.Test;
+ 
+ import static org.hamcrest.MatcherAssert.assertThat;
++import static org.hamcrest.Matchers.containsString;
++import static org.hamcrest.Matchers.equalTo;
++import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+ import static org.hamcrest.Matchers.is;
+ 
+ public class OpenIdAuthenticationTest
+@@ -52,8 +65,12 @@ public class OpenIdAuthenticationTest
+     private ServerConnector connector;
+     private HttpClient client;
+ 
+-    @BeforeEach
+-    public void setup() throws Exception
++    public void setup(LoginService loginService) throws Exception
++    {
++        setup(loginService, null);
++    }
++
++    public void setup(LoginService loginService, Consumer<OpenIdConfiguration> configure) throws Exception
+     {
+         openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
+         openIdProvider.start();
+@@ -93,22 +110,29 @@ public class OpenIdAuthenticationTest
+ 
+         // security handler
+         ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+-        securityHandler.setRealmName("OpenID Connect Authentication");
++        assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
++
++        securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
++        securityHandler.setRealmName(openIdProvider.getProvider());
+         securityHandler.addConstraintMapping(profileMapping);
+         securityHandler.addConstraintMapping(loginMapping);
+         securityHandler.addConstraintMapping(adminMapping);
+ 
+         // Authentication using local OIDC Provider
+-        OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
+-
+-        // Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
+-        OpenIdLoginService loginService = new OpenIdLoginService(configuration);
+-        securityHandler.setLoginService(loginService);
+-
+-        Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
+-        securityHandler.setAuthenticator(authenticator);
++        OpenIdConfiguration openIdConfiguration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
++        if (configure != null)
++            configure.accept(openIdConfiguration);
++        securityHandler.setLoginService(new OpenIdLoginService(openIdConfiguration, loginService));
++        server.addBean(openIdConfiguration);
++        securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
+         context.setSecurityHandler(securityHandler);
+ 
++        File datastoreDir = MavenTestingUtils.getTargetTestingDir("datastore");
++        IO.delete(datastoreDir);
++        FileSessionDataStoreFactory fileSessionDataStoreFactory = new FileSessionDataStoreFactory();
++        fileSessionDataStoreFactory.setStoreDir(datastoreDir);
++        server.addBean(fileSessionDataStoreFactory);
++
+         server.start();
+         String redirectUri = "http://localhost:" + connector.getLocalPort() + "/j_security_check";
+         openIdProvider.addRedirectUri(redirectUri);
+@@ -127,41 +151,127 @@ public class OpenIdAuthenticationTest
+     @Test
+     public void testLoginLogout() throws Exception
+     {
++        setup(null);
++        openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
++
+         String appUriString = "http://localhost:" + connector.getLocalPort();
+ 
+         // Initially not authenticated
+         ContentResponse response = client.GET(appUriString + "/");
+         assertThat(response.getStatus(), is(HttpStatus.OK_200));
+-        String[] content = response.getContentAsString().split("[\r\n]+");
+-        assertThat(content.length, is(1));
+-        assertThat(content[0], is("not authenticated"));
++        String content = response.getContentAsString();
++        assertThat(content, containsString("not authenticated"));
+ 
+         // Request to login is success
+         response = client.GET(appUriString + "/login");
+         assertThat(response.getStatus(), is(HttpStatus.OK_200));
+-        content = response.getContentAsString().split("[\r\n]+");
+-        assertThat(content.length, is(1));
+-        assertThat(content[0], is("success"));
++        content = response.getContentAsString();
++        assertThat(content, containsString("success"));
+ 
+         // Now authenticated we can get info
+         response = client.GET(appUriString + "/");
+         assertThat(response.getStatus(), is(HttpStatus.OK_200));
+-        content = response.getContentAsString().split("[\r\n]+");
+-        assertThat(content.length, is(3));
+-        assertThat(content[0], is("userId: 123456789"));
+-        assertThat(content[1], is("name: Alice"));
+-        assertThat(content[2], is("email: Alice at example.com"));
++        content = response.getContentAsString();
++        assertThat(content, containsString("userId: 123456789"));
++        assertThat(content, containsString("name: Alice"));
++        assertThat(content, containsString("email: Alice at example.com"));
+ 
+         // Request to admin page gives 403 as we do not have admin role
+         response = client.GET(appUriString + "/admin");
+         assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403));
+ 
++        // We can restart the server and still be logged in as we have persistent session datastore.
++        server.stop();
++        server.start();
++        appUriString = "http://localhost:" + connector.getLocalPort();
++
++        // After restarting server the authentication is saved as a session authentication.
++        response = client.GET(appUriString + "/");
++        assertThat(response.getStatus(), is(HttpStatus.OK_200));
++        content = response.getContentAsString();
++        assertThat(content, containsString("userId: 123456789"));
++        assertThat(content, containsString("name: Alice"));
++        assertThat(content, containsString("email: Alice at example.com"));
++
+         // We are no longer authenticated after logging out
+         response = client.GET(appUriString + "/logout");
+         assertThat(response.getStatus(), is(HttpStatus.OK_200));
+-        content = response.getContentAsString().split("[\r\n]+");
+-        assertThat(content.length, is(1));
+-        assertThat(content[0], is("not authenticated"));
++        content = response.getContentAsString();
++        assertThat(content, containsString("not authenticated"));
++
++        // Test that the user was logged out successfully on the openid provider.
++        assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
++        assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
++    }
++
++    @Test
++    public void testNestedLoginService() throws Exception
++    {
++        AtomicBoolean loggedIn = new AtomicBoolean(true);
++        setup(new AbstractLoginService()
++        {
++            @Override
++            protected String[] loadRoleInfo(UserPrincipal user)
++            {
++                return new String[]{"admin"};
++            }
++
++            @Override
++            protected UserPrincipal loadUserInfo(String username)
++            {
++                return new UserPrincipal(username, new Password(""));
++            }
++
++            @Override
++            public boolean validate(UserIdentity user)
++            {
++                if (!loggedIn.get())
++                    return false;
++                return super.validate(user);
++            }
++        });
++
++        openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
++
++        String appUriString = "http://localhost:" + connector.getLocalPort();
++
++        // Initially not authenticated
++        ContentResponse response = client.GET(appUriString + "/");
++        assertThat(response.getStatus(), is(HttpStatus.OK_200));
++        String content = response.getContentAsString();
++        assertThat(content, containsString("not authenticated"));
++
++        // Request to login is success
++        response = client.GET(appUriString + "/login");
++        assertThat(response.getStatus(), is(HttpStatus.OK_200));
++        content = response.getContentAsString();
++        assertThat(content, containsString("success"));
++
++        // Now authenticated we can get info
++        response = client.GET(appUriString + "/");
++        assertThat(response.getStatus(), is(HttpStatus.OK_200));
++        content = response.getContentAsString();
++        assertThat(content, containsString("userId: 123456789"));
++        assertThat(content, containsString("name: Alice"));
++        assertThat(content, containsString("email: Alice at example.com"));
++
++        // The nested login service has supplied the admin role.
++        response = client.GET(appUriString + "/admin");
++        assertThat(response.getStatus(), is(HttpStatus.OK_200));
++
++        // This causes any validation of UserIdentity in the LoginService to fail
++        // causing subsequent requests to be redirected to the auth endpoint for login again.
++        loggedIn.set(false);
++        client.setFollowRedirects(false);
++        response = client.GET(appUriString + "/admin");
++        assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303));
++        String location = response.getHeaders().get(HttpHeader.LOCATION);
++        assertThat(location, containsString(openIdProvider.getProvider() + "/auth"));
++
++        // Note that we couldn't follow "OpenID Connect RP-Initiated Logout 1.0" because we redirect straight to auth endpoint.
++        assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(1L));
++        assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
++        assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
+     }
+ 
+     public static class LoginPage extends HttpServlet
+@@ -169,16 +279,18 @@ public class OpenIdAuthenticationTest
+         @Override
+         protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
+         {
++            response.setContentType("text/html");
+             response.getWriter().println("success");
++            response.getWriter().println("<br><a href=\"/\">Home</a>");
+         }
+     }
+ 
+     public static class LogoutPage extends HttpServlet
+     {
+         @Override
+-        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
++        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+         {
+-            request.getSession().invalidate();
++            request.logout();
+             response.sendRedirect("/");
+         }
+     }
+@@ -188,7 +300,7 @@ public class OpenIdAuthenticationTest
+         @Override
+         protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
+         {
+-            Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
++            Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
+             response.getWriter().println(userInfo.get("sub") + ": success");
+         }
+     }
+@@ -198,18 +310,20 @@ public class OpenIdAuthenticationTest
+         @Override
+         protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
+         {
+-            response.setContentType("text/plain");
++            response.setContentType("text/html");
+             Principal userPrincipal = request.getUserPrincipal();
+             if (userPrincipal != null)
+             {
+-                Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
+-                response.getWriter().println("userId: " + userInfo.get("sub"));
+-                response.getWriter().println("name: " + userInfo.get("name"));
+-                response.getWriter().println("email: " + userInfo.get("email"));
++                Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
++                response.getWriter().println("userId: " + userInfo.get("sub") + "<br>");
++                response.getWriter().println("name: " + userInfo.get("name") + "<br>");
++                response.getWriter().println("email: " + userInfo.get("email") + "<br>");
++                response.getWriter().println("<br><a href=\"/logout\">Logout</a>");
+             }
+             else
+             {
+                 response.getWriter().println("not authenticated");
++                response.getWriter().println("<br><a href=\"/login\">Login</a>");
+             }
+         }
+     }
+@@ -219,8 +333,9 @@ public class OpenIdAuthenticationTest
+         @Override
+         protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
+         {
+-            response.setContentType("text/plain");
++            response.setContentType("text/html");
+             response.getWriter().println("not authorized");
++            response.getWriter().println("<br><a href=\"/\">Home</a>");
+         }
+     }
+ }
+diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
+index eacd753..7004d99 100644
+--- a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
++++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
+@@ -19,14 +19,16 @@
+ package org.eclipse.jetty.security.openid;
+ 
+ import java.io.IOException;
++import java.io.PrintWriter;
+ import java.time.Duration;
++import java.time.Instant;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+-import java.util.Random;
++import java.util.Objects;
+ import java.util.UUID;
+ import javax.servlet.ServletException;
+ import javax.servlet.http.HttpServlet;
+@@ -40,23 +42,52 @@ import org.eclipse.jetty.server.Server;
+ import org.eclipse.jetty.server.ServerConnector;
+ import org.eclipse.jetty.servlet.ServletContextHandler;
+ import org.eclipse.jetty.servlet.ServletHolder;
+-import org.eclipse.jetty.util.StringUtil;
+ import org.eclipse.jetty.util.component.ContainerLifeCycle;
++import org.eclipse.jetty.util.log.Log;
++import org.eclipse.jetty.util.log.Logger;
++import org.eclipse.jetty.util.statistic.CounterStatistic;
+ 
+ public class OpenIdProvider extends ContainerLifeCycle
+ {
++    private static final Logger LOG = Log.getLogger(OpenIdProvider.class);
++
+     private static final String CONFIG_PATH = "/.well-known/openid-configuration";
+     private static final String AUTH_PATH = "/auth";
+     private static final String TOKEN_PATH = "/token";
++    private static final String END_SESSION_PATH = "/end_session";
+     private final Map<String, User> issuedAuthCodes = new HashMap<>();
+ 
+     protected final String clientId;
+     protected final String clientSecret;
+     protected final List<String> redirectUris = new ArrayList<>();
+-
++    private final ServerConnector connector;
++    private final Server server;
++    private int port = 0;
+     private String provider;
+-    private Server server;
+-    private ServerConnector connector;
++    private User preAuthedUser;
++    private final CounterStatistic loggedInUsers = new CounterStatistic();
++    private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
++
++    public static void main(String[] args) throws Exception
++    {
++        String clientId = "CLIENT_ID123";
++        String clientSecret = "PASSWORD123";
++        int port = 5771;
++        String redirectUri = "http://localhost:8080/j_security_check";
++
++        OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
++        openIdProvider.addRedirectUri(redirectUri);
++        openIdProvider.setPort(port);
++        openIdProvider.start();
++        try
++        {
++            openIdProvider.join();
++        }
++        finally
++        {
++            openIdProvider.stop();
++        }
++    }
+ 
+     public OpenIdProvider(String clientId, String clientSecret)
+     {
+@@ -69,25 +100,67 @@ public class OpenIdProvider extends ContainerLifeCycle
+ 
+         ServletContextHandler contextHandler = new ServletContextHandler();
+         contextHandler.setContextPath("/");
+-        contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
+-        contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
+-        contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
++        contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
++        contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
++        contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
++        contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
+         server.setHandler(contextHandler);
+ 
+         addBean(server);
+     }
+ 
++    public void setIdTokenDuration(long duration)
++    {
++        _idTokenDuration = duration;
++    }
++
++    public long getIdTokenDuration()
++    {
++        return _idTokenDuration;
++    }
++
++    public void join() throws InterruptedException
++    {
++        server.join();
++    }
++
++    public OpenIdConfiguration getOpenIdConfiguration()
++    {
++        String provider = getProvider();
++        String authEndpoint = provider + AUTH_PATH;
++        String tokenEndpoint = provider + TOKEN_PATH;
++        return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
++    }
++
++    public CounterStatistic getLoggedInUsers()
++    {
++        return loggedInUsers;
++    }
++
+     @Override
+     protected void doStart() throws Exception
+     {
++        connector.setPort(port);
+         super.doStart();
+         provider = "http://localhost:" + connector.getLocalPort();
+     }
+ 
+-    public String getProvider()
++    public void setPort(int port)
+     {
+-        if (!isStarted())
++        if (isStarted())
+             throw new IllegalStateException();
++        this.port = port;
++    }
++
++    public void setUser(User user)
++    {
++        this.preAuthedUser = user;
++    }
++
++    public String getProvider()
++    {
++        if (!isStarted() && port == 0)
++            throw new IllegalStateException("Port of OpenIdProvider not configured");
+         return provider;
+     }
+ 
+@@ -96,10 +169,10 @@ public class OpenIdProvider extends ContainerLifeCycle
+         redirectUris.add(uri);
+     }
+ 
+-    public class OpenIdAuthEndpoint extends HttpServlet
++    public class AuthEndpoint extends HttpServlet
+     {
+         @Override
+-        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
++        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
+         {
+             if (!clientId.equals(req.getParameter("client_id")))
+             {
+@@ -110,12 +183,13 @@ public class OpenIdProvider extends ContainerLifeCycle
+             String redirectUri = req.getParameter("redirect_uri");
+             if (!redirectUris.contains(redirectUri))
+             {
++                LOG.warn("invalid redirectUri {}", redirectUri);
+                 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
+                 return;
+             }
+ 
+             String scopeString = req.getParameter("scope");
+-            List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
++            List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
+             if (!scopes.contains("openid"))
+             {
+                 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
+@@ -135,20 +209,75 @@ public class OpenIdProvider extends ContainerLifeCycle
+                 return;
+             }
+ 
++            if (preAuthedUser == null)
++            {
++                PrintWriter writer = resp.getWriter();
++                resp.setContentType("text/html");
++                writer.println("<h2>Login to OpenID Connect Provider</h2>");
++                writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
++                writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
++                writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
++                writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
++                writer.println("<input type=\"submit\">");
++                writer.println("</form>");
++            }
++            else
++            {
++                redirectUser(req, preAuthedUser, redirectUri, state);
++            }
++        }
++
++        @Override
++        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
++        {
++            String redirectUri = req.getParameter("redirectUri");
++            if (!redirectUris.contains(redirectUri))
++            {
++                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
++                return;
++            }
++
++            String state = req.getParameter("state");
++            if (state == null)
++            {
++                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
++                return;
++            }
++
++            String username = req.getParameter("username");
++            if (username == null)
++            {
++                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
++                return;
++            }
++
++            User user = new User(username);
++            redirectUser(req, user, redirectUri, state);
++        }
++
++        public void redirectUser(HttpServletRequest request, User user, String redirectUri, String state) throws IOException
++        {
+             String authCode = UUID.randomUUID().toString().replace("-", "");
+-            User user = new User(123456789, "Alice");
+             issuedAuthCodes.put(authCode, user);
+ 
+-            final Request baseRequest = Request.getBaseRequest(req);
+-            final Response baseResponse = baseRequest.getResponse();
+-            redirectUri += "?code=" + authCode + "&state=" + state;
+-            int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
+-                ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
+-            baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri));
++            try
++            {
++                final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
++                final Response baseResponse = baseRequest.getResponse();
++                redirectUri += "?code=" + authCode + "&state=" + state;
++                int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
++                    ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
++                baseResponse.sendRedirect(redirectCode, baseResponse.encodeRedirectURL(redirectUri));
++            }
++            catch (Throwable t)
++            {
++                issuedAuthCodes.remove(authCode);
++                throw t;
++            }
+         }
+     }
+ 
+-    public class OpenIdTokenEndpoint extends HttpServlet
++    private class TokenEndpoint extends HttpServlet
+     {
+         @Override
+         protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+@@ -173,45 +302,79 @@ public class OpenIdProvider extends ContainerLifeCycle
+             }
+ 
+             String accessToken = "ABCDEFG";
+-            long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
++            long accessTokenDuration = Duration.ofMinutes(10).getSeconds();
+             String response = "{" +
+                 "\"access_token\": \"" + accessToken + "\"," +
+-                "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken()) + "\"," +
+-                "\"expires_in\": " + expiry + "," +
++                "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
++                "\"expires_in\": " + accessTokenDuration + "," +
+                 "\"token_type\": \"Bearer\"" +
+                 "}";
+ 
++            loggedInUsers.increment();
+             resp.setContentType("text/plain");
+             resp.getWriter().print(response);
+         }
+     }
+ 
+-    public class OpenIdConfigServlet extends HttpServlet
++    private class EndSessionEndpoint extends HttpServlet
+     {
+         @Override
+-        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
++        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
++        {
++            doPost(req, resp);
++        }
++
++        @Override
++        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
++        {
++            String idToken = req.getParameter("id_token_hint");
++            if (idToken == null)
++            {
++                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
++                return;
++            }
++
++            String logoutRedirect = req.getParameter("post_logout_redirect_uri");
++            if (logoutRedirect == null)
++            {
++                resp.setStatus(HttpServletResponse.SC_OK);
++                resp.getWriter().println("logout success on end_session_endpoint");
++                return;
++            }
++
++            loggedInUsers.decrement();
++            resp.setContentType("text/plain");
++            resp.sendRedirect(logoutRedirect);
++        }
++    }
++
++    private class ConfigServlet extends HttpServlet
++    {
++        @Override
++        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
+         {
+             String discoveryDocument = "{" +
+                 "\"issuer\": \"" + provider + "\"," +
+                 "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
+                 "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
++                "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
+                 "}";
+ 
+             resp.getWriter().write(discoveryDocument);
+         }
+     }
+ 
+-    public class User
++    public static class User
+     {
+-        private long subject;
+-        private String name;
++        private final String subject;
++        private final String name;
+ 
+         public User(String name)
+         {
+-            this(new Random().nextLong(), name);
++            this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
+         }
+ 
+-        public User(long subject, String name)
++        public User(String subject, String name)
+         {
+             this.subject = subject;
+             this.name = name;
+@@ -222,10 +385,29 @@ public class OpenIdProvider extends ContainerLifeCycle
+             return name;
+         }
+ 
+-        public String getIdToken()
++        public String getSubject()
++        {
++            return subject;
++        }
++
++        public String getIdToken(String provider, String clientId, long duration)
++        {
++            long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
++            return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
++        }
++
++        @Override
++        public boolean equals(Object obj)
++        {
++            if (!(obj instanceof User))
++                return false;
++            return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
++        }
++
++        @Override
++        public int hashCode()
+         {
+-            long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
+-            return JwtEncoder.createIdToken(provider, clientId, Long.toString(subject), name, expiry);
++            return Objects.hash(subject, name);
+         }
+     }
+ }


=====================================
debian/patches/series
=====================================
@@ -10,3 +10,8 @@ CVE-2021-34428.patch
 CVE-2021-34429.patch
 CVE-2022-2047.patch
 CVE-2022-2048.patch
+CVE-2023-26048.patch
+CVE-2023-26049.patch
+CVE-2023-36479.patch
+CVE-2023-40167.patch
+CVE-2023-41900.patch



View it on GitLab: https://salsa.debian.org/java-team/jetty9/-/commit/8648cca8e0b426be8e0381e8e1204dd93cfbb52a

-- 
View it on GitLab: https://salsa.debian.org/java-team/jetty9/-/commit/8648cca8e0b426be8e0381e8e1204dd93cfbb52a
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20230930/4d789126/attachment.htm>


More information about the pkg-java-commits mailing list