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