Technical Advisory – OpenJDK – Weak Parsing Logic in java.net.InetAddress and Related Classes

Vendor: OpenJDK Project
Vendor URL: https://openjdk.java.net
Versions affected: 8-17+ (and likely earlier versions)
Systems Affected: All supported systems
Author: Jeff Dileo <jeff.dileo[at]nccgroup[dot]com>
Advisory URL / CVE Identifier: TBD
Risk: Low (implicit data validation bypass)

Summary

The private static InetAddress::getAllByName(String,InetAddress) method is used internally and by the public static InetAddress::getAllByName(String) to resolve host or IP strings to IP addresses. It is also used to implement the public static InetAddress::getByName(String) and private static InetAddress::getByName(String,InetAddress) methods. When these methods are passed IP address strings, they will, per the Java documentation, validate the format of the address.

However, the OpenJDK implementation of this method does not conform to the documented API, and does not properly validate the format of a given IP address string, allowing arbitrary characters within IPv6 address strings, including those representing IPv4 addresses. Due to this, any uses of this method to validate host names to protect against injection attacks may be bypassed.

Location

  • src/java.base/share/classes/java/net/InetAddress.java
    • private static int checkNumericZone(String)
    • private static InetAddress[] getAllByName(String,InetAddress)
    • private static InetAddress getByName(String,InetAddress)
    • public static InetAddress getByName(String)
    • public static InetAddress[] getAllByName(String)
  • src/java.base/share/classes/sun/net/util/IPAddressUtil.java
    • public static byte[] textToNumericFormatV6(String)
    • public static byte[] convertFromIPv4MappedAddress(byte[])

Impact

An attacker may trivially bypass the use of InetAddress::getAllByName to validate inputs.

Note: As input validation is not an appropriate mechanism to protect against injection attacks — as opposed to output encoding and Harvard architecture-style APIs — this issue is itself considered to be of Low risk as code relying on the documented validation for such purposes should be considered insecure regardless of this issue.

Details

The static InetAddress::getAllByName method, and the static InetAddress::getByName method it underpins, are used to resolve host strings to IP addresses in the form of java.net.InetAddress objects, specifically the Inet4Address and Inet6Address classes that subclass InetAddress.

These methods accept strings of IP addresses, and, per the Java documentation for the methods, are expected only to validate the format of the address1:

Given the name of a host, returns an array of its IP addresses based on the configured name service on the system.

The host name can either be a machine name, such as “www.example.com”, or a textual representation of its IP address. If a literal IP address is supplied, only the validity of the address format is checked.

For host specified in literal IPv6 address, either the form defined in RFC 2732 or the literal IPv6 address format defined in RFC 2373 is accepted. A literal IPv6 address may also be qualified by appending a scoped zone identifier or scope_id.

However, the underlying implementation for these methods within OpenJDK, the official reference implementation of Java, does not properly implement its IP address parser, specifically its handling of IPv6 scoped address zone identifiers.

Within the InetAddress class implementation, the underlying parsing flow will attempt to parse for IP address strings, and fall back to host name lookup. Within this IP address parsing logic, it will first parse for IPv4 addresses, and then if that parse fails, treat the string as a potential IPv6 address. However, to handle zone identifiers, if the private InetAddress::getAllByName observes a literal percent character (%) within the string, it will pass the string to the private InetAddress::checkNumericZone static method.

addr = IPAddressUtil.textToNumericFormatV4(host);
if (addr == null) {
    // This is supposed to be an IPv6 literal
    // Check if a numeric or string zone id is present
    int pos;
    if ((pos=host.indexOf ('%')) != -1) {
        numericZone = checkNumericZone (host);
        if (numericZone == -1) { /* remainder of string must be an ifname */
            ifname = host.substring (pos+1);
        }
    }
    ...
src/java.base/share/classes/java/net/InetAddress.java

This method incorrectly assumes that a ] character represents the end of the address string, but does not verify that this is the case, only checking to ensure that the ] character does not appear immediately after the %.

