File apache-commons-io-CVE-2021-29425.patch of Package apache-commons-io.19112
From 2736b6fe0b3fa22ec8e2b4184897ecadb021fc78 Mon Sep 17 00:00:00 2001
From: Stefan Bodewig <stefan.bodewig@innoq.com>
Date: Thu, 21 Dec 2017 13:49:06 +0100
Subject: [PATCH 2088/2940] IO-559 verify hostname part of suspected UNC paths
in FileNameUtils
---
.../org/apache/commons/io/FilenameUtils.java | 147 +++++++++++++++++-
.../commons/io/FilenameUtilsTestCase.java | 18 +++
2 files changed, 164 insertions(+), 1 deletion(-)
Index: commons-io-2.6-src/src/main/java/org/apache/commons/io/FilenameUtils.java
===================================================================
--- commons-io-2.6-src.orig/src/main/java/org/apache/commons/io/FilenameUtils.java
+++ commons-io-2.6-src/src/main/java/org/apache/commons/io/FilenameUtils.java
@@ -19,8 +19,12 @@ package org.apache.commons.io;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* General filename and filepath manipulation utilities.
@@ -679,7 +683,9 @@ public class FilenameUtils {
}
posUnix = posUnix == NOT_FOUND ? posWin : posUnix;
posWin = posWin == NOT_FOUND ? posUnix : posWin;
- return Math.min(posUnix, posWin) + 1;
+ int pos = Math.min(posUnix, posWin) + 1;
+ String hostnamePart = filename.substring(2, pos - 1);
+ return isValidHostName(hostnamePart) ? pos : NOT_FOUND;
} else {
return isSeparator(ch0) ? 1 : 0;
}
@@ -1450,4 +1456,143 @@ public class FilenameUtils {
return list.toArray( new String[ list.size() ] );
}
+ /**
+ * Checks whether a given string is a valid host name according to
+ * RFC 3986.
+ *
+ * <p>Accepted are IP addresses (v4 and v6) as well as what the
+ * RFC calls a "reg-name". Percent encoded names don't seem to be
+ * valid names in UNC paths.</p>
+ *
+ * @see "https://tools.ietf.org/html/rfc3986#section-3.2.2"
+ * @param name the hostname to validate
+ * @return true if the given name is a valid host name
+ */
+ private static boolean isValidHostName(String name) {
+ return isIPv4Address(name) || isIPv6Address(name) || isRFC3986HostName(name);
+ }
+
+ private static final Pattern IPV4_PATTERN =
+ Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$");
+ private static final int IPV4_MAX_OCTET_VALUE = 255;
+
+ // mostly copied from org.apache.commons.validator.routines.InetAddressValidator#isValidInet4Address
+ private static boolean isIPv4Address(String name) {
+ Matcher m = IPV4_PATTERN.matcher(name);
+ if (!m.matches() || m.groupCount() != 4) {
+ return false;
+ }
+
+ // verify that address subgroups are legal
+ for (int i = 1; i < 5; i++) {
+ String ipSegment = m.group(i);
+ if (ipSegment == null || ipSegment.length() == 0) {
+ return false;
+ }
+
+ int iIpSegment = 0;
+
+ try {
+ iIpSegment = Integer.parseInt(ipSegment);
+ } catch(NumberFormatException e) {
+ return false;
+ }
+
+ if (iIpSegment > IPV4_MAX_OCTET_VALUE) {
+ return false;
+ }
+
+ if (ipSegment.length() > 1 && ipSegment.startsWith("0")) {
+ return false;
+ }
+
+ }
+
+ return true;
+ }
+
+ private static final int IPV6_MAX_HEX_GROUPS = 8;
+ private static final int IPV6_MAX_HEX_DIGITS_PER_GROUP = 4;
+ private static final int MAX_UNSIGNED_SHORT = 0xffff;
+ private static final int BASE_16 = 16;
+
+ // copied from org.apache.commons.validator.routines.InetAddressValidator#isValidInet6Address
+ private static boolean isIPv6Address(String inet6Address) {
+ boolean containsCompressedZeroes = inet6Address.contains("::");
+ if (containsCompressedZeroes && (inet6Address.indexOf("::") != inet6Address.lastIndexOf("::"))) {
+ return false;
+ }
+ if ((inet6Address.startsWith(":") && !inet6Address.startsWith("::"))
+ || (inet6Address.endsWith(":") && !inet6Address.endsWith("::"))) {
+ return false;
+ }
+ String[] octets = inet6Address.split(":");
+ if (containsCompressedZeroes) {
+ List<String> octetList = new ArrayList<String>(Arrays.asList(octets));
+ if (inet6Address.endsWith("::")) {
+ // String.split() drops ending empty segments
+ octetList.add("");
+ } else if (inet6Address.startsWith("::") && !octetList.isEmpty()) {
+ octetList.remove(0);
+ }
+ octets = octetList.toArray(new String[octetList.size()]);
+ }
+ if (octets.length > IPV6_MAX_HEX_GROUPS) {
+ return false;
+ }
+ int validOctets = 0;
+ int emptyOctets = 0; // consecutive empty chunks
+ for (int index = 0; index < octets.length; index++) {
+ String octet = octets[index];
+ if (octet.length() == 0) {
+ emptyOctets++;
+ if (emptyOctets > 1) {
+ return false;
+ }
+ } else {
+ emptyOctets = 0;
+ // Is last chunk an IPv4 address?
+ if (index == octets.length - 1 && octet.contains(".")) {
+ if (!isIPv4Address(octet)) {
+ return false;
+ }
+ validOctets += 2;
+ continue;
+ }
+ if (octet.length() > IPV6_MAX_HEX_DIGITS_PER_GROUP) {
+ return false;
+ }
+ int octetInt = 0;
+ try {
+ octetInt = Integer.parseInt(octet, BASE_16);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ if (octetInt < 0 || octetInt > MAX_UNSIGNED_SHORT) {
+ return false;
+ }
+ }
+ validOctets++;
+ }
+ if (validOctets > IPV6_MAX_HEX_GROUPS || (validOctets < IPV6_MAX_HEX_GROUPS && !containsCompressedZeroes)) {
+ return false;
+ }
+ return true;
+ }
+
+ private static final Pattern REG_NAME_PART_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9-]*$");
+
+ private static boolean isRFC3986HostName(String name) {
+ String[] parts = name.split("\\.", -1);
+ for (int i = 0; i < parts.length; i++) {
+ if (parts[i].length() == 0) {
+ // trailing dot is legal, otherwise we've hit a .. sequence
+ return i == parts.length - 1;
+ }
+ if (!REG_NAME_PART_PATTERN.matcher(parts[i]).matches()) {
+ return false;
+ }
+ }
+ return true;
+ }
}
Index: commons-io-2.6-src/src/test/java/org/apache/commons/io/FilenameUtilsTestCase.java
===================================================================
--- commons-io-2.6-src.orig/src/test/java/org/apache/commons/io/FilenameUtilsTestCase.java
+++ commons-io-2.6-src/src/test/java/org/apache/commons/io/FilenameUtilsTestCase.java
@@ -244,6 +244,15 @@ public class FilenameUtilsTestCase {
assertEquals(null, FilenameUtils.normalize("//server/../a"));
assertEquals(null, FilenameUtils.normalize("//server/.."));
assertEquals(SEP + SEP + "server" + SEP + "", FilenameUtils.normalize("//server/"));
+
+ assertEquals(SEP + SEP + "127.0.0.1" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\\\127.0.0.1\\a\\b\\c.txt"));
+ assertEquals(SEP + SEP + "::1" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\\\::1\\a\\b\\c.txt"));
+ assertEquals(SEP + SEP + "server.example.org" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\\\server.example.org\\a\\b\\c.txt"));
+ assertEquals(SEP + SEP + "server." + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\\\server.\\a\\b\\c.txt"));
+
+ assertEquals(null, FilenameUtils.normalize("\\\\-server\\a\\b\\c.txt"));
+ assertEquals(null, FilenameUtils.normalize("\\\\.\\a\\b\\c.txt"));
+ assertEquals(null, FilenameUtils.normalize("\\\\..\\a\\b\\c.txt"));
}
@Test
@@ -560,6 +569,15 @@ public class FilenameUtilsTestCase {
assertEquals(1, FilenameUtils.getPrefixLength("/:foo"));
assertEquals(1, FilenameUtils.getPrefixLength("/:/"));
assertEquals(1, FilenameUtils.getPrefixLength("/:::::::.txt"));
+
+ assertEquals(12, FilenameUtils.getPrefixLength("\\\\127.0.0.1\\a\\b\\c.txt"));
+ assertEquals(6, FilenameUtils.getPrefixLength("\\\\::1\\a\\b\\c.txt"));
+ assertEquals(21, FilenameUtils.getPrefixLength("\\\\server.example.org\\a\\b\\c.txt"));
+ assertEquals(10, FilenameUtils.getPrefixLength("\\\\server.\\a\\b\\c.txt"));
+
+ assertEquals(-1, FilenameUtils.getPrefixLength("\\\\-server\\a\\b\\c.txt"));
+ assertEquals(-1, FilenameUtils.getPrefixLength("\\\\.\\a\\b\\c.txt"));
+ assertEquals(-1, FilenameUtils.getPrefixLength("\\\\..\\a\\b\\c.txt"));
}
@Test