From 57cd9208c4b8a523934024654bcd40238a1857dc Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 12:35:24 -0700 Subject: [PATCH 1/7] adding `Valve` workaround for GHSA-95v2-fvxr-qg83 with implementation suggestion for metacat<3.5.0 --- .../dataone/security/TemporaryMitigationValve | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/java/org/dataone/security/TemporaryMitigationValve diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve b/src/main/java/org/dataone/security/TemporaryMitigationValve new file mode 100644 index 0000000..ca9d5c9 --- /dev/null +++ b/src/main/java/org/dataone/security/TemporaryMitigationValve @@ -0,0 +1,74 @@ +package org.dataone.security; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +import javax.servlet.ServletException; + +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; + +/** + * Temporary mitigation Valve to handle GHSA-95v2-fvxr-qg83-style path confusion/bypass attempts. + * Added 2026-07-10. + * Enable via the element under in server.xml, e.g.: + * + */ +public class TemporaryMitigationValve extends ValveBase { + + private static final int MAX_DECODE_ROUNDS = 3; + + // deny patterns + private static final Pattern SUSPICIOUS = Pattern.compile( + "(?i)(\\.{2}|%2e|%2f|%5c|\\\\|/\\./|/\\.\\./|%252e|%252f|%255c)" + ); + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + String uri = safe(request.getRequestURI()); + String qs = request.getQueryString(); + String target = (qs == null) ? uri : (uri + "?" + qs); + + if (isSuspicious(target)) { + response.sendError(400, "Malformed request target"); + return; + } + + Valve next = getNext(); + if (next != null) { + next.invoke(request, response); + } + } + + private boolean isSuspicious(String input) { + String current = input; + for (int i = 0; i < MAX_DECODE_ROUNDS; i++) { + if (SUSPICIOUS.matcher(current).find()) { + return true; + } + String decoded = decodeOnce(current); + if (decoded.equals(current)) { + break; + } + current = decoded; + } + return SUSPICIOUS.matcher(current).find(); + } + + private String decodeOnce(String value) { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + // Invalid %-encoding => treat as suspicious + return "%BAD_ENCODING%"; + } + } + + private String safe(String v) { + return v == null ? "" : v; + } +} \ No newline at end of file From 896e35966a0050c0417642cafeba985f40900b80 Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 12:58:01 -0700 Subject: [PATCH 2/7] correcting filename --- .../{TemporaryMitigationValve => TemporaryMitigationValve.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/org/dataone/security/{TemporaryMitigationValve => TemporaryMitigationValve.java} (100%) diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve b/src/main/java/org/dataone/security/TemporaryMitigationValve.java similarity index 100% rename from src/main/java/org/dataone/security/TemporaryMitigationValve rename to src/main/java/org/dataone/security/TemporaryMitigationValve.java From c88caa382acaa3951d6939b964b1ceebcc5ec3c4 Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 13:00:12 -0700 Subject: [PATCH 3/7] adding license statement --- .../security/TemporaryMitigationValve.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve.java b/src/main/java/org/dataone/security/TemporaryMitigationValve.java index ca9d5c9..cb1adde 100644 --- a/src/main/java/org/dataone/security/TemporaryMitigationValve.java +++ b/src/main/java/org/dataone/security/TemporaryMitigationValve.java @@ -1,3 +1,25 @@ +/** + * This work was created by participants in the DataONE project, and is + * jointly copyrighted by participating institutions in DataONE. For + * more information on DataONE, see our web site at http://dataone.org. + * + * Copyright 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $Id$ + */ + package org.dataone.security; import java.io.IOException; From 347f2d739c734c1157c4dcd1babaf0bc48713335 Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 13:08:23 -0700 Subject: [PATCH 4/7] addressing suggestion not to return a magic string but instead return `null` when an illegal sequence is found --- .../org/dataone/security/TemporaryMitigationValve.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve.java b/src/main/java/org/dataone/security/TemporaryMitigationValve.java index cb1adde..dfa4f74 100644 --- a/src/main/java/org/dataone/security/TemporaryMitigationValve.java +++ b/src/main/java/org/dataone/security/TemporaryMitigationValve.java @@ -73,6 +73,9 @@ private boolean isSuspicious(String input) { return true; } String decoded = decodeOnce(current); + if (decoded == null) { + return true; + } if (decoded.equals(current)) { break; } @@ -85,8 +88,8 @@ private String decodeOnce(String value) { try { return URLDecoder.decode(value, StandardCharsets.UTF_8); } catch (IllegalArgumentException e) { - // Invalid %-encoding => treat as suspicious - return "%BAD_ENCODING%"; + // Invalid %-encoding: signal decode failure to caller. + return null; } } From 705c789917b829fb72e46c407db5823be79f7f12 Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 13:21:14 -0700 Subject: [PATCH 5/7] adding logging --- .../java/org/dataone/security/TemporaryMitigationValve.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve.java b/src/main/java/org/dataone/security/TemporaryMitigationValve.java index dfa4f74..ec4456e 100644 --- a/src/main/java/org/dataone/security/TemporaryMitigationValve.java +++ b/src/main/java/org/dataone/security/TemporaryMitigationValve.java @@ -33,6 +33,8 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * Temporary mitigation Valve to handle GHSA-95v2-fvxr-qg83-style path confusion/bypass attempts. @@ -42,6 +44,8 @@ */ public class TemporaryMitigationValve extends ValveBase { + private static final Logger logger = LogManager.getLogger(TemporaryMitigationValve.class.getName()); + private static final int MAX_DECODE_ROUNDS = 3; // deny patterns @@ -56,6 +60,8 @@ public void invoke(Request request, Response response) throws IOException, Servl String target = (qs == null) ? uri : (uri + "?" + qs); if (isSuspicious(target)) { + logger.warn("Rejecting suspicious request from remote address {} for URI {}", + safe(request.getRemoteAddr()), uri); response.sendError(400, "Malformed request target"); return; } From f7c782529d4278b21e8cba973acdf26209270359 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:25:50 +0000 Subject: [PATCH 6/7] test: add focused TemporaryMitigationValve regression coverage --- .../TemporaryMitigationValveTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/test/java/org/dataone/security/TemporaryMitigationValveTest.java diff --git a/src/test/java/org/dataone/security/TemporaryMitigationValveTest.java b/src/test/java/org/dataone/security/TemporaryMitigationValveTest.java new file mode 100644 index 0000000..bae86dc --- /dev/null +++ b/src/test/java/org/dataone/security/TemporaryMitigationValveTest.java @@ -0,0 +1,73 @@ +/** + * This work was created by participants in the DataONE project, and is + * jointly copyrighted by participating institutions in DataONE. For + * more information on DataONE, see our web site at http://dataone.org. + * + * Copyright 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $Id$ + */ + +package org.dataone.security; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.Test; + +public class TemporaryMitigationValveTest { + + private final TemporaryMitigationValve valve = new TemporaryMitigationValve(); + + @Test + public void rejectsPlainTraversal() { + assertTrue(isSuspicious("/v2/resolve/../etc/passwd")); + } + + @Test + public void rejectsSingleEncodedTraversal() { + assertTrue(isSuspicious("/v2/resolve/%2e%2e%2fetc/passwd")); + } + + @Test + public void rejectsDoubleEncodedTraversal() { + assertTrue(isSuspicious("/v2/resolve/%252e%252e%252fetc/passwd")); + } + + @Test + public void rejectsMalformedPercentEncoding() { + assertTrue(isSuspicious("/v2/resolve/%ZZ")); + } + + @Test + public void allowsNormalRequestTarget() { + assertFalse(isSuspicious("/v2/resolve/abc123?includeMetadata=true")); + } + + private boolean isSuspicious(String input) { + try { + Method method = TemporaryMitigationValve.class.getDeclaredMethod("isSuspicious", String.class); + method.setAccessible(true); + return (Boolean) method.invoke(valve, input); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError("Failed to access TemporaryMitigationValve.isSuspicious", e); + } catch (InvocationTargetException e) { + throw new AssertionError("Unexpected exception from TemporaryMitigationValve.isSuspicious", e); + } + } +} From d0d106cadfb5bdd27efcec8abe309a833d0297e9 Mon Sep 17 00:00:00 2001 From: Ian Nesbitt Date: Fri, 10 Jul 2026 13:32:25 -0700 Subject: [PATCH 7/7] Implementing docstring suggestion for explicit code references Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/org/dataone/security/TemporaryMitigationValve.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dataone/security/TemporaryMitigationValve.java b/src/main/java/org/dataone/security/TemporaryMitigationValve.java index ec4456e..a86e489 100644 --- a/src/main/java/org/dataone/security/TemporaryMitigationValve.java +++ b/src/main/java/org/dataone/security/TemporaryMitigationValve.java @@ -39,8 +39,8 @@ /** * Temporary mitigation Valve to handle GHSA-95v2-fvxr-qg83-style path confusion/bypass attempts. * Added 2026-07-10. - * Enable via the element under in server.xml, e.g.: - * + * Enable via the {@code } element under {@code } in {@code server.xml}, e.g.: + * {@code } */ public class TemporaryMitigationValve extends ValveBase {