for (int i=percent+1; i<slen; i++) {
    char c = s.charAt(i);
    if (c == ']') {
        if (i == percent+1) {
            /* empty per-cent field */
            return -1;
        }
        break;
    }
    ...
src/java.base/share/classes/java/net/InetAddress.java

This is an issue as no such validation occurs earlier within the private InetAddress::getAllByName. Instead, it uses only a simple check that the first and last characters are [ and ], respectively, the format for using literal IPv6 addresses within URLs, in order to remove them.

if (host.charAt(0) == '[') {
    // This is supposed to be an IPv6 literal
    if (host.length() > 2 && host.charAt(host.length()-1) == ']') {
        host = host.substring(1, host.length() -1);
src/java.base/share/classes/java/net/InetAddress.java

Following the call to InetAddress::checkNumericZone, the IPAddressUtil::textToNumericFormatV6 static method is used to actually parse the IPv6 address string into a byte array representation. This method specifically ignores zone identifiers by effectively truncating the content it parses to the last character before the first % if one exists.

char[] srcb = src.toCharArray();
byte[] dst = new byte[INADDR16SZ];

int srcb_length = srcb.length;
int pc = src.indexOf ('%');
if (pc == srcb_length -1) {
    return null;
}

if (pc != -1) {
    srcb_length = pc;
}
src/java.base/share/classes/sun/net/util/IPAddressUtil.java

As a result of each of these components of the IPv6 address parsing logic truncating and/or ignoring data beyond certain metacharacters, InetAddress::getAllByName will accept invalid IPv6 address strings such as the following:

  • ::1%1]foo.bar baz'"
  • [::1%1]foo.bar baz'"]
  • 2606:4700:4700::1111%1]foo.bar baz'"
  • [2606:4700:4700::1111%1]foo.bar baz'"]

This additionally applies to IPv4-compatible IPv6 addresses, such as the following:

  • ::1.1.1.1%1]foo.bar baz '"
  • [::1.1.1.1%1]foo.bar baz '"]
  • ::0101:0101%1]foo.bar baz '"
  • [::0101:0101%1]foo.bar baz '"]

Furthermore, a separate issue exists in the handling of IPv4-mapped IPv6 addresses, as, unlike IPv4-compatible IPv6 addresses, which are parsed into Inet6Address objects, the IPv4-mapped addresses are returned as Inet4Address objects with no concept of an IPv6 scope. This occurs between a special case handled by the static IPAddressUtil::textToNumericFormatV6 method:

if (j != INADDR16SZ)
    return null;
byte[] newdst = convertFromIPv4MappedAddress(dst);
if (newdst != null) {
    return newdst;
} else {
    return dst;
}
src/java.base/share/classes/sun/net/util/IPAddressUtil.java

The static IPAddressUtil::convertFromIPv4MappedAddress method will return a byte array of size 4 (INADDR4SZ) containing the IPv4 address bytes from the byte array representation of the address string, should it match the structure of an IPv4-mapped IPv6 address:

public static byte[] convertFromIPv4MappedAddress(byte[] addr) {
    if (isIPv4MappedAddress(addr)) {
        byte[] newAddr = new byte[INADDR4SZ];
        System.arraycopy(addr, 12, newAddr, 0, INADDR4SZ);
        return newAddr;
    }
    return null;
}
src/java.base/share/classes/sun/net/util/IPAddressUtil.java

When such a byte array is returned back to the private InetAddress::getAllByName static method, it will then be used to return an Inet4Address.

InetAddress[] ret = new InetAddress[1];
if(addr != null) {
    if (addr.length == Inet4Address.INADDRSZ) {
        ret[0] = new Inet4Address(null, addr);
    } else {
        if (ifname != null) {
            ret[0] = new Inet6Address(null, addr, ifname);
        } else {
            ret[0] = new Inet6Address(null, addr, numericZone);
        }
    }
    return ret;
}
src/java.base/share/classes/java/net/InetAddress.java

