diff --git a/modules/swagger-parser-safe-url-resolver/pom.xml b/modules/swagger-parser-safe-url-resolver/pom.xml new file mode 100644 index 0000000000..0fef7d069c --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + io.swagger.parser.v3 + swagger-parser-project + 2.1.14-SNAPSHOT + ../../pom.xml + + + swagger-parser-safe-url-resolver + + + + commons-io + commons-io + ${commons-io-version} + + + org.slf4j + slf4j-simple + ${slf4j-version} + test + + + org.testng + testng + ${testng-version} + test + + + junit + junit + ${junit-version} + test + + + org.jmockit + jmockit + ${jmockit-version} + test + + + \ No newline at end of file diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/PermittedUrlsChecker.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/PermittedUrlsChecker.java new file mode 100644 index 0000000000..70b4198ab2 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/PermittedUrlsChecker.java @@ -0,0 +1,97 @@ +package io.swagger.v3.parser.urlresolver; + +import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException; +import io.swagger.v3.parser.urlresolver.matchers.UrlPatternMatcher; +import io.swagger.v3.parser.urlresolver.models.ResolvedUrl; +import io.swagger.v3.parser.urlresolver.utils.NetUtils; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; + +public class PermittedUrlsChecker { + + protected final UrlPatternMatcher allowlistMatcher; + protected final UrlPatternMatcher denylistMatcher; + + public PermittedUrlsChecker() { + this.allowlistMatcher = new UrlPatternMatcher(Collections.emptyList()); + this.denylistMatcher = new UrlPatternMatcher(Collections.emptyList()); + } + + public PermittedUrlsChecker(List allowlist, List denylist) { + this.allowlistMatcher = new UrlPatternMatcher(allowlist); + this.denylistMatcher = new UrlPatternMatcher(denylist); + } + + public ResolvedUrl verify(String url) throws HostDeniedException { + URL parsed; + + try { + parsed = new URL(url); + } catch (MalformedURLException e) { + throw new HostDeniedException(String.format("Failed to parse URL. URL [%s]", url), e); + } + + if (!parsed.getProtocol().equals("http") && !parsed.getProtocol().equals("https")) { + throw new HostDeniedException(String.format("URL does not use a supported protocol. URL [%s]", url)); + } + + String hostname; + try { + hostname = NetUtils.getHostFromUrl(url); + } catch (MalformedURLException e) { + throw new HostDeniedException(String.format("Failed to get hostname from URL. URL [%s]", url), e); + } + + if (this.allowlistMatcher.matches(url)) { + return new ResolvedUrl(url, hostname); + } + + if (this.denylistMatcher.matches(url)) { + throw new HostDeniedException(String.format("URL is part of the explicit denylist. URL [%s]", url)); + } + + InetAddress ip; + try { + ip = NetUtils.getHostByName(hostname); + } catch (UnknownHostException e) { + throw new HostDeniedException( + String.format("Failed to resolve IP from hostname. Hostname [%s]", hostname), e); + } + + String urlWithIp; + try { + urlWithIp = NetUtils.setHost(url, ip.getHostAddress()); + } catch (MalformedURLException e) { + throw new HostDeniedException( + String.format("Failed to create new URL with IP. IP [%s] URL [%s]", ip.getHostAddress(), url), e); + } + + if (this.allowlistMatcher.matches(urlWithIp)) { + return new ResolvedUrl(urlWithIp, hostname); + } + + if (isRestrictedIpRange(ip)) { + throw new HostDeniedException(String.format("IP is restricted. URL [%s]", urlWithIp)); + } + + if (this.denylistMatcher.matches(urlWithIp)) { + throw new HostDeniedException(String.format("IP is part of the explicit denylist. URL [%s]", urlWithIp)); + } + + return new ResolvedUrl(urlWithIp, hostname); + } + + protected boolean isRestrictedIpRange(InetAddress ip) { + return ip.isLinkLocalAddress() + || ip.isSiteLocalAddress() + || ip.isLoopbackAddress() + || ip.isAnyLocalAddress() + || NetUtils.isUniqueLocalAddress(ip) + || NetUtils.isNAT64Address(ip); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/exceptions/HostDeniedException.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/exceptions/HostDeniedException.java new file mode 100644 index 0000000000..ed402001f0 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/exceptions/HostDeniedException.java @@ -0,0 +1,11 @@ +package io.swagger.v3.parser.urlresolver.exceptions; + +public class HostDeniedException extends Exception { + public HostDeniedException(String message) { + super(message); + } + + public HostDeniedException(String message, Throwable e) { + super(message, e); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcher.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcher.java new file mode 100644 index 0000000000..a0c70fcc2e --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcher.java @@ -0,0 +1,60 @@ +package io.swagger.v3.parser.urlresolver.matchers; + +import io.swagger.v3.parser.urlresolver.utils.NetUtils; + +import java.net.IDN; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.io.FilenameUtils.wildcardMatch; + +public class UrlPatternMatcher { + + private final List patterns; + + public UrlPatternMatcher(List patterns) { + this.patterns = new ArrayList<>(); + + patterns.forEach(pattern -> { + String patternLower = pattern.toLowerCase(); + String hostAndPort = pattern.contains(":") ? patternLower : patternLower + ":*"; + String[] split = hostAndPort.split(":"); + String host = Character.isDigit(split[0].charAt(0)) ? split[0] : IDN.toASCII(split[0], IDN.ALLOW_UNASSIGNED); + String port = split.length > 1 ? split[1] : "*"; + + // Ignore domains that end in a wildcard + if (host.length() > 1 && !NetUtils.isIPv4(host.replace("*", "0")) && host.endsWith("*")) { + return; + } + + this.patterns.add(String.format("%s:%s", host, port)); + }); + } + + public boolean matches(String url) { + URL parsed; + try { + parsed = new URL(url.toLowerCase()); + } catch (MalformedURLException e) { + return false; + } + + String host = IDN.toASCII(parsed.getHost(), IDN.ALLOW_UNASSIGNED); + String hostAndPort; + if (parsed.getPort() == -1) { + if (parsed.getProtocol().equals("http")) { + hostAndPort = host + ":80"; + } else if (parsed.getProtocol().equals("https")) { + hostAndPort = host + ":443"; + } else { + return false; + } + } else { + hostAndPort = host + ":" + parsed.getPort(); + } + + return this.patterns.stream().anyMatch(pattern -> wildcardMatch(hostAndPort, pattern)); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/models/ResolvedUrl.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/models/ResolvedUrl.java new file mode 100644 index 0000000000..b5c9bf39c4 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/models/ResolvedUrl.java @@ -0,0 +1,36 @@ +package io.swagger.v3.parser.urlresolver.models; + +public class ResolvedUrl { + + private String url; + private String hostHeader; + + public ResolvedUrl(String url, String hostHeader) { + this.url = url; + this.hostHeader = hostHeader; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHostHeader() { + return hostHeader; + } + + public void setHostHeader(String hostHeader) { + this.hostHeader = hostHeader; + } + + @Override + public String toString() { + return "ResolvedUrl{" + + "url='" + url + '\'' + + ", hostHeader='" + hostHeader + '\'' + + '}'; + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/utils/NetUtils.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/utils/NetUtils.java new file mode 100644 index 0000000000..7aa7088c77 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/utils/NetUtils.java @@ -0,0 +1,90 @@ +package io.swagger.v3.parser.urlresolver.utils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; + +public class NetUtils { + + private NetUtils() {} + + public static InetAddress getHostByName(String hostname) throws UnknownHostException { + return InetAddress.getByName(hostname); + } + + public static String getHostFromUrl(String url) throws MalformedURLException { + String hostnameOrIP = new URL(url).getHost(); + //IPv6 addresses in URLs are surrounded by square brackets + if (hostnameOrIP.length() > 2 && hostnameOrIP.startsWith("[") && hostnameOrIP.endsWith("]")) { + return hostnameOrIP.substring(1, hostnameOrIP.length() - 1); + } + return hostnameOrIP; + } + + public static String setHost(String url, String host) throws MalformedURLException { + URL parsed = new URL(url); + if (isIPv6(host)) { + return url.replace(parsed.getHost(), "[" + host + "]"); + } else { + return url.replace(parsed.getHost(), host); + } + } + + public static boolean isIPv4(String ipAddress) { + boolean isIPv4 = false; + + if (ipAddress != null) { + try { + InetAddress inetAddress = InetAddress.getByName(ipAddress); + isIPv4 = (inetAddress instanceof Inet4Address); + } catch (UnknownHostException ignored) { + return false; + } + } + + return isIPv4; + } + + public static boolean isIPv6(String ipAddress) { + boolean isIPv6 = false; + + if (ipAddress != null) { + try { + InetAddress inetAddress = InetAddress.getByName(ipAddress); + isIPv6 = (inetAddress instanceof Inet6Address); + } catch (UnknownHostException ignored) { + return false; + } + } + + return isIPv6; + } + + // Not picked up by Inet6Address.is*Address() checks + public static boolean isUniqueLocalAddress(InetAddress ip) { + // Only applies to IPv6 + if (ip instanceof Inet4Address) { + return false; + } + + byte[] address = ip.getAddress(); + return (address[0] & 0xff) == 0xfc || (address[0] & 0xff) == 0xfd; + } + + // Not picked up by Inet6Address.is*Address() checks + public static boolean isNAT64Address(InetAddress ip) { + // Only applies to IPv6 + if (ip instanceof Inet4Address) { + return false; + } + + byte[] address = ip.getAddress(); + return (address[0] & 0xff) == 0x00 + && (address[1] & 0xff) == 0x64 + && (address[2] & 0xff) == 0xff + && (address[3] & 0xff) == 0x9b; + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/PermittedUrlsCheckerTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/PermittedUrlsCheckerTest.java new file mode 100644 index 0000000000..4625cd719b --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/PermittedUrlsCheckerTest.java @@ -0,0 +1,283 @@ +package io.swagger.v3.parser.urlresolver; + +import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException; +import io.swagger.v3.parser.urlresolver.models.ResolvedUrl; +import io.swagger.v3.parser.urlresolver.utils.NetUtils; +import mockit.*; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + + +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; + +public class PermittedUrlsCheckerTest { + + private final List emptyAllowlist = Collections.emptyList(); + private final List emptyDenylist = Collections.emptyList(); + @Mocked + private NetUtils netUtils; + + private PermittedUrlsChecker checker; + + @BeforeMethod + void beforeMethod() { + this.checker = new PermittedUrlsChecker(Collections.emptyList(), Collections.emptyList()); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectPrivateSIITIPv4in6HostReferencesInABCDFormat() throws Exception { + String url = "https://[0:0:0:0:0:ffff:10.1.33.147]:8000/v1/operation?theThing=something"; + String expectedIp = "10.1.33.147"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test + public void shouldAllowPublicSIITIPv4in6HostReferencesInABCDFormat() throws Exception { + String url = "https://[0:0:0:0:0:ffff:1.2.3.4]:8000/v1/operation?theThing=something"; + String expectedIp = "1.2.3.4"; + String expectedUrl = "https://1.2.3.4:8000/v1/operation?theThing=something"; + String expectedHostHeader = "0:0:0:0:0:ffff:1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHostHeader; + NetUtils.getHostByName(expectedHostHeader); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), expectedUrl); + Assert.assertEquals(result.getHostHeader(), expectedHostHeader); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectPrivateSIITIPv4in6HostReferencesInIPv6Format() throws Exception { + String url = "https://[0:0:0:0:0:ffff:a01:219]:8000/v1/operation?theThing=something"; + String expectedIp = "10.1.2.25"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectNAT64HostReferences() throws Exception { + String url = "https://[64:ff9b::]:8000/v1/operation?theThing=something"; + String expectedIp = "64:ff9b:0:0:0:0:0:0"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + NetUtils.isNAT64Address(withInstanceOf(InetAddress.class)); times = 1; result = true; + }}; + + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectDecimalIPsThatResolveToLocalIPs() throws Exception { + String url = "https://3232235778:8000/api/v3/pet/findByStatus?status=available"; + String expectedIp = "192.168.1.2"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test + public void shouldPreferAllowlistOverEverythingElse() throws Exception { + String url = "https://localhost:3000/1"; + String expectedHostname = "localhost"; + List allowlist = Collections.singletonList("localhost"); + this.checker = new PermittedUrlsChecker(allowlist, emptyDenylist); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHostname; + }}; + + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), "https://localhost:3000/1"); + Assert.assertEquals(result.getHostHeader(), "localhost"); + } + + @Test + public void shouldAllowPublicDomainsByDefault() throws Exception { + String url = "https://smartbear.com:3000/1"; + String expectedUrl = "https://1.2.3.4:3000/1"; + String expectedHost = "smartbear.com"; + String expectedIp = "1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), expectedUrl); + Assert.assertEquals(result.getHostHeader(), expectedHost); + } + + @Test + public void shouldAllowPublicIPsByDefault() throws Exception { + String url = "https://1.2.3.4:3000/1"; + String expectedHost = "1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedHost); + NetUtils.setHost(url, expectedHost); times = 1; result = url; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), url); + Assert.assertEquals(result.getHostHeader(), expectedHost); + } + + @Test( + dataProvider = "shouldBlockRestrictedIPv4sByDefault", + expectedExceptions = HostDeniedException.class, + expectedExceptionsMessageRegExp = ".*IP is restricted.*" + ) + public void shouldBlockIPv4Localhost(String url, String expectedIp) throws Exception { + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @DataProvider(name = "shouldBlockRestrictedIPv4sByDefault") + private Object[][] shouldBlockRestrictedIPv4sByDefault() { + return new Object[][]{ + {"https://localhost:3000/1", "127.0.0.1"}, + {"https://127.0.0.1/", "127.0.0.1"}, + {"https://192.168.1.2/", "192.168.1.2"}, + {"https://127.3", "127.0.0.3"} + }; + } + + @Test( + dataProvider = "shouldBlockRestrictedIPv6sByDefault", + expectedExceptions = HostDeniedException.class, + expectedExceptionsMessageRegExp = ".*IP is restricted.*" + ) + public void shouldBlockIPv6Localhost(String url, String expectedIp) throws Exception { + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + NetUtils.isUniqueLocalAddress(withInstanceOf(InetAddress.class)); result = true; + }}; + + checker.verify(url); + } + + @DataProvider(name = "shouldBlockRestrictedIPv6sByDefault") + private Object[][] shouldBlockRestrictedIPv6sByDefault() { + return new Object[][]{ + {"https://[fc00::1]/", "fc00:0:0:0:0:0:0:1"}, + {"https://[fd00:ec2::254]/", "fd00:ec2:0:0:0:0:0:254"} + }; + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldBlockDomainNamesThatResolveToPrivateIPs() throws Exception { + String url = "https://evil.com"; + String expectedUrl = "https://192.168.1.1:3000/1"; + String expectedHost = "evil.com"; + String expectedIp = "192.168.1.1"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*URL is part of the explicit denylist.*") + public void shouldBlockSpecificallyDenylistedURLs() throws Exception { + String url = "https://smartbear.com"; + List denylist = Collections.singletonList("smartbear.com"); + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is part of the explicit denylist.*") + public void shouldBlockBasedOnResolvedIP() throws Exception { + String url = "https://smartbear.com"; + String expectedUrl = "https://1.2.3.4:3000/1"; + String expectedHost = "smartbear.com"; + String expectedIp = "1.2.3.4"; + List denylist = Collections.singletonList("1.2.3.4"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*URL is part of the explicit denylist.*") + public void shouldBlockURLMatchingWildcardPattern() throws Exception { + String url = "https://foo.example.com"; + String expectedHost = "foo.example.com"; + List denylist = Collections.singletonList("f*.example.com"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test + public void shouldAllowURLMatchingWildcardPattern() throws Exception { + String url = "https://foo.example.com"; + String expectedHost = "foo.example.com"; + List allowlist = Collections.singletonList("f*.example.com"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + }}; + + this.checker = new PermittedUrlsChecker(allowlist, emptyDenylist); + checker.verify(url); + } + +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcherTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcherTest.java new file mode 100644 index 0000000000..bce4a3e2cd --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/matchers/UrlPatternMatcherTest.java @@ -0,0 +1,129 @@ +package io.swagger.v3.parser.urlresolver.matchers; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; + +public class UrlPatternMatcherTest { + + @Test + public void returnsFalseWhenUrlCannotBeParsed() { + List patterns = Collections.emptyList(); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("not a url")); + } + + @Test + public void returnsFalseWhenUrlIsNotHttpOrHttps() { + List patterns = Collections.emptyList(); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("file://not a url")); + } + + @Test + public void domainWithoutPortMatchesAnyPort() { + List patterns = Collections.singletonList("example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("https://example.com")); + Assert.assertTrue(matcher.matches("http://example.com:12345")); + Assert.assertTrue(matcher.matches("https://example.com:12345")); + Assert.assertFalse(matcher.matches("https://not.example.com:12345")); + } + + @Test + public void domainWithPortMatchesOnlyThatPort() { + List patterns = Collections.singletonList("example.com:443"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("https://example.com")); + Assert.assertTrue(matcher.matches("http://example.com:443")); + Assert.assertFalse(matcher.matches("http://example.com:12345")); + Assert.assertFalse(matcher.matches("https://example.com:1234")); + Assert.assertFalse(matcher.matches("https://not.example.com:12345")); + } + + @Test + public void domainSupportsWildcards() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertFalse(matcher.matches("https://example.com")); + Assert.assertFalse(matcher.matches("https://fooexample.com")); + Assert.assertTrue(matcher.matches("https://foo.example.com")); + Assert.assertTrue(matcher.matches("https://foo.bar.example.com")); + } + + @Test + public void domainInUrlIsCaseInsensitive() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://ExAmPlE.CoM")); + Assert.assertTrue(matcher.matches("https://FoO.ExAmPlE.CoM")); + } + + @Test + public void domainInPatternIsCaseInsensitive() { + List patterns = Collections.singletonList("*.EXamPLe.Com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://ExAmPlE.CoM")); + Assert.assertTrue(matcher.matches("https://FoO.ExAmPlE.CoM")); + } + + @Test + public void supportForMatchingInternationalizedDomainNames() { + List patterns = Collections.singletonList("*.😋.local"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("http://blah.😋.local")); + Assert.assertTrue(matcher.matches("http://blah.xn--p28h.local")); + } + + @Test + public void domainsDoNotSupportWildcardsAtTheEnd() { + List patterns = Collections.singletonList("example.co*"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("https://example.net")); + Assert.assertFalse(matcher.matches("https://example.co.uk")); + Assert.assertFalse(matcher.matches("https://example.com")); + } + + @Test + public void ipAddressesSupportWildcardsAtTheEnd() { + List patterns = Collections.singletonList("10.100.*.*"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("http://10.100.1.2")); + Assert.assertFalse(matcher.matches("http://10.101.1.2")); + } + + @Test + public void worksWithUrlsWithAuthPathAndQueryComponents() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("https://foo:bar@example.com/path?q=1")); + Assert.assertTrue(matcher.matches("https://foo:bar@foo.example.com/path?q=1")); + } + + @Test + public void supportsIpAddressesInPatterns() { + List patterns = Collections.singletonList("1.*.3.4"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("https://foo:bar@1.2.3.4/path?q=1")); + Assert.assertFalse(matcher.matches("https://foo:bar@1.2.3.5/path?q=1")); + } + +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/utils/NetUtilsTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/utils/NetUtilsTest.java new file mode 100644 index 0000000000..6f533e85f6 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/v3/parser/urlresolver/utils/NetUtilsTest.java @@ -0,0 +1,138 @@ +package io.swagger.v3.parser.urlresolver.utils; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.UnknownHostException; + +public class NetUtilsTest { + + @Test + public void getHostFromUrlWithDomainNameShouldReturnHostname() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "example.com"); + } + + @Test + public void getHostFromUrlWithIPv4AddressShouldReturnIPAddress() throws MalformedURLException { + String url = "https://1.2.3.4/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "1.2.3.4"); + } + + @Test + public void getHostFromUrlWithIPv6AddressShouldReturnIPAddress() throws MalformedURLException { + String url = "https://[::1]/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "::1"); + } + + @Test + public void setHostShouldSetIPv4AddressInUrl() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + String ip = "1.2.3.4"; + + String result = NetUtils.setHost(url, ip); + + Assert.assertEquals(result, "https://1.2.3.4/hello?query=world"); + } + + @Test + public void setHostShouldSetIPv6AddressInUrlWithBrackets() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + String ip = "::1"; + + String result = NetUtils.setHost(url, ip); + + Assert.assertEquals(result, "https://[::1]/hello?query=world"); + } + + @Test + public void isIPv4WithIPv4AddressShouldReturnTrue() { + String ip = "1.2.3.4"; + + Assert.assertTrue(NetUtils.isIPv4(ip)); + } + + @Test + public void isIPv4WithIPv6AddressShouldReturnFalse() { + String ip = "::1"; + + Assert.assertFalse(NetUtils.isIPv4(ip)); + } + + @Test + public void isIPv6WithIPv6AddressShouldReturnTrue() { + String ip = "::1"; + + Assert.assertTrue(NetUtils.isIPv6(ip)); + } + + @Test + public void isIPv6WithIPv4AddressShouldReturnFalse() { + String ip = "1.2.3.4"; + + Assert.assertFalse(NetUtils.isIPv6(ip)); + } + + @Test + public void isIPv6WithImproperAddressShouldReturnFalse() { + String ip = "999.999.999.999"; + + Assert.assertFalse(NetUtils.isIPv6(ip)); + } + + @Test + public void isUniqueLocalAddressWithULAShouldReturnTrue() throws UnknownHostException { + InetAddress ulaIp = InetAddress.getByName("fc00::1"); + InetAddress ulaIpWithLBit = InetAddress.getByName("fd00:ec2::254"); + + Assert.assertTrue(NetUtils.isUniqueLocalAddress(ulaIp)); + Assert.assertTrue(NetUtils.isUniqueLocalAddress(ulaIpWithLBit)); + } + + @Test + public void isUniqueLocalAddressWithNonULAIPv6AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("::1"); + + Assert.assertFalse(NetUtils.isUniqueLocalAddress(ip)); + } + + @Test + public void isUniqueLocalAddressWithIPv4AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("1.2.3.4"); + + Assert.assertFalse(NetUtils.isUniqueLocalAddress(ip)); + } + + @Test + public void isNAT64WithNAT64AddressShouldReturnTrue() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("64:ff9b::"); + + Assert.assertTrue(NetUtils.isNAT64Address(ip)); + } + + @Test + public void isNAT64WithRegularIPv6AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("fc00::1"); + + Assert.assertFalse(NetUtils.isNAT64Address(ip)); + } + + @Test + public void isNAT64WithIPv4AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("1.2.3.4"); + + Assert.assertFalse(NetUtils.isNAT64Address(ip)); + } + +} diff --git a/pom.xml b/pom.xml index f908071414..780f734f85 100644 --- a/pom.xml +++ b/pom.xml @@ -381,6 +381,7 @@ modules/swagger-parser-v2-converter modules/swagger-parser modules/swagger-parser-cli + modules/swagger-parser-safe-url-resolver