This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-vfs.git


The following commit(s) were added to refs/heads/master by this push:
     new 1c9b5850c Reject non-ascii hex digits in UriParser percent-decoding 
(#774)
1c9b5850c is described below

commit 1c9b5850c1ef3f61dde411e2fe03153e0e7307cb
Author: Naveed Khan <[email protected]>
AuthorDate: Wed Jul 22 13:38:06 2026 +0000

    Reject non-ascii hex digits in UriParser percent-decoding (#774)
    
    * reject non-ascii hex digits in UriParser percent-decoding
    
    * reuse CharUtils.isHex in hexDigit
---
 .../apache/commons/vfs2/provider/UriParser.java    | 24 ++++++++++++++++++----
 .../commons/vfs2/provider/UriParserTest.java       | 17 +++++++++++++++
 2 files changed, 37 insertions(+), 4 deletions(-)

diff --git 
a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/UriParser.java 
b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/UriParser.java
index 9ca25eff3..9133873f1 100644
--- a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/UriParser.java
+++ b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/UriParser.java
@@ -18,6 +18,7 @@ package org.apache.commons.vfs2.provider;
 
 import java.util.Arrays;
 
+import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.SystemUtils;
 import org.apache.commons.vfs2.FileName;
 import org.apache.commons.vfs2.FileSystemException;
@@ -47,6 +48,21 @@ public final class UriParser {
 
     private static final char LOW_MASK = 0x0F;
 
+    /**
+     * Returns the value of an ASCII hexadecimal digit, or {@code -1} if the 
character is not one.
+     * <p>
+     * Unlike {@link Character#digit(char, int)}, this only accepts the ASCII 
digits {@code 0-9}, {@code a-f} and
+     * {@code A-F} that RFC 3986 permits in a percent-encoding, so Unicode 
look-alikes such as the fullwidth digits are
+     * rejected instead of silently decoded.
+     * </p>
+     *
+     * @param ch the character to convert.
+     * @return the value 0-15, or {@code -1} if {@code ch} is not an ASCII 
hexadecimal digit.
+     */
+    private static int hexDigit(final char ch) {
+        return CharUtils.isHex(ch) ? Character.digit(ch, HEX_BASE) : -1;
+    }
+
     /**
      * Encodes and appends a string to a StringBuilder.
      *
@@ -88,8 +104,8 @@ public final class UriParser {
                 }
 
                 // Decode
-                final int dig1 = Character.digit(buffer.charAt(index + 1), 
HEX_BASE);
-                final int dig2 = Character.digit(buffer.charAt(index + 2), 
HEX_BASE);
+                final int dig1 = hexDigit(buffer.charAt(index + 1));
+                final int dig2 = hexDigit(buffer.charAt(index + 2));
                 if (dig1 == -1 || dig2 == -1) {
                     throw new 
FileSystemException("vfs.provider/invalid-escape-sequence.error",
                             buffer.substring(index, index + 3));
@@ -204,8 +220,8 @@ public final class UriParser {
             }
 
             // Decode
-            final int dig1 = Character.digit(buffer.charAt(index + 1), 
HEX_BASE);
-            final int dig2 = Character.digit(buffer.charAt(index + 2), 
HEX_BASE);
+            final int dig1 = hexDigit(buffer.charAt(index + 1));
+            final int dig2 = hexDigit(buffer.charAt(index + 2));
             if (dig1 == -1 || dig2 == -1) {
                 throw new 
FileSystemException("vfs.provider/invalid-escape-sequence.error",
                         buffer.substring(index, index + 3));
diff --git 
a/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/UriParserTest.java
 
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/UriParserTest.java
index 5c72a3b10..4508ec1b7 100644
--- 
a/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/UriParserTest.java
+++ 
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/UriParserTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.vfs2.provider;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import org.apache.commons.vfs2.FileSystemException;
@@ -75,6 +76,22 @@ public class UriParserTest {
                 
UriParser.decode("ftp://host/outside%25text[inside%25text]tail";));
     }
 
+    @Test
+    public void testDecodeAcceptsAsciiHexDigits() throws FileSystemException {
+        // Valid ASCII escapes still decode, both letter cases.
+        assertEquals("../", UriParser.decode("%2e%2E%2f"));
+        assertEquals("Az", UriParser.decode("%41%7a"));
+    }
+
+    @Test
+    public void testDecodeRejectsNonAsciiHexDigits() {
+        // RFC 3986 pct-encoding is ASCII HEXDIG only. Fullwidth "2e2e2f" 
would otherwise decode to "../".
+        assertThrows(FileSystemException.class, () -> 
UriParser.decode("%2e%2e%2f"));
+        // A single non-ASCII digit in either nibble is enough to reject: 
arabic-indic five (U+0665).
+        assertThrows(FileSystemException.class, () -> UriParser.decode("%4٥"));
+        assertThrows(FileSystemException.class, () -> UriParser.decode("%٥5"));
+    }
+
     @Test
     public void testDecodePreservesPercentInsideIPv6Host() throws 
FileSystemException {
         assertEquals("ftp://[fe80::1%25eth0]/path";,

Reply via email to