Package: release.debian.org Severity: normal Tags: bookworm X-Debbugs-Cc: commons-...@packages.debian.org Control: affects -1 + src:commons-vfs User: release.debian....@packages.debian.org Usertags: pu
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 [ Reason ] CVE-2025-27553 has been fixed in Sid/Trixie and in Bullseye for some time now. But users of Bookworm are still vulnerable. This upload attempts to close that gap and to ensure a clean upgrade path for LTS users to Bookworm. [ Impact ] If the upload isn't approved, Bookworm users will continue to be vulnerable, and LTS users that upgrade to Bookworm will become vulnerable. [ Tests ] The tests are run during build and don't show any issues. The patch was also tested by the author and package maintainer, and there haven't been any reports about issues by users of Sid/Trixie or Bullseye. [ Risks ] The usual risks include regressions. But the patch has been tested and successfully deployed to Sid/Trixie and Bullseye without any reported issues. [ Checklist ] [x] *all* changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] The patch changes the normalization process, taking into account URL-encoded characters. [ Other info ] n/a -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEEvu1N7VVEpMA+KD3HS80FZ8KW0F0FAmhjDyYACgkQS80FZ8KW 0F0lPg/8DsPyzqlYgvpUFWZumkfhp5Umxc0A53qmNV+n4eMaKaSmJ6tM9jrelsZB Q2AV614CUpu4alScqpSPvx5MTfNmnoqnyi0isVGKNSXTzIEKr9QRcizFBfv+yVqD uQS5XSOu6oSn4+sSscuk1jgDO9zJsWY77jXH94Uq0YJrNnmNGGv9HyvvGjF4Dqgt Lzw/etpLOTj87qP+CFHdZjW8BSCu0FM1qnZBpbQh9alleIAg+8c3qUWeFGG93qzg QuvD5ky/lW/fUndbzMqySf8bRqb4+JLq3NIszih+E/Xf/bqbOenqXvtpLw+qWpuR oAl7FIBES83K4vMkoyo7o96WhMj4yVZtobsW4H1aDBg6TbzUaXkcqBlZAn3sLlLT Z4MSn4y0Wiq34TYnO6unjfgwgpUIwC+IO6Wl8yXkOfFuXdTF6L4GxKeCuNscvDuH BVcNQkQHU65GZdnqGmn2ytckze8koe0RJwps2WCkNhCcvHxUixMS+3tj5XWkNS1o ePRaPdPO9fnHwfWrZcCuX8k00mPuvsGTe/lrwAf7a6ALDl4zrP+DJFLNzrF6eOB6 y6+a40S1yKkFl2uY0/HS851j82O14VsCUPAHCbHZkCQy9chDYBYocxo+QZSzaF08 lJu2BoWe9pbQ/OwMoORWyBgZkBBe3g43JgiIJyhVwNZLe8QlKZ8= =KxCt -----END PGP SIGNATURE-----
diff -Nru commons-vfs-2.1/debian/changelog commons-vfs-2.1/debian/changelog --- commons-vfs-2.1/debian/changelog 2023-01-02 16:36:10.000000000 +0100 +++ commons-vfs-2.1/debian/changelog 2025-06-30 23:44:12.000000000 +0200 @@ -1,3 +1,15 @@ +commons-vfs (2.1-4+deb12u1) bookworm; urgency=medium + + * Non-maintainer upload by the Debian LTS team. + * d/maven.properties: Force a Java 8 build. + * d/patches/CVE-2025-27553.patch: Fix CVE-2025-27553 (closes: #1101204). + Arnout Engelen discovered a Relative Path Traversal vulnerability in + Commons VFS, a Java library that provides a single API for accessing + various different file systems. A local or remote attacker may use this + flaw to access files and directories outside of a root folder. + + -- Daniel Leidert <dleid...@debian.org> Mon, 30 Jun 2025 23:44:12 +0200 + commons-vfs (2.1-4) unstable; urgency=medium * Team upload. diff -Nru commons-vfs-2.1/debian/gbp.conf commons-vfs-2.1/debian/gbp.conf --- commons-vfs-2.1/debian/gbp.conf 1970-01-01 01:00:00.000000000 +0100 +++ commons-vfs-2.1/debian/gbp.conf 2025-06-30 23:44:12.000000000 +0200 @@ -0,0 +1,4 @@ +[DEFAULT] +upstream-branch = upstream +debian-branch = bookworm +pristine-tar = True diff -Nru commons-vfs-2.1/debian/maven.properties commons-vfs-2.1/debian/maven.properties --- commons-vfs-2.1/debian/maven.properties 2023-01-02 16:36:10.000000000 +0100 +++ commons-vfs-2.1/debian/maven.properties 2025-06-30 23:44:12.000000000 +0200 @@ -1,6 +1,6 @@ maven.test.skip=true -maven.compiler.source=1.5 -maven.compiler.target=1.5 +maven.compiler.source=1.8 +maven.compiler.target=1.8 project.build.sourceEncoding=ISO-8859-1 diff -Nru commons-vfs-2.1/debian/patches/CVE-2025-27553.patch commons-vfs-2.1/debian/patches/CVE-2025-27553.patch --- commons-vfs-2.1/debian/patches/CVE-2025-27553.patch 1970-01-01 01:00:00.000000000 +0100 +++ commons-vfs-2.1/debian/patches/CVE-2025-27553.patch 2025-06-30 23:44:12.000000000 +0200 @@ -0,0 +1,950 @@ +From: Markus Koschany <a...@debian.org> +Date: Wed, 1 Apr 2025 18:30:24 +0200 +Subject: CVE-2025-27553 + +Bug-Debian: https://bugs.debian.org/1101204 +Origin: https://github.com/apache/commons-vfs/pull/396/commits/f1611bfcd518fdcce4d48fe07b83b1f54a6b7b8c +--- + .../apache/commons/vfs2/provider/UriParser.java | 769 +++++++++++---------- + .../org/apache/commons/vfs2/test/NamingTests.java | 7 + + 2 files changed, 409 insertions(+), 367 deletions(-) + +diff --git a/core/src/main/java/org/apache/commons/vfs2/provider/UriParser.java b/core/src/main/java/org/apache/commons/vfs2/provider/UriParser.java +index ef0ba84..b596af1 100644 +--- a/core/src/main/java/org/apache/commons/vfs2/provider/UriParser.java ++++ b/core/src/main/java/org/apache/commons/vfs2/provider/UriParser.java +@@ -16,22 +16,21 @@ + */ + package org.apache.commons.vfs2.provider; + ++import java.util.Arrays; ++ + import org.apache.commons.vfs2.FileName; + import org.apache.commons.vfs2.FileSystemException; + import org.apache.commons.vfs2.FileType; + import org.apache.commons.vfs2.VFS; +-import org.apache.commons.vfs2.util.Os; + + /** + * Utilities for dealing with URIs. See RFC 2396 for details. +- * +- * 2005) $ + */ +-public final class UriParser +-{ ++public final class UriParser { ++ + /** +- * The set of valid separators. These are all converted to the normalized +- * one. Does <i>not</i> contain the normalized separator ++ * The set of valid separators. These are all converted to the normalized one. Does <em>not</em> contain the ++ * normalized separator + */ + // public static final char[] separators = {'\\'}; + public static final char TRANS_SEPARATOR = '\\'; +@@ -47,256 +46,100 @@ public final class UriParser + + private static final char LOW_MASK = 0x0F; + +- private UriParser() +- { +- } +- + /** +- * Extracts the first element of a path. +- * @param name StringBuilder containing the path. +- * @return The first element of the path. ++ * Encodes and appends a string to a StringBuilder. ++ * ++ * @param buffer The StringBuilder to append to. ++ * @param unencodedValue The String to encode and append. ++ * @param reserved characters to encode. + */ +- public static String extractFirstElement(final StringBuilder name) +- { +- final int len = name.length(); +- if (len < 1) +- { +- return null; +- } +- int startPos = 0; +- if (name.charAt(0) == SEPARATOR_CHAR) +- { +- startPos = 1; +- } +- for (int pos = startPos; pos < len; pos++) +- { +- if (name.charAt(pos) == SEPARATOR_CHAR) +- { +- // Found a separator +- final String elem = name.substring(startPos, pos); +- name.delete(startPos, pos + 1); +- return elem; +- } +- } ++ public static void appendEncoded(final StringBuilder buffer, final String unencodedValue, final char[] reserved) { ++ final int offset = buffer.length(); ++ buffer.append(unencodedValue); ++ encode(buffer, offset, unencodedValue.length(), reserved); ++ } + +- // No separator +- final String elem = name.substring(startPos); +- name.setLength(0); +- return elem; ++ static void appendEncodedRfc2396(final StringBuilder buffer, final String unencodedValue, final char[] allowed) { ++ final int offset = buffer.length(); ++ buffer.append(unencodedValue); ++ encodeRfc2396(buffer, offset, unencodedValue.length(), allowed); + } + + /** +- * Normalises a path. Does the following: +- * <ul> +- * <li>Removes empty path elements. +- * <li>Handles '.' and '..' elements. +- * <li>Removes trailing separator. +- * </ul> ++ * Canonicalizes a path. + * +- * Its assumed that the separators are already fixed. +- * +- * @param path The path to normalize. +- * @return The FileType. +- * @throws FileSystemException if an error occurs. +- * +- * @see #fixSeparators ++ * @param buffer Source data. ++ * @param offset Where to start reading. ++ * @param length How much to read. ++ * @param fileNameParser Now to encode and decode. ++ * @throws FileSystemException If an I/O error occurs. + */ +- public static FileType normalisePath(final StringBuilder path) +- throws FileSystemException +- { +- FileType fileType = FileType.FOLDER; +- if (path.length() == 0) +- { +- return fileType; +- } +- +- if (path.charAt(path.length() - 1) != '/') +- { +- fileType = FileType.FILE; +- } +- +- // Adjust separators +- // fixSeparators(path); +- +- // Determine the start of the first element +- int startFirstElem = 0; +- if (path.charAt(0) == SEPARATOR_CHAR) +- { +- if (path.length() == 1) +- { +- return fileType; +- } +- startFirstElem = 1; +- } +- +- // Iterate over each element +- int startElem = startFirstElem; +- int maxlen = path.length(); +- while (startElem < maxlen) +- { +- // Find the end of the element +- int endElem = startElem; +- for (; endElem < maxlen && path.charAt(endElem) != SEPARATOR_CHAR; endElem++) +- { +- } +- +- final int elemLen = endElem - startElem; +- if (elemLen == 0) +- { +- // An empty element - axe it +- path.delete(endElem, endElem + 1); +- maxlen = path.length(); +- continue; +- } +- if (elemLen == 1 && path.charAt(startElem) == '.') +- { +- // A '.' element - axe it +- path.delete(startElem, endElem + 1); +- maxlen = path.length(); +- continue; +- } +- if (elemLen == 2 && path.charAt(startElem) == '.' +- && path.charAt(startElem + 1) == '.') +- { +- // A '..' element - remove the previous element +- if (startElem == startFirstElem) +- { +- // Previous element is missing +- throw new FileSystemException( +- "vfs.provider/invalid-relative-path.error"); ++ public static void canonicalizePath(final StringBuilder buffer, final int offset, final int length, ++ final FileNameParser fileNameParser) throws FileSystemException { ++ int index = offset; ++ int count = length; ++ for (; count > 0; count--, index++) { ++ final char ch = buffer.charAt(index); ++ if (ch == '%') { ++ if (count < 3) { ++ throw new FileSystemException("vfs.provider/invalid-escape-sequence.error", ++ buffer.substring(index, index + count)); + } + +- // Find start of previous element +- int pos = startElem - 2; +- for (; pos >= 0 && path.charAt(pos) != SEPARATOR_CHAR; pos--) +- { ++ // Decode ++ final int dig1 = Character.digit(buffer.charAt(index + 1), HEX_BASE); ++ final int dig2 = Character.digit(buffer.charAt(index + 2), HEX_BASE); ++ if (dig1 == -1 || dig2 == -1) { ++ throw new FileSystemException("vfs.provider/invalid-escape-sequence.error", ++ buffer.substring(index, index + 3)); + } +- startElem = pos + 1; +- +- path.delete(startElem, endElem + 1); +- maxlen = path.length(); +- continue; +- } +- +- // A regular element +- startElem = endElem + 1; +- } ++ final char value = (char) (dig1 << BITS_IN_HALF_BYTE | dig2); + +- // Remove trailing separator +- if (!VFS.isUriStyle() && maxlen > 1 && path.charAt(maxlen - 1) == SEPARATOR_CHAR) +- { +- path.delete(maxlen - 1, maxlen); +- } ++ final boolean match = value == '%' || fileNameParser.encodeCharacter(value); + +- return fileType; +- } ++ if (match) { ++ // this is a reserved character, not allowed to decode ++ index += 2; ++ count -= 2; ++ continue; ++ } + +- /** +- * Normalises the separators in a name. +- * @param name The StringBuilder containing the name +- * @return true if the StringBuilder was modified. +- */ +- public static boolean fixSeparators(final StringBuilder name) +- { +- boolean changed = false; +- final int maxlen = name.length(); +- for (int i = 0; i < maxlen; i++) +- { +- final char ch = name.charAt(i); +- if (ch == TRANS_SEPARATOR) +- { +- name.setCharAt(i, SEPARATOR_CHAR); +- changed = true; ++ // Replace ++ buffer.setCharAt(index, value); ++ buffer.delete(index + 1, index + 3); ++ count -= 2; ++ } else if (fileNameParser.encodeCharacter(ch)) { ++ // Encode ++ final char[] digits = {Character.forDigit(ch >> BITS_IN_HALF_BYTE & LOW_MASK, HEX_BASE), Character.forDigit(ch & LOW_MASK, HEX_BASE)}; ++ buffer.setCharAt(index, '%'); ++ buffer.insert(index + 1, digits); ++ index += 2; + } + } +- return changed; +- } +- +- /** +- * Extracts the scheme from a URI. +- * +- * @param uri The URI. +- * @return The scheme name. Returns null if there is no scheme. +- */ +- public static String extractScheme(final String uri) +- { +- return extractScheme(uri, null); + } + + /** +- * Extracts the scheme from a URI. Removes the scheme and ':' delimiter from +- * the front of the URI. ++ * Decodes the String. + * +- * @param uri The URI. +- * @param buffer Returns the remainder of the URI. +- * @return The scheme name. Returns null if there is no scheme. ++ * @param uri The String to decode. ++ * @throws FileSystemException if an error occurs. + */ +- public static String extractScheme(final String uri, final StringBuilder buffer) +- { +- if (buffer != null) +- { +- buffer.setLength(0); +- buffer.append(uri); +- } +- +- final int maxPos = uri.length(); +- for (int pos = 0; pos < maxPos; pos++) +- { +- final char ch = uri.charAt(pos); +- +- if (ch == ':') +- { +- // Found the end of the scheme +- final String scheme = uri.substring(0, pos); +- if (scheme.length() <= 1 && Os.isFamily(Os.OS_FAMILY_WINDOWS)) +- { +- // This is not a scheme, but a Windows drive letter +- return null; +- } +- if (buffer != null) +- { +- buffer.delete(0, pos + 1); +- } +- return scheme.intern(); +- } +- +- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) +- { +- // A scheme character +- continue; +- } +- if (pos > 0 +- && ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.')) +- { +- // A scheme character (these are not allowed as the first +- // character of the scheme, but can be used as subsequent +- // characters. +- continue; +- } +- +- // Not a scheme character +- break; +- } +- +- // No scheme in URI +- return null; ++ public static void checkUriEncoding(final String uri) throws FileSystemException { ++ decode(uri); + } + + /** + * Removes %nn encodings from a string. ++ * + * @param encodedStr The encoded String. + * @return The decoded String. + * @throws FileSystemException if an error occurs. + */ +- public static String decode(final String encodedStr) +- throws FileSystemException +- { +- if (encodedStr == null) +- { ++ public static String decode(final String encodedStr) throws FileSystemException { ++ if (encodedStr == null) { + return null; + } +- if (encodedStr.indexOf('%') < 0) +- { ++ if (encodedStr.indexOf('%') < 0) { + return encodedStr; + } + final StringBuilder buffer = new StringBuilder(encodedStr); +@@ -306,38 +149,40 @@ public final class UriParser + + /** + * Removes %nn encodings from a string. ++ * + * @param buffer StringBuilder containing the string to decode. + * @param offset The position in the string to start decoding. + * @param length The number of characters to decode. + * @throws FileSystemException if an error occurs. + */ + public static void decode(final StringBuilder buffer, final int offset, final int length) +- throws FileSystemException +- { ++ throws FileSystemException { + int index = offset; + int count = length; +- for (; count > 0; count--, index++) +- { ++ boolean ipv6Host = false; ++ for (; count > 0; count--, index++) { + final char ch = buffer.charAt(index); +- if (ch != '%') +- { ++ if (ch == '[') { ++ ipv6Host = true; ++ } ++ if (ch == ']') { ++ ipv6Host = false; ++ } ++ if (ch != '%' || ipv6Host) { + continue; + } +- if (count < 3) +- { +- throw new FileSystemException( +- "vfs.provider/invalid-escape-sequence.error", buffer +- .substring(index, index + count)); ++ ++ if (count < 3) { ++ throw new FileSystemException("vfs.provider/invalid-escape-sequence.error", ++ buffer.substring(index, index + count)); + } + + // Decode + final int dig1 = Character.digit(buffer.charAt(index + 1), HEX_BASE); + final int dig2 = Character.digit(buffer.charAt(index + 2), HEX_BASE); +- if (dig1 == -1 || dig2 == -1) +- { +- throw new FileSystemException( +- "vfs.provider/invalid-escape-sequence.error", buffer +- .substring(index, index + 3)); ++ if (dig1 == -1 || dig2 == -1) { ++ throw new FileSystemException("vfs.provider/invalid-escape-sequence.error", ++ buffer.substring(index, index + 3)); + } + final char value = (char) (dig1 << BITS_IN_HALF_BYTE | dig2); + +@@ -349,52 +194,70 @@ public final class UriParser + } + + /** +- * Encodes and appends a string to a StringBuilder. +- * @param buffer The StringBuilder to append to. +- * @param unencodedValue The String to encode and append. +- * @param reserved characters to encode. ++ * Converts "special" characters to their %nn value. ++ * ++ * @param decodedStr The decoded String. ++ * @return The encoded String. + */ +- public static void appendEncoded(final StringBuilder buffer, +- final String unencodedValue, final char[] reserved) +- { +- final int offset = buffer.length(); +- buffer.append(unencodedValue); +- encode(buffer, offset, unencodedValue.length(), reserved); ++ public static String encode(final String decodedStr) { ++ return encode(decodedStr, null); + } + + /** +- * Encodes a set of reserved characters in a StringBuilder, using the URI %nn +- * encoding. Always encodes % characters. ++ * Converts "special" characters to their %nn value. ++ * ++ * @param decodedStr The decoded String. ++ * @param reserved Characters to encode. ++ * @return The encoded String ++ */ ++ public static String encode(final String decodedStr, final char[] reserved) { ++ if (decodedStr == null) { ++ return null; ++ } ++ final StringBuilder buffer = new StringBuilder(decodedStr); ++ encode(buffer, 0, buffer.length(), reserved); ++ return buffer.toString(); ++ } ++ ++ /** ++ * Encode an array of Strings. ++ * ++ * @param strings The array of Strings to encode. ++ * @return An array of encoded Strings. ++ */ ++ public static String[] encode(final String[] strings) { ++ if (strings == null) { ++ return null; ++ } ++ Arrays.setAll(strings, i -> encode(strings[i])); ++ return strings; ++ } ++ ++ /** ++ * Encodes a set of reserved characters in a StringBuilder, using the URI %nn encoding. Always encodes % characters. ++ * + * @param buffer The StringBuilder to append to. + * @param offset The position in the buffer to start encoding at. + * @param length The number of characters to encode. + * @param reserved characters to encode. + */ +- public static void encode(final StringBuilder buffer, final int offset, +- final int length, final char[] reserved) +- { ++ public static void encode(final StringBuilder buffer, final int offset, final int length, final char[] reserved) { + int index = offset; + int count = length; +- for (; count > 0; index++, count--) +- { ++ for (; count > 0; index++, count--) { + final char ch = buffer.charAt(index); + boolean match = ch == '%'; +- if (reserved != null) +- { +- for (int i = 0; !match && i < reserved.length; i++) +- { +- if (ch == reserved[i]) +- { ++ if (reserved != null) { ++ for (int i = 0; !match && i < reserved.length; i++) { ++ if (ch == reserved[i]) { + match = true; ++ break; + } + } + } +- if (match) +- { ++ if (match) { + // Encode +- final char[] digits = +- {Character.forDigit((ch >> BITS_IN_HALF_BYTE) & LOW_MASK, HEX_BASE), +- Character.forDigit(ch & LOW_MASK, HEX_BASE)}; ++ final char[] digits = {Character.forDigit(ch >> BITS_IN_HALF_BYTE & LOW_MASK, HEX_BASE), Character.forDigit(ch & LOW_MASK, HEX_BASE)}; + buffer.setCharAt(index, '%'); + buffer.insert(index + 1, digits); + index += 2; +@@ -402,135 +265,307 @@ public final class UriParser + } + } + +- /** +- * Removes %nn encodings from a string. +- * @param decodedStr The decoded String. +- * @return The encoded String. +- */ +- public static String encode(final String decodedStr) +- { +- return encode(decodedStr, null); ++ static void encodeRfc2396(final StringBuilder buffer, final int offset, final int length, final char[] allowed) { ++ int index = offset; ++ int count = length; ++ for (; count > 0; index++, count--) { ++ final char ch = buffer.charAt(index); ++ if (Arrays.binarySearch(allowed, ch) < 0) { ++ // Encode ++ final char[] digits = {Character.forDigit(ch >> BITS_IN_HALF_BYTE & LOW_MASK, HEX_BASE), Character.forDigit(ch & LOW_MASK, HEX_BASE)}; ++ buffer.setCharAt(index, '%'); ++ buffer.insert(index + 1, digits); ++ index += 2; ++ } ++ } + } + + /** +- * Converts "special" characters to their %nn value. +- * @param decodedStr The decoded String. +- * @param reserved Characters to encode. +- * @return The encoded String ++ * Extracts the first element of a path. ++ * ++ * @param name StringBuilder containing the path. ++ * @return The first element of the path. + */ +- public static String encode(final String decodedStr, final char[] reserved) +- { +- if (decodedStr == null) +- { ++ public static String extractFirstElement(final StringBuilder name) { ++ final int len = name.length(); ++ if (len < 1) { + return null; + } +- final StringBuilder buffer = new StringBuilder(decodedStr); +- encode(buffer, 0, buffer.length(), reserved); +- return buffer.toString(); ++ int startPos = 0; ++ if (name.charAt(0) == SEPARATOR_CHAR) { ++ startPos = 1; ++ } ++ for (int pos = startPos; pos < len; pos++) { ++ if (name.charAt(pos) == SEPARATOR_CHAR) { ++ // Found a separator ++ final String elem = name.substring(startPos, pos); ++ name.delete(startPos, pos + 1); ++ return elem; ++ } ++ } ++ ++ // No separator ++ final String elem = name.substring(startPos); ++ name.setLength(0); ++ return elem; + } + + /** +- * Encode an array of Strings. +- * @param strings The array of Strings to encode. +- * @return An array of encoded Strings. ++ * Extract the query String from the URI. ++ * ++ * @param name StringBuilder containing the URI. ++ * @return The query string, if any. null otherwise. + */ +- public static String[] encode(final String[] strings) +- { +- if (strings == null) +- { +- return null; +- } +- for (int i = 0; i < strings.length; i++) +- { +- strings[i] = encode(strings[i]); ++ public static String extractQueryString(final StringBuilder name) { ++ for (int pos = 0; pos < name.length(); pos++) { ++ if (name.charAt(pos) == '?') { ++ final String queryString = name.substring(pos + 1); ++ name.delete(pos, name.length()); ++ return queryString; ++ } + } +- return strings; ++ ++ return null; + } + + /** +- * Decodes the String. +- * @param uri The String to decode. +- * @throws FileSystemException if an error occurs. ++ * Extracts the scheme from a URI. ++ * ++ * @param uri The URI. ++ * @return The scheme name. Returns null if there is no scheme. ++ * @deprecated Use instead {@link #extractScheme}. Will be removed in 3.0. + */ +- public static void checkUriEncoding(final String uri) throws FileSystemException +- { +- decode(uri); ++ @Deprecated ++ public static String extractScheme(final String uri) { ++ return extractScheme(uri, null); + } + +- public static void canonicalizePath(final StringBuilder buffer, final int offset, +- final int length, final FileNameParser fileNameParser) +- throws FileSystemException +- { +- int index = offset; +- int count = length; +- for (; count > 0; count--, index++) +- { +- final char ch = buffer.charAt(index); +- if (ch == '%') +- { +- if (count < 3) +- { +- throw new FileSystemException( +- "vfs.provider/invalid-escape-sequence.error", +- buffer.substring(index, index + count)); +- } ++ /** ++ * Extracts the scheme from a URI. Removes the scheme and ':' delimiter from the front of the URI. ++ * ++ * @param uri The URI. ++ * @param buffer Returns the remainder of the URI. ++ * @return The scheme name. Returns null if there is no scheme. ++ * @deprecated Use instead {@link #extractScheme}. Will be removed in 3.0. ++ */ ++ @Deprecated ++ public static String extractScheme(final String uri, final StringBuilder buffer) { ++ if (buffer != null) { ++ buffer.setLength(0); ++ buffer.append(uri); ++ } + +- // Decode +- final int dig1 = Character.digit(buffer.charAt(index + 1), HEX_BASE); +- final int dig2 = Character.digit(buffer.charAt(index + 2), HEX_BASE); +- if (dig1 == -1 || dig2 == -1) +- { +- throw new FileSystemException( +- "vfs.provider/invalid-escape-sequence.error", +- buffer.substring(index, index + 3)); ++ final int maxPos = uri.length(); ++ for (int pos = 0; pos < maxPos; pos++) { ++ final char ch = uri.charAt(pos); ++ ++ if (ch == ':') { ++ // Found the end of the scheme ++ final String scheme = uri.substring(0, pos); ++ if (buffer != null) { ++ buffer.delete(0, pos + 1); + } +- final char value = (char) (dig1 << BITS_IN_HALF_BYTE | dig2); ++ return scheme.intern(); ++ } + +- final boolean match = value == '%' || fileNameParser.encodeCharacter(value); ++ if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { ++ // A scheme character ++ continue; ++ } ++ if (!(pos > 0 && (ch >= '0' && ch <= '9' || ch == '+' || ch == '-' || ch == '.'))) { ++ // Not a scheme character ++ break; ++ } ++ // A scheme character (these are not allowed as the first ++ // character of the scheme), but can be used as subsequent ++ // characters. ++ } + +- if (match) +- { +- // this is a reserved character, not allowed to decode +- index += 2; +- count -= 2; +- continue; ++ // No scheme in URI ++ return null; ++ } ++ ++ /** ++ * Extracts the scheme from a URI. Removes the scheme and ':' delimiter from the front of the URI. ++ * <p> ++ * The scheme is extracted based on the currently supported schemes in the system. That is to say the schemes ++ * supported by the registered providers. ++ * </p> ++ * <p> ++ * This allows us to handle varying scheme's without making assumptions based on the ':' character. Specifically ++ * handle scheme extraction calls for URI parameters that are not actually uri's, but may be names with ':' in them. ++ * </p> ++ * @param schemes The schemes to check. ++ * @param uri The potential URI. May also be a name. ++ * @return The scheme name. Returns null if there is no scheme. ++ * @since 2.3 ++ */ ++ public static String extractScheme(final String[] schemes, final String uri) { ++ return extractScheme(schemes, uri, null); ++ } ++ ++ /** ++ * Extracts the scheme from a URI. Removes the scheme and ':' delimiter from the front of the URI. ++ * <p> ++ * The scheme is extracted based on the given set of schemes. Normally, that is to say the schemes ++ * supported by the registered providers. ++ * </p> ++ * <p> ++ * This allows us to handle varying scheme's without making assumptions based on the ':' character. Specifically ++ * handle scheme extraction calls for URI parameters that are not actually URI's, but may be names with ':' in them. ++ * </p> ++ * @param schemes The schemes to check. ++ * @param uri The potential URI. May also just be a name. ++ * @param buffer Returns the remainder of the URI. ++ * @return The scheme name. Returns null if there is no scheme. ++ * @since 2.3 ++ */ ++ public static String extractScheme(final String[] schemes, final String uri, final StringBuilder buffer) { ++ if (buffer != null) { ++ buffer.setLength(0); ++ buffer.append(uri); ++ } ++ for (final String scheme : schemes) { ++ if (uri.startsWith(scheme + ":")) { ++ if (buffer != null) { ++ buffer.delete(0, uri.indexOf(':') + 1); + } ++ return scheme; ++ } ++ } ++ return null; ++ } + +- // Replace +- buffer.setCharAt(index, value); +- buffer.delete(index + 1, index + 3); +- count -= 2; ++ /** ++ * Normalises the separators in a name. ++ * ++ * @param name The StringBuilder containing the name ++ * @return true if the StringBuilder was modified. ++ */ ++ public static boolean fixSeparators(final StringBuilder name) { ++ boolean changed = false; ++ int maxlen = name.length(); ++ for (int i = 0; i < maxlen; i++) { ++ final char ch = name.charAt(i); ++ if (ch == TRANS_SEPARATOR) { ++ name.setCharAt(i, SEPARATOR_CHAR); ++ changed = true; + } +- else if (fileNameParser.encodeCharacter(ch)) +- { +- // Encode +- final char[] digits = +- {Character.forDigit((ch >> BITS_IN_HALF_BYTE) & LOW_MASK, HEX_BASE), +- Character.forDigit(ch & LOW_MASK, HEX_BASE)}; +- buffer.setCharAt(index, '%'); +- buffer.insert(index + 1, digits); +- index += 2; ++ if (i < maxlen - 2 && name.charAt(i) == '%' && name.charAt(i + 1) == '2') { ++ if (name.charAt(i + 2) == 'f' || name.charAt(i + 2) == 'F') { ++ name.setCharAt(i, SEPARATOR_CHAR); ++ name.delete(i + 1, i + 3); ++ maxlen -= 2; ++ changed = true; ++ } else if (name.charAt(i + 2) == 'e' || name.charAt(i + 2) == 'E') { ++ name.setCharAt(i, '.'); ++ name.delete(i + 1, i + 3); ++ maxlen -= 2; ++ changed = true; ++ } + } + } ++ return changed; + } + + /** +- * Extract the query String from the URI. +- * @param name StringBuilder containing the URI. +- * @return The query string, if any. null otherwise. ++ * Normalises a path. Does the following: ++ * <ul> ++ * <li>Removes empty path elements. ++ * <li>Handles '.' and '..' elements. ++ * <li>Removes trailing separator. ++ * </ul> ++ * ++ * Its assumed that the separators are already fixed. ++ * ++ * @param path The path to normalize. ++ * @return The FileType. ++ * @throws FileSystemException if an error occurs. ++ * ++ * @see #fixSeparators + */ +- public static String extractQueryString(final StringBuilder name) +- { +- for (int pos = 0; pos < name.length(); pos++) +- { +- if (name.charAt(pos) == '?') +- { +- final String queryString = name.substring(pos + 1); +- name.delete(pos, name.length()); +- return queryString; ++ public static FileType normalisePath(final StringBuilder path) throws FileSystemException { ++ FileType fileType = FileType.FOLDER; ++ if (path.length() == 0) { ++ return fileType; ++ } ++ ++ // '/' or '.' or '..' or anyPath/..' or 'anyPath/.' should always be a path ++ if (path.charAt(path.length() - 1) != '/' ++ && path.lastIndexOf("/..") != path.length() - 3 ++ && path.lastIndexOf("/.") != path.length() - 2 ++ && path.lastIndexOf("..") != 0 ++ && path.lastIndexOf(".") != 0 ++ ) { ++ fileType = FileType.FILE; ++ } ++ ++ // Adjust separators ++ // fixSeparators(path); ++ ++ // Determine the start of the first element ++ int startFirstElem = 0; ++ if (path.charAt(0) == SEPARATOR_CHAR) { ++ if (path.length() == 1) { ++ return fileType; + } ++ startFirstElem = 1; + } + +- return null; ++ // Iterate over each element ++ int startElem = startFirstElem; ++ int maxlen = path.length(); ++ while (startElem < maxlen) { ++ // Find the end of the element ++ int endElem = startElem; ++ while (endElem < maxlen && path.charAt(endElem) != SEPARATOR_CHAR) { ++ endElem++; ++ } ++ ++ final int elemLen = endElem - startElem; ++ if (elemLen == 0) { ++ // An empty element - axe it ++ path.deleteCharAt(endElem); ++ maxlen = path.length(); ++ continue; ++ } ++ if (elemLen == 1 && path.charAt(startElem) == '.') { ++ // A '.' element - axe it ++ path.deleteCharAt(startElem); ++ maxlen = path.length(); ++ continue; ++ } ++ if (elemLen == 2 && path.charAt(startElem) == '.' && path.charAt(startElem + 1) == '.') { ++ // A '..' element - remove the previous element ++ if (startElem == startFirstElem) { ++ // Previous element is missing ++ throw new FileSystemException("vfs.provider/invalid-relative-path.error"); ++ } ++ ++ // Find start of previous element ++ int pos = startElem - 2; ++ while (pos >= 0 && path.charAt(pos) != SEPARATOR_CHAR) { ++ pos--; ++ } ++ startElem = pos + 1; ++ ++ path.delete(startElem, endElem + 1); ++ maxlen = path.length(); ++ continue; ++ } ++ ++ // A regular element ++ startElem = endElem + 1; ++ } ++ ++ // Remove trailing separator ++ if (!VFS.isUriStyle() && maxlen > 1 && path.charAt(maxlen - 1) == SEPARATOR_CHAR) { ++ path.deleteCharAt(maxlen - 1); ++ } ++ ++ return fileType; ++ } ++ ++ private UriParser() { + } + } +diff --git a/core/src/test/java/org/apache/commons/vfs2/test/NamingTests.java b/core/src/test/java/org/apache/commons/vfs2/test/NamingTests.java +index 9d90d45..a1672c5 100644 +--- a/core/src/test/java/org/apache/commons/vfs2/test/NamingTests.java ++++ b/core/src/test/java/org/apache/commons/vfs2/test/NamingTests.java +@@ -188,6 +188,9 @@ public class NamingTests + final String path = name.getPath() + "/a"; + assertSameName(path, name, path, scope); + assertSameName(path, name, "../" + name.getBaseName() + "/a", scope); ++ assertSameName(path, name, "foo%2f..%2fa", scope); ++ assertSameName(path, name, "foo%2f/..%2fa", scope); ++ assertSameName(path, name, "foo/%2f..%2fa", scope); + + // Test an empty name + assertBadName(name, "", scope); +@@ -201,6 +204,10 @@ public class NamingTests + assertBadName(name, "../a", scope); + assertBadName(name, "../" + name.getBaseName() + "a", scope); + assertBadName(name, "a/..", scope); ++ assertBadName(name, "%2e%2e/ab", scope); ++ assertBadName(name, "..%2f../ab", scope); ++ assertBadName(name, "foo%2f..%2f..%2fa", scope); ++ assertBadName(name, "foo%2f..%2f..%2fnone-child%2fa", scope); + + // Test absolute names + assertBadName(name, "/", scope); diff -Nru commons-vfs-2.1/debian/patches/series commons-vfs-2.1/debian/patches/series --- commons-vfs-2.1/debian/patches/series 2023-01-02 16:36:10.000000000 +0100 +++ commons-vfs-2.1/debian/patches/series 2025-06-30 23:44:12.000000000 +0200 @@ -1,2 +1,3 @@ disable_webdav_provider.diff disable_hdfs_provider.diff +CVE-2025-27553.patch