Due to this, any arbitrary scope value can be provided, as the ifname variable would only be validated in the Inet6Address(String,byte[],String) constructor, regardless of if it being set due to InetAddress::checkNumericZone rejecting the address string. As a result, InetAddress::getAllByName will additionally accept invalid IPv4-mapped IPv6 address strings such as the following:

  • ::ffff:1.1.1.1%1]foo.bar baz'"
  • [::ffff:1.1.1.1%1]foo.bar baz'"]
  • ::ffff:0101:0101%1]foo.bar baz'"
  • [::ffff:0101:0101%1]foo.bar baz'"]
  • ::ffff:1.1.1.1%1foo.bar baz'"
  • [::ffff:1.1.1.1%1foo.bar baz'"]
  • ::ffff:0101:0101%1foo.bar baz'"
  • [::ffff:0101:0101%1foo.bar baz'"]

Technical Recommendation

Modify the InetAddress::checkNumericZone static method to remove the iteration check for ] characters as it should never be passed a string containing [ or ] characters. This will force all characters after the % to be parsed as a non-negative base 10 integer, or rejected.

Additionally, modify the private InetAddress::getAllByName static method to handle length 4 byte arrays returned by IPAddressUtil::textToNumericFormatV4 and IPAddressUtil::textToNumericFormatV6 differently, such that those returned by the latter do not contain any % characters.

Additionally, or alternatively to the above remediations, consider reimplementing the entire public InetAddress::{getAllByName,getByName} interface along the lines of the Android implementation, which parses IP addresses extremely strictly, and allows interface name IPv6 scoped zone identifiers only for link-local addresses.234567 It is worth noting that the Android implementation additionally validates interface name IPv6 scoped zone identifiers against the system network interfaces,8 such a construction is, while not invalid per the InetAddress and Inet6Address Java documentation, arguably not in the spirit of them either as these APIs are intended for general-purpose IP address operations, including address representations that do not necessarily refer to the interfaces of the host operating on them. Instead, consider introducing an additional API for the InetAddress class whereby a getAllByName or getByName operation is performed with such additional, host-specific validation.

Developer Recommendation

Ensure that hostname and IP address values are handled securely and output-encoded or sanitized in a context appropriate manner. Do not rely on methods such as InetAddress::getByName(String) or InetAddress::getAllByName(String) to validate or sanitize external inputs.

An example demonstrating vulnerable code relying on InetAddress::getByName(String) is included for reference:

Note: When run, an injection will occur in the ping(String) function, resulting in a file, /tmp/id2, being created with the output of the id program on Unix-based systems.

import java.net.InetAddress;

class Ping {
  public static boolean validateHost(String host) {
    try {
      InetAddress address = InetAddress.getByName(host);
    } catch (Throwable t) {
      return false;
    }
    return true;
  }

  public static int ping(String host) {
    try {
      Process p = new ProcessBuilder(
        "/bin/sh", "-c", "ping -c 1 '" + host + "'"
      ).start();
      p.waitFor();
      return p.exitValue();
    } catch (Throwable t) {
      t.printStackTrace();
      return -1;
    }
  }

  public static void test(String[] hosts) {
    for (String host : hosts) {
      System.out.println("  testing `" + host + "`:");
      boolean valid = validateHost(host);
      System.out.println("    valid?: " + valid);
      if (valid) {
        int retcode = ping(host);
        boolean reachable = 0 == retcode;
        System.out.println(
          "    reachable?: " + reachable + " (" + retcode + ")"
        );
      }
    }
  }

  public static void main(String[] argv) throws Throwable {
    String[] good_inputs = new String[]{
      "127.0.0.1", "wikipedia.org"
    };
    String[] bad_inputs = new String[]{
      "https://wikipedia.org", "127.0.0.1; id>/tmp/id"
    };
    String[] evil_inputs = new String[]{
      "::1%1]foo.bar baz'; id>/tmp/id2; exit '42"
    };
    System.out.println("testing good inputs: (these should work)");
    test(good_inputs);
    System.out.println("testing bad inputs: (these should not work)");
    test(bad_inputs);
    System.out.println("testing evil inputs: (these work, but shouldn't)");
    test(evil_inputs);
  }
}
$ java Ping
testing good inputs: (these should work)
  testing `127.0.0.1`:
    valid?: true
    reachable?: true (0)
  testing `wikipedia.org`:
    valid?: true
    reachable?: true (0)
testing bad inputs: (these should not work)
  testing `https://wikipedia.org`:
    valid?: false
  testing `127.0.0.1; id>/tmp/id`:
    valid?: false
testing evil inputs: (these work, but shouldn't)
  testing `::1%1]foo.bar baz'; id>/tmp/id2; exit '42`:
    valid?: true
    reachable?: false (42)

Vendor Communication

2/17/22: NCC Group disclosed vulnerability to the security email of the OpenJDK
         project, vuln-report@openjdk.java.net, using their PGP key.
2/17/22: NCC Group receives a reply from Oracle's Security Alerts team
         (secalert_us@oracle.com) indicating that they have received the
         disclosure and will get back to NCC Group on it.
2/18/22: The Oracle Security Alerts team emails NCC Group asking about
         NCC Group's 30 day disclosure policy and notes that they release
         "Critical Patch Updates 4 times in a year," and requests an extension
         to after the upcoming one on April 19, 2022 (i.e. the July 2022
         update).
2/19/22: NCC Group replies, indicating a willingness to wait until April 19th.
2/22/22: The Oracle Security Alerts team replies, thanking NCC Group for the
         extension.
2/24/22: NCC Group receives an automated status report email from the
         secalert_us@oracle.com issue tracker, with the description
         "Weak Parsing Logic in java.net.InetAddress and Related Classes" and a
         status of "Issue addressed in future release, backports in progress
         for supported releases, scheduled for a future CPU"
3/3/22:  The Oracle Security Alerts team replies indicating that they consider
         the vulnerability to be "Security-in-Depth issue", and additionally
         that "the CVSS score for this issue is zero." They state that it will
         be addressed in a future update and then that because they are locking
         down changes for the April update, they request an extension to
         "postpone the fix to July CPU, to allow more time for testing."
3/24/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Issue addressed in future
         release, backports in progress for supported releases, scheduled for a
         future CPU".
4/24/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Issue addressed in future
         release, backports in progress for supported releases, scheduled for a
         future CPU".
5/24/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Issue addressed in future
         release, backports in progress for supported releases, scheduled for a
         future CPU".
6/24/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Issue addressed in future
         release, backports in progress for supported releases, scheduled for a
         future CPU".
7/24/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Closed: Alert or CPU issued"
         and an additional note of "Addressed in: Pipeline for CPU".
8/11/22: NCC Group reviews the July 2022 CPU update
         (https://www.oracle.com/security-alerts/cpujul2022.html) and does not
         find any mention of the disclosed vulnerability. In further reviewing
         associated updates for Java 8 (8u341), 11 (11.0.16), 17 (17.0.4), and
         18 (18.0.2), NCC Group identifies a change named "Update
         java.net.InetAddress to Detect Ambiguous IPv4 Address Literals" within
         the "Other Notes" sections, which refer to a non-public issue,
         "JDK-8277608" (https://bugs.openjdk.org/browse/JDK-8277608). NCC Group
         identifies and reviews the commit introducing the change to the public
         https://github.com/openjdk/jdk repository,
         `cdc1582d1d7629c2077f6cd19786d23323111018`, and determines that the
         vulnerability has not been fixed and that the commit appears
         unrelated, simply introducing a non-security relevant breaking change
         that disables alternate numerical textual representations of IP
         addresses, such as hexadecimal and octal radixes referred to as
         "BSD-style". This change causes IP address strings such as
         "0x7f.016.0.0xa" (127.14.0.10), "0x7f000001" (127.0.0.1), or
         "017700000001" (127.0.0.1) to be rejected by default unless the
         `-Djdk.net.allowAmbiguousIPAddressLiterals=true` option is passed to
         `java`. It should be noted that this validation does not restrict
         purely numeric text representations such as "2130706433" or
         "02130706433" (both parsed to 127.0.0.1). Single segment octal
         representations are restricted when they cannot be parsed into valid
         addresses as decimal. This is due to Java's longtime improper handling
         of octal-based IP addresses, which requires at least one segment to be
         larger than the maximum value when parsed as decimal to trigger an
         octal parse. Due to this, octal-based IP addressed are often parsed
         as decimal by Java.
8/12/22: NCC Group emails both the secalert_us@oracle.com and
         vuln-report@openjdk.java.net lists asking for the current timeline for
         the resolution of the issue, and provides the internal issue tracker
         ID. In the email, NCC Group includes a brief analysis of the "Update
         java.net.InetAddress to Detect Ambiguous IPv4 Address Literals"
         change, stating that it does not appear to be related to the disclosed
         vulnerability, which is still active in the updated releases of Java.
         Lastly, NCC Group states their intention to publish an advisory with
         with guidance for developers instead of waiting for a later CPU to
         resolve the vulnerability as the Oracle Security Alerts team had rated
         it with a CVSS score of 0.
9/14/22: The Oracle Security Alerts team replies to the previous email
         informing NCC Group and the vuln-report@openjdk.java.net list that
         they "revisited the original report and learned that the issue
         reported was not addressed by the fixes released in the July CPU."
         They also stated that it was "too late to the fix into the upcoming
         2022 October CPU", and that they were "targeting the fix for the 2023
         January CPU." They additionally sought to determine if NCC Group would
         delay disclosure until after the January CPU was published.
9/22/22: NCC Group replies to Oracle Security Alerts team and the
         vuln-report@openjdk.java.net list that waiting another 4-5 months far
         exceeds our disclosure policy. NCC Group also states their intention
         to publish an advisory on Sept 26, 2022, so that developers can
         mitigate the vulnerability within their codebases without an upstream
         patch.
9/23/22: The Oracle Security Alerts team replies on the thread thanking
         NCC Group for informing them of the decision to publish an advisory.
9/23/22: NCC Group receives an automated issue tracker update email from
         secalert_us@oracle.com with a status of "Under investigation / Being
         addressed in future and supported releases".
9/23/22: Late in the day, the Oracle Security Alerts team replies to
         NCC Group's most recent email, requesting additional time until
         noon PT on Wednesday, Sept 28, 2022, so that they can "work on a plan
         to get the fix into Oct CPU".
9/26/22: Early in the morning, NCC Group North America CTO, Dave Goldsmith,
         replies, stating that NCC Group tries "our best to work positively
         with vendors when disclosing vulnerabilities," and "that we've been
         pretty flexible" in handling the disclosure for this vulnerability.
         He offers an extension until Wednesday, Sept 28, 2022, at noon PT,
         but requests that the Oracle Security Alerts team re-evaluates the
         0.0 CVSS score of the vulnerability, as, if that remains Oracle's
         calculation, "then we don’t think it will be contentious to publish
         without a patch."
9/28/22: At 10:40am PT, the Oracle Security Alerts team replies stating that
         they "have confirmed the issue based on the report that was
         submitted" and that "the issue is a client side issue and there are
         ample best practices on input validation." They include reference
         links to an Oracle secure coding guide for Java
         (https://www.oracle.com/java/technologies/javase/seccodeguide.html),
         and the OWASP Top 10 entry on injection vulnerabilities
         (https://owasp.org/Top10/A03_2021-Injection/), the latter of which
         states the following as its second bullet on prevention: "Use positive
         server-side input validation. This is not a complete defense".
         Additionally, the Oracle Security Alerts requested a proof-of-concept
         demonstrating a server-side attack to recalculate the CVSS score.
         However, the reply did not contain any mention of "a plan to to get
         the fix into Oct CPU" per the Oracle Security Alerts team's 9/23/22
         email. It should be noted that NCC Group considers this vulnerability
         to impact code that takes untrusted hostname strings as input, a group
         that primarily includes server-side applications and services, as it
         enables a trivial bypass to official Java input validation routines
         used to protect against injection-type issues.
10/4/22: NCC Group North America CTO, Dave Goldsmith, replies, providing
         examples of how server-side input validation based on the vulnerable
         API would result in server-side systems being exploitable, including
         an example of such vulnerable code implementing input validation for
         `ping`. He requests a clear answer from the Oracle Security Alerts on
         both re-evaluating the CVSS score given the provided examples, and a
         commitment to fix the vulnerability in the October CPU. He
         additionally states that if both are provided by close of business on
         Wednesday, October 5, 2022, NCC Group will hold off on publishing the
         advisory until the October CPU is published; otherwise, the advisory
         will be published on Thursday, October 6, 2022.
10/6/22: The Oracle Security Alerts team replies at 12:10am PT, stating that
         their "evaluation is that this is an input validation issue and we are
         scoring it as a CVSS 0" and that "[a]s mentioned earlier we are
         targeting to release defense-in-depth fixes in the January 2022
         Critical Patch Update."
10/6/22: NCC Group publishes this security advisory.
10/6/22: NCC Group replies on the thread informing the Oracle Security Alerts
         team and the vuln-report@openjdk.java.net list that the advisory has
         been published.

Thanks to

Jennifer Fernick and Dave Goldsmith for their support throughout the disclosure process.

About NCC Group

NCC Group is a global expert in cyber security and risk mitigation, working with businesses to protect their brand, value and reputation against the ever-evolving threat landscape.

With our knowledge, experience and global footprint, we are best placed to help businesses identify, assess, mitigate and respond to the risks they face.

We are passionate about making the Internet safer and revolutionizing the way in which organizations think about cybersecurity.


  1. https://github.com/openjdk/jdk/blob/jdk-17+35/src/java.base/share/classes/java/net/InetAddress.java#L1261↩︎

  2. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:libcore/ojluni/src/main/java/java/net/InetAddress.java;l=1148↩︎

  3. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:libcore/ojluni/src/main/java/java/net/Inet6AddressImpl.java;l=90↩︎

  4. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:libcore/ojluni/src/main/java/java/net/Inet6AddressImpl.java;l=113;bpv=1;bpt=1↩︎

  5. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:bionic/libc/dns/net/getaddrinfo.c;l=586↩︎

  6. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:bionic/libc/dns/net/getaddrinfo.c;l=1015↩︎

  7. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:bionic/libc/dns/net/getaddrinfo.c;l=1245↩︎

  8. https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:bionic/libc/bionic/net_if.cpp;drc=android-12.0.0_r31;l=56↩︎