[Git][java-team/jetty9][master] 2 commits: Fix CVE-2021-34429
Markus Koschany (@apo)
gitlab at salsa.debian.org
Sun Jul 18 18:57:46 BST 2021
Markus Koschany pushed to branch master at Debian Java Maintainers / jetty9
Commits:
bc08e0c1 by Markus Koschany at 2021-07-18T19:37:53+02:00
Fix CVE-2021-34429
Closes: #991188
Thanks: Salvatore Bonaccorso for the report.
- - - - -
dc520b43 by Markus Koschany at 2021-07-18T19:38:26+02:00
Update changelog
- - - - -
3 changed files:
- debian/changelog
- + debian/patches/CVE-2021-34429.patch
- debian/patches/series
Changes:
=====================================
debian/changelog
=====================================
@@ -1,3 +1,13 @@
+jetty9 (9.4.39-3) unstable; urgency=high
+
+ * Team upload.
+ * Fix CVE-2021-34429:
+ URIs can be crafted using some encoded characters to access the content of
+ the WEB-INF directory and/or bypass some security constraints.
+ Thanks to Salvatore Bonaccorso for the report. (Closes: #991188)
+
+ -- Markus Koschany <apo at debian.org> Sun, 18 Jul 2021 19:37:57 +0200
+
jetty9 (9.4.39-2) unstable; urgency=high
* Team upload.
=====================================
debian/patches/CVE-2021-34429.patch
=====================================
@@ -0,0 +1,1147 @@
+From: Markus Koschany <apo at debian.org>
+Date: Sun, 18 Jul 2021 18:58:17 +0200
+Subject: CVE-2021-34429
+
+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 ++++++++-----
+ .../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 ++
+ .../org/eclipse/jetty/servlet/RequestURITest.java | 42 +++-
+ .../main/java/org/eclipse/jetty/util/URIUtil.java | 266 +++++++++++----------
+ .../eclipse/jetty/util/resource/FileResource.java | 7 +-
+ .../eclipse/jetty/util/resource/PathResource.java | 19 +-
+ .../org/eclipse/jetty/util/resource/Resource.java | 6 +-
+ .../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(-)
+
+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
+--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
++++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
+@@ -617,10 +617,12 @@ public class HttpURI
+ }
+ else if (_path != null)
+ {
+- String canonical = URIUtil.canonicalPath(_path);
+- if (canonical == null)
+- throw new BadMessageException("Bad URI");
+- _decodedPath = URIUtil.decodePath(canonical);
++ // The RFC requires this to be canonical before decoding, but this can leave dot segments and dot dot segments
++ // which are not canonicalized and could be used in an attempt to bypass security checks.
++ String decodedNonCanonical = URIUtil.decodePath(_path);
++ _decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
++ if (_decodedPath == null)
++ throw new IllegalArgumentException("Bad URI");
+ }
+ }
+
+@@ -670,7 +672,8 @@ public class HttpURI
+ }
+
+ /**
+- * @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousSeparator()}.
++ * @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousEmptySegment()}
++ * or {@link #hasAmbiguousSeparator()} or {@link #hasAmbiguousParameter()}
+ */
+ 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
+--- 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
+ assertEquals("", uri.toString());
+
+ uri.setPath("/path/info");
+- assertEquals("/path/info", uri.toString());
++ assertEquals("/path/info", uri.toString());
+
+- uri.setAuthority("host", 8080);
+- assertEquals("//host:8080/path/info", uri.toString());
++ uri.setAuthority("host", 8080);
++ assertEquals("//host:8080/path/info", uri.toString());
+
+- uri.setParam("param");
+- assertEquals("//host:8080/path/info;param", uri.toString());
++ uri.setParam("param");
++ assertEquals("//host:8080/path/info;param", uri.toString());
+
+- uri.setQuery("a=b");
+- assertEquals("//host:8080/path/info;param?a=b", uri.toString());
++ uri.setQuery("a=b");
++ assertEquals("//host:8080/path/info;param?a=b", uri.toString());
+
+- uri.setScheme("http");
+- assertEquals("http://host:8080/path/info;param?a=b", uri.toString());
++ uri.setScheme("http");
++ assertEquals("http://host:8080/path/info;param?a=b", uri.toString());
+
+- uri.setPathQuery("/other;xxx/path;ppp?query");
+- assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
++ uri.setPathQuery("/other;xxx/path;ppp?query");
++ assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
+
+- assertThat(uri.getScheme(), is("http"));
+- assertThat(uri.getAuthority(), is("host:8080"));
+- assertThat(uri.getHost(), is("host"));
+- assertThat(uri.getPort(), is(8080));
+- assertThat(uri.getPath(), is("/other;xxx/path;ppp"));
+- assertThat(uri.getDecodedPath(), is("/other/path"));
+- assertThat(uri.getParam(), is("ppp"));
+- assertThat(uri.getQuery(), is("query"));
+- assertThat(uri.getPathQuery(), is("/other;xxx/path;ppp?query"));
++ assertThat(uri.getScheme(), is("http"));
++ assertThat(uri.getAuthority(), is("host:8080"));
++ assertThat(uri.getHost(), is("host"));
++ assertThat(uri.getPort(), is(8080));
++ assertThat(uri.getPath(), is("/other;xxx/path;ppp"));
++ assertThat(uri.getDecodedPath(), is("/other/path"));
++ assertThat(uri.getParam(), is("ppp"));
++ assertThat(uri.getQuery(), is("query"));
++ assertThat(uri.getPathQuery(), is("/other;xxx/path;ppp?query"));
+
+- uri.setPathQuery(null);
+- assertEquals("http://host:8080?query", uri.toString()); // Yes silly result!
++ uri.setPathQuery(null);
++ assertEquals("http://host:8080?query", uri.toString()); // Yes silly result!
+
+- uri.setQuery(null);
+- assertEquals("http://host:8080", uri.toString());
++ uri.setQuery(null);
++ assertEquals("http://host:8080", uri.toString());
+
+- uri.setPathQuery("/other;xxx/path;ppp?query");
+- assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
++ uri.setPathQuery("/other;xxx/path;ppp?query");
++ assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
+
+- uri.setScheme(null);
+- assertEquals("//host:8080/other;xxx/path;ppp?query", uri.toString());
++ uri.setScheme(null);
++ assertEquals("//host:8080/other;xxx/path;ppp?query", uri.toString());
+
+- uri.setAuthority(null, -1);
+- assertEquals("/other;xxx/path;ppp?query", uri.toString());
++ uri.setAuthority(null, -1);
++ assertEquals("/other;xxx/path;ppp?query", uri.toString());
+
+- uri.setParam(null);
+- assertEquals("/other;xxx/path?query", uri.toString());
++ uri.setParam(null);
++ assertEquals("/other;xxx/path?query", uri.toString());
+
+- uri.setQuery(null);
+- assertEquals("/other;xxx/path", uri.toString());
++ uri.setQuery(null);
++ assertEquals("/other;xxx/path", uri.toString());
+
+- uri.setPath(null);
+- assertEquals("", uri.toString());
+- }
++ uri.setPath(null);
++ assertEquals("", uri.toString());
++}
+
+- public static Stream<Arguments> decodePathTests()
+- {
+- return Arrays.stream(new Object[][]
+- {
+- // Simple path example
+- {"http://host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+- {"//host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+- {"/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+-
+- // legal non ambiguous relative paths
+- {"http://host/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
+- {"http://host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
+- {"http://host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+- {"//host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
+- {"//host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+- {"/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
+- {"/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
+- {"path/../info", "info", EnumSet.noneOf(Ambiguous.class)},
+- {"path/./info", "path/info", EnumSet.noneOf(Ambiguous.class)},
++public static Stream<Arguments> decodePathTests()
++{
++ return Arrays.stream(new Object[][]
++ {
++ // Simple path example
++ {"http://host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
++ {"//host/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
++ {"/path/info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
++
++ // legal non ambiguous relative paths
++ {"http://host/../path/info", null, EnumSet.noneOf(Ambiguous.class)},
++ {"http://host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
++ {"http://host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
++ {"//host/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
++ {"//host/path/./info", "/path/info", EnumSet.noneOf(Ambiguous.class)},
++ {"/path/../info", "/info", EnumSet.noneOf(Ambiguous.class)},
++ {"/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
+ assertThat(decodedPath, nullValue());
+ }
+ }
++
++ public static Stream<Arguments> queryData()
++ {
++ return Stream.of(
++ new String[]{"/path?p=%U20AC", "p=%U20AC"},
++ new String[]{"/path?p=%u20AC", "p=%u20AC"}
++ ).map(Arguments::of);
++ }
++
++ @ParameterizedTest
++ @MethodSource("queryData")
++ public void testEncodedQuery(String input, String expectedQuery)
++ {
++ HttpURI httpURI = new HttpURI(input);
++ assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
++ }
+ }
+diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
+index 6fc476d..1040018 100644
+--- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
++++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
+@@ -53,12 +53,12 @@ public final class RedirectUtil
+ String path = request.getRequestURI();
+ String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
+ location = URIUtil.canonicalPath(URIUtil.addEncodedPaths(parent, location));
+- if (!location.startsWith("/"))
++ if (location != null && !location.startsWith("/"))
+ url.append('/');
+ }
+
+ if (location == null)
+- throw new IllegalStateException("path cannot be above root");
++ throw new IllegalStateException("redirect path cannot be above root");
+ url.append(location);
+
+ location = url.toString();
+diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
+index fae7150..bf63fe7 100644
+--- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
++++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
+@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertFalse;
++import static org.junit.jupiter.api.Assertions.assertThrows;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
+
+ @SuppressWarnings("unused")
+@@ -75,6 +76,12 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
+
+ @Test
+ public void testInvalidJsp() throws Exception
++ {
++ assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/bean1.jsp%00"));
++ }
++
++ @Test
++ public void testInvalidJspWithNullByte() throws Exception
+ {
+ _rule.setCode("405");
+ _rule.setReason("foo");
+@@ -113,6 +120,12 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
+ assertEquals(200, _response.getStatus());
+ }
+
++ @Test
++ public void testInvalidShamrock() throws Exception
++ {
++ assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/shamrock-%00%E2%98%98.jsp"));
++ }
++
+ @Test
+ public void testCharacters() throws Exception
+ {
+@@ -124,4 +137,3 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
+ //@checkstyle-enable-check : IllegalTokenText
+ }
+ }
+-
+diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
+index 4e16095..48bebcb 100644
+--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
++++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
+@@ -1948,7 +1948,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+
+ try
+ {
+- path = URIUtil.canonicalPath(path);
+ Resource resource = _baseResource.addPath(path);
+
+ if (checkAlias(path, resource))
+@@ -2135,9 +2134,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return ContextHandler.this;
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getContext(java.lang.String)
+- */
+ @Override
+ public ServletContext getContext(String uripath)
+ {
+@@ -2226,9 +2222,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return null;
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getMimeType(java.lang.String)
+- */
+ @Override
+ public String getMimeType(String file)
+ {
+@@ -2237,9 +2230,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return _mimeTypes.getMimeByExtension(file);
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getRequestDispatcher(java.lang.String)
+- */
+ @Override
+ public RequestDispatcher getRequestDispatcher(String uriInContext)
+ {
+@@ -2253,6 +2243,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+
+ try
+ {
++ // The uriInContext will be canonicalized by HttpURI.
+ HttpURI uri = new HttpURI(null, null, 0, uriInContext);
+
+ String pathInfo = URIUtil.canonicalPath(uri.getDecodedPath());
+@@ -2278,6 +2269,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ @Override
+ public String getRealPath(String path)
+ {
++ // This is an API call from the application which may have arbitrary non canonical paths passed
++ // Thus we canonicalize here, to avoid the enforcement of only canonical paths in
++ // ContextHandler.this.getResource(path).
++ path = URIUtil.canonicalPath(path);
+ if (path == null)
+ return null;
+ if (path.length() == 0)
+@@ -2312,9 +2307,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return null;
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getResourceAsStream(java.lang.String)
+- */
+ @Override
+ public InputStream getResourceAsStream(String path)
+ {
+@@ -2402,9 +2394,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return o;
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getAttributeNames()
+- */
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+@@ -2423,9 +2412,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ return Collections.enumeration(set);
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#setAttribute(java.lang.String, java.lang.Object)
+- */
+ @Override
+ public void setAttribute(String name, Object value)
+ {
+@@ -2452,9 +2438,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ }
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#removeAttribute(java.lang.String)
+- */
+ @Override
+ public void removeAttribute(String name)
+ {
+@@ -2471,9 +2454,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
+ }
+ }
+
+- /*
+- * @see javax.servlet.ServletContext#getServletContextName()
+- */
+ @Override
+ public String getServletContextName()
+ {
+diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
+index b5b0db0..63d94ff 100644
+--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
++++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
+@@ -188,7 +188,9 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
+ }
+ }
+ else if (_context != null)
++ {
+ r = _context.getResource(path);
++ }
+
+ if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
+ r = getStylesheet();
+diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
+index 72197e6..ada19a0 100644
+--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
++++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
+@@ -829,6 +829,12 @@ public class HttpConnectionTest
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 200"); //now fallback to iso-8859-1
+
++ response = connector.getResponse("GET /foo/bar%c0%00 HTTP/1.1\r\n" +
++ "Host: localhost\r\n" +
++ "Connection: close\r\n" +
++ "\r\n");
++ checkContains(response, 0, "HTTP/1.1 400");
++
+ response = connector.getResponse("GET /bad/utf8%c1 HTTP/1.1\r\n" +
+ "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
+--- 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
+ assertNull(url);
+ }
+
++ @Test
++ public void testAlias() throws Exception
++ {
++ String path = "/./index.html";
++ Resource resource = context.getResource(path);
++ assertNull(resource);
++ URL resourceURL = context.getServletContext().getResource(path);
++ assertFalse(resourceURL.getPath().contains("/./"));
++
++ path = "/down/../index.html";
++ resource = context.getResource(path);
++ assertNull(resource);
++ resourceURL = context.getServletContext().getResource(path);
++ assertFalse(resourceURL.getPath().contains("/../"));
++
++ path = "//index.html";
++ resource = context.getResource(path);
++ assertNull(resource);
++ resourceURL = context.getServletContext().getResource(path);
++ assertNull(resourceURL);
++
+ @Test
+ public void testDeep() throws Exception
+ {
+diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
+index de3814a..2b3e29a 100644
+--- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
++++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
+@@ -196,7 +196,6 @@ public class RequestURITest
+ // Read the response.
+ String response = readResponse(client);
+
+- // TODO: is HTTP/1.1 response appropriate for an HTTP/1.0 request?
+ assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString("RequestURI: " + expectedReqUri));
+ assertThat(response, Matchers.containsString("QueryString: " + expectedQuery));
+@@ -223,4 +222,45 @@ public class RequestURITest
+ assertThat(response, Matchers.containsString("QueryString: " + expectedQuery));
+ }
+ }
++
++ public static Stream<Arguments> badData()
++ {
++ List<Arguments> ret = new ArrayList<>();
++ ret.add(Arguments.of("/hello\000"));
++ ret.add(Arguments.of("/hello%00"));
++ ret.add(Arguments.of("/hello%u0000"));
++ ret.add(Arguments.of("/hello\000/world"));
++ ret.add(Arguments.of("/hello%00world"));
++ ret.add(Arguments.of("/hello%u0000world"));
++ ret.add(Arguments.of("/hello%GG"));
++ ret.add(Arguments.of("/hello%;/world"));
++ ret.add(Arguments.of("/hello/../../world"));
++ ret.add(Arguments.of("/hello/..;/world"));
++ ret.add(Arguments.of("/hello/..;?/world"));
++ ret.add(Arguments.of("/hello/%#x/../world"));
++ ret.add(Arguments.of("/../hello/world"));
++ ret.add(Arguments.of("/hello%u00u00/world"));
++ ret.add(Arguments.of("hello"));
++
++ return ret.stream();
++ }
++
++ @ParameterizedTest
++ @MethodSource("badData")
++ public void testGetBadRequestsURIHTTP10(String rawpath) throws Exception
++ {
++ try (Socket client = newSocket(serverURI.getHost(), serverURI.getPort()))
++ {
++ OutputStream os = client.getOutputStream();
++
++ String request = String.format("GET %s HTTP/1.0\r\n\r\n", rawpath);
++ os.write(request.getBytes(StandardCharsets.ISO_8859_1));
++ os.flush();
++
++ // Read the response.
++ String response = readResponse(client);
++
++ assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
++ }
++ }
+ }
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
+index 6818e54..7cf5862 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
+@@ -475,8 +475,8 @@ public class URIUtil
+ char u = path.charAt(i + 1);
+ if (u == 'u')
+ {
+- // TODO remove %u support in jetty-10
+- // this is wrong. This is a codepoint not a char
++ // In Jetty-10 UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
++ // This is wrong. This is a codepoint not a char
+ builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
+ i += 5;
+ }
+@@ -537,7 +537,6 @@ public class URIUtil
+ {
+ throw new IllegalArgumentException("cannot decode URI", e);
+ }
+-
+ }
+
+ /* Decode a URI path and strip parameters of ISO-8859-1 path
+@@ -562,8 +561,7 @@ public class URIUtil
+ char u = path.charAt(i + 1);
+ if (u == 'u')
+ {
+- // TODO remove %u encoding support in jetty-10
+- // This is wrong. This is a codepoint not a char
++ // In Jetty-10 UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS. // This is wrong. This is a codepoint not a char
+ builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
+ i += 5;
+ }
+@@ -782,143 +780,139 @@ public class URIUtil
+ }
+
+ /**
+- * Convert an encoded path to a canonical form.
++ * Convert a partial URI to a canonical form.
+ * <p>
+- * All instances of "." and ".." are factored out.
++ * All segments of "." and ".." are factored out.
+ * Null is returned if the path tries to .. above its root.
+ * </p>
+ *
+- * @param path the path to convert, decoded, with path separators '/' and no queries.
++ * @param uri the encoded URI from the path onwards, which may contain query strings and/or fragments
+ * @return the canonical path, or null if path traversal above root.
++ * @see #canonicalPath(String)
++ * @see #canonicalURI(String)
+ */
+- public static String canonicalPath(String path)
++ public static String canonicalURI(String uri)
+ {
+- // See https://tools.ietf.org/html/rfc3986#section-5.2.4
+-
+- if (path == null || path.isEmpty())
+- return path;
++ if (uri == null || uri.isEmpty())
++ return uri;
+
+- int end = path.length();
++ boolean slash = true;
++ int end = uri.length();
+ int i = 0;
+- int dots = 0;
+
++ // Initially just loop looking if we may need to normalize
+ loop: while (i < end)
+ {
+- char c = path.charAt(i);
++ char c = uri.charAt(i);
+ switch (c)
+ {
+ case '/':
+- dots = 0;
++ slash = true;
+ break;
+
+ case '.':
+- if (dots == 0)
+- {
+- dots = 1;
++ if (slash)
+ break loop;
+- }
+- dots = -1;
++ slash = false;
+ break;
+
++ case '?':
++ case '#':
++ // Nothing to normalize so return original path
++ return uri;
++
+ default:
+- dots = -1;
++ slash = false;
+ }
+
+ i++;
+ }
+
++ // Nothing to normalize so return original path
+ if (i == end)
+- return path;
++ return uri;
+
+- StringBuilder canonical = new StringBuilder(path.length());
+- canonical.append(path, 0, i);
++ // We probably need to normalize, so copy to path so far into builder
++ StringBuilder canonical = new StringBuilder(uri.length());
++ canonical.append(uri, 0, i);
+
++ // Loop looking for single and double dot segments
++ int dots = 1;
+ i++;
+- while (i <= end)
++ loop : while (i < end)
+ {
+- char c = i < end ? path.charAt(i) : '\0';
++ char c = uri.charAt(i);
+ switch (c)
+ {
+- case '\0':
+- if (dots == 2)
+- {
+- if (canonical.length() < 2)
+- return null;
+- canonical.setLength(canonical.length() - 1);
+- canonical.setLength(canonical.lastIndexOf("/") + 1);
+- }
+- break;
+-
+ case '/':
+- switch (dots)
+- {
+- case 1:
+- break;
+-
+- case 2:
+- if (canonical.length() < 2)
+- return null;
+- canonical.setLength(canonical.length() - 1);
+- canonical.setLength(canonical.lastIndexOf("/") + 1);
+- break;
+-
+- default:
+- canonical.append(c);
+- }
++ if (doDotsSlash(canonical, dots))
++ return null;
++ slash = true;
+ dots = 0;
+ break;
+
++ case '?':
++ case '#':
++ // finish normalization at a query
++ break loop;
++
+ case '.':
+- switch (dots)
+- {
+- case 0:
+- dots = 1;
+- break;
+- case 1:
+- dots = 2;
+- break;
+- case 2:
+- canonical.append("...");
+- dots = -1;
+- break;
+- default:
+- canonical.append('.');
+- }
++ // Count dots only if they are leading in the segment
++ if (dots > 0)
++ dots++;
++ else if (slash)
++ dots = 1;
++ else
++ canonical.append('.');
++ slash = false;
+ break;
+
+ default:
+- switch (dots)
+- {
+- case 1:
+- canonical.append('.');
+- break;
+- case 2:
+- canonical.append("..");
+- break;
+- default:
+- }
++ // Add leading dots to the path
++ while (dots-- > 0)
++ canonical.append('.');
+ canonical.append(c);
+- dots = -1;
++ dots = 0;
++ slash = false;
+ }
+-
+ i++;
+ }
++
++ // process any remaining dots
++ if (doDots(canonical, dots))
++ return null;
++
++ // append any query
++ if (i < end)
++ canonical.append(uri, i, end);
++
+ return canonical.toString();
+ }
+
+ /**
+- * Convert a path to a cananonical form.
+- * <p>
+- * All instances of "." and ".." are factored out.
+- * </p>
++ * @param path the encoded URI from the path onwards, which may contain query strings and/or fragments
++ * @return the canonical path, or null if path traversal above root.
++ * @deprecated Use {@link #canonicalURI(String)}
++ */
++ @Deprecated
++ public static String canonicalEncodedPath(String path)
++ {
++ return canonicalURI(path);
++ }
++
++ /**
++ * Convert a decoded URI path to a canonical form.
+ * <p>
++ * All segments of "." and ".." are factored out.
+ * Null is returned if the path tries to .. above its root.
+ * </p>
+ *
+- * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/')
++ * @param path the decoded URI path to convert. Any special characters (e.g. '?', "#") are assumed to be part of
++ * the path segments.
+ * @return the canonical path, or null if path traversal above root.
++ * @see #canonicalURI(String)
+ */
+- public static String canonicalEncodedPath(String path)
++ public static String canonicalPath(String path)
+ {
+ if (path == null || path.isEmpty())
+ return path;
+@@ -927,8 +921,8 @@ public class URIUtil
+ int end = path.length();
+ int i = 0;
+
+- loop:
+- while (i < end)
++ // Initially just loop looking if we may need to normalize
++ loop: while (i < end)
+ {
+ char c = path.charAt(i);
+ switch (c)
+@@ -943,9 +937,6 @@ public class URIUtil
+ slash = false;
+ break;
+
+- case '?':
+- return path;
+-
+ default:
+ slash = false;
+ }
+@@ -953,56 +944,31 @@ public class URIUtil
+ i++;
+ }
+
++ // Nothing to normalize so return original path
+ if (i == end)
+ return path;
+
++ // We probably need to normalize, so copy to path so far into builder
+ StringBuilder canonical = new StringBuilder(path.length());
+ canonical.append(path, 0, i);
+
++ // Loop looking for single and double dot segments
+ int dots = 1;
+ i++;
+- while (i <= end)
++ while (i < end)
+ {
+- char c = i < end ? path.charAt(i) : '\0';
++ char c = path.charAt(i);
+ switch (c)
+ {
+- case '\0':
+ case '/':
+- case '?':
+- switch (dots)
+- {
+- case 0:
+- if (c != '\0')
+- canonical.append(c);
+- break;
+-
+- case 1:
+- if (c == '?')
+- canonical.append(c);
+- break;
+-
+- case 2:
+- if (canonical.length() < 2)
+- return null;
+- canonical.setLength(canonical.length() - 1);
+- canonical.setLength(canonical.lastIndexOf("/") + 1);
+- if (c == '?')
+- canonical.append(c);
+- break;
+- default:
+- while (dots-- > 0)
+- {
+- canonical.append('.');
+- }
+- if (c != '\0')
+- canonical.append(c);
+- }
+-
++ if (doDotsSlash(canonical, dots))
++ return null;
+ slash = true;
+ dots = 0;
+ break;
+
+ case '.':
++ // Count dots only if they are leading in the segment
+ if (dots > 0)
+ dots++;
+ else if (slash)
+@@ -1013,20 +979,66 @@ public class URIUtil
+ break;
+
+ default:
++ // Add leading dots to the path
+ while (dots-- > 0)
+- {
+ canonical.append('.');
+- }
+ canonical.append(c);
+ dots = 0;
+ slash = false;
+ }
+-
+ i++;
+ }
++
++ // process any remaining dots
++ if (doDots(canonical, dots))
++ return null;
++
+ return canonical.toString();
+ }
+
++ private static boolean doDots(StringBuilder canonical, int dots)
++ {
++ switch (dots)
++ {
++ case 0:
++ case 1:
++ break;
++ case 2:
++ if (canonical.length() < 2)
++ return true;
++ canonical.setLength(canonical.length() - 1);
++ canonical.setLength(canonical.lastIndexOf("/") + 1);
++ break;
++ default:
++ while (dots-- > 0)
++ canonical.append('.');
++ }
++ return false;
++ }
++
++ private static boolean doDotsSlash(StringBuilder canonical, int dots)
++ {
++ switch (dots)
++ {
++ case 0:
++ canonical.append('/');
++ break;
++ case 1:
++ break;
++ case 2:
++ if (canonical.length() < 2)
++ return true;
++ canonical.setLength(canonical.length() - 1);
++ canonical.setLength(canonical.lastIndexOf("/") + 1);
++ break;
++ default:
++ while (dots-- > 0)
++ canonical.append('.');
++ canonical.append('/');
++ }
++ return false;
++ }
++
+ /**
+ * Convert a path to a compact form.
+ * All instances of "//" and "///" etc. are factored out to single "/"
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
+index 9ccfe96..d62b117 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
+@@ -271,8 +271,11 @@ public class FileResource extends Resource
+ assertValidPath(path);
+ path = org.eclipse.jetty.util.URIUtil.canonicalPath(path);
+
+- if (path == null)
+- throw new MalformedURLException();
++ // Check that the path is within the root,
++ // but use the original path to create the
++ // resource, to preserve aliasing.
++ if (URIUtil.canonicalPath(path) == null)
++ throw new MalformedURLException(path);
+
+ if ("/".equals(path))
+ return this;
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
+index af377b1..67a31b3 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
+@@ -97,6 +97,13 @@ public class PathResource extends Resource
+ abs = path.toAbsolutePath();
+ }
+
++ // Any normalization difference means it's an alias,
++ // and we don't want to bother further to follow
++ // symlinks as it's an alias anyway.
++ Path normal = path.normalize();
++ if (!isSameName(abs, normal))
++ return normal;
++
+ try
+ {
+ if (Files.isSymbolicLink(path))
+@@ -104,11 +111,8 @@ public class PathResource extends Resource
+ if (Files.exists(path))
+ {
+ Path real = abs.toRealPath(FOLLOW_LINKS);
+-
+ if (!isSameName(abs, real))
+- {
+ return real;
+- }
+ }
+ }
+ catch (IOException e)
+@@ -363,12 +367,13 @@ public class PathResource extends Resource
+ @Override
+ public Resource addPath(final String subpath) throws IOException
+ {
+- String cpath = URIUtil.canonicalPath(subpath);
+-
+- if ((cpath == null) || (cpath.length() == 0))
++ // Check that the path is within the root,
++ // but use the original path to create the
++ // resource, to preserve aliasing.
++ if (URIUtil.canonicalPath(subpath) == null)
+ throw new MalformedURLException(subpath);
+
+- if ("/".equals(cpath))
++ if ("/".equals(subpath))
+ return this;
+
+ // subpaths are always under PathResource
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
+index 4574436..3448e94 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
+@@ -459,10 +459,12 @@ public abstract class Resource implements ResourceFactory, Closeable
+ * Returns the resource contained inside the current resource with the
+ * given name.
+ *
+- * @param path The path segment to add, which is not encoded
++ * @param path The path segment to add, which is not encoded. The path may be non canonical, but if so then
++ * the resulting Resource will return true from {@link #isAlias()}.
+ * @return the Resource for the resolved path within this Resource.
+ * @throws IOException if unable to resolve the path
+- * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
++ * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed, or
++ * a relative path attempts to access above the root resource.
+ */
+ public abstract Resource addPath(String path)
+ throws IOException, MalformedURLException;
+diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
+index ffd9c1c..40d68ab 100644
+--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
++++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
+@@ -271,10 +271,11 @@ public class URLResource extends Resource
+ public Resource addPath(String path)
+ throws IOException
+ {
+- if (path == null)
+- return null;
+-
+- path = URIUtil.canonicalPath(path);
++ // Check that the path is within the root,
++ // but use the original path to create the
++ // resource, to preserve aliasing.
++ if (URIUtil.canonicalPath(path) == null)
++ throw new MalformedURLException(path);
+
+ return newResource(URIUtil.addEncodedPaths(_url.toExternalForm(), URIUtil.encodePath(path)), _useCaches);
+ }
+diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
+index 82f2771..a272926 100644
+--- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
++++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
+@@ -149,4 +149,24 @@ public class URIUtilCanonicalPathTest
+ {
+ assertThat(URIUtil.canonicalPath(input), is(expectedResult));
+ }
++
++ public static Stream<Arguments> queries()
++ {
++ String[][] data =
++ {
++ {"/ctx/../dir?/../index.html", "/dir?/../index.html"},
++ {"/get-files?file=/etc/passwd", "/get-files?file=/etc/passwd"},
++ {"/get-files?file=../../../../../passwd", "/get-files?file=../../../../../passwd"}
++ };
++ return Stream.of(data).map(Arguments::of);
++ }
++
++ @ParameterizedTest
++ @MethodSource("queries")
++ public void testQuery(String input, String expectedPath)
++ {
++ String actual = URIUtil.canonicalURI(input);
++ assertThat(actual, is(expectedPath));
++ }
++
+ }
+diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
+index c93dc73..5d7c7b2 100644
+--- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
++++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
+@@ -21,6 +21,7 @@ package org.eclipse.jetty.util.resource;
+ import java.io.File;
+ import java.io.IOException;
+ import java.io.InputStream;
++import java.net.MalformedURLException;
+ import java.net.URI;
+ import java.net.URL;
+ import java.nio.file.Path;
+@@ -45,6 +46,8 @@ import static org.hamcrest.Matchers.equalTo;
+ import static org.hamcrest.Matchers.startsWith;
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
++import static org.junit.jupiter.api.Assertions.assertThrows;
++import static org.junit.jupiter.api.Assertions.assertTrue;
+
+ public class ResourceTest
+ {
+@@ -325,4 +328,19 @@ public class ResourceTest
+
+ assertEquals(rb, ra);
+ }
++
++ @Test
++ public void testClimbAboveBase() throws Exception
++ {
++ Resource resource = Resource.newResource("/foo/bar");
++ assertThrows(MalformedURLException.class, () -> resource.addPath(".."));
++
++ Resource same = resource.addPath(".");
++ assertNotNull(same);
++ assertTrue(same.isAlias());
++
++ assertThrows(MalformedURLException.class, () -> resource.addPath("./.."));
++
++ assertThrows(MalformedURLException.class, () -> resource.addPath("./../bar"));
++ }
+ }
=====================================
debian/patches/series
=====================================
@@ -7,3 +7,4 @@
09-tweak-distribution.patch
CVE-2021-28169.patch
CVE-2021-34428.patch
+CVE-2021-34429.patch
View it on GitLab: https://salsa.debian.org/java-team/jetty9/-/compare/6c7cc80fcd072e4bb28d4e8ed441d625ea0ecc5c...dc520b433e7761c30ef5defb93ae89ea3463a88f
--
View it on GitLab: https://salsa.debian.org/java-team/jetty9/-/compare/6c7cc80fcd072e4bb28d4e8ed441d625ea0ecc5c...dc520b433e7761c30ef5defb93ae89ea3463a88f
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/20210718/c74afff6/attachment.htm>
More information about the pkg-java-commits
mailing list