diff --git a/modules/swagger-parser-safe-url-resolver/README.md b/modules/swagger-parser-safe-url-resolver/README.md
new file mode 100644
index 0000000000..d6bbc29462
--- /dev/null
+++ b/modules/swagger-parser-safe-url-resolver/README.md
@@ -0,0 +1,71 @@
+# Swagger Parser Safe URL Resolver
+
+The `swagger-parser-safe-url-resolver` is a library used for verifying that the hostname of URLs does not resolve to a private/restricted IPv4/IPv6 address range.
+This library can be used in services that deal with user-submitted URLs that get fetched (like in swagger-parser when resolving external URL $refs) to protect against Server-Side Request Forgery and DNS rebinding attacks.
+
+## How does it work?
+The main class of the package is the `PermittedUrlsChecker` which has one method: `verify(String url)`.
+This method takes in a string URL and performs the following steps:
+
+1. Gets the hostname portion from the URL
+2. Resolves the hostname to an IP address
+3. Checks if that IP address is in a private/restricted IP address range (and throws an exception if it is)
+4. Returns a `ResolvedUrl` object which contains
+ 4.1. `String url` where the original URL has the hostname replaced with the IP address
+ 4.2. A `String hostHeader` which contains the hostname from the original URL to be added as a host header
+
+This behavior can also be customized with the allowlist and denylist in the constructor, whereby:
+
+- An entry in the allowlist will allow the URL to pass even if it resolves to a private/restricted IP address
+- An entry in the denylist will throw an exception even when the URL resolves to a public IP address
+
+## Installation
+Add the following to you `pom.xml` file under `dependencies`
+```xml
+
+ io.swagger.parser.v3
+ swagger-parser-safe-url-resolver
+ // version of swagger-parser being used
+ 2.1.14
+
+```
+
+## Example usage
+
+```java
+import io.swagger.v3.parser.urlresolver.PermittedUrlsChecker;
+import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException;
+import io.swagger.v3.parser.urlresolver.models.ResolvedUrl;
+
+import java.util.List;
+
+public class Main {
+ public static void main() {
+ List allowlist = List.of("mysite.local");
+ List denylist = List.of("*.example.com:443");
+ var checker = new PermittedUrlsChecker(allowlist, denylist);
+
+ try {
+ // Will throw a HostDeniedException as `localhost`
+ // resolves to local IP and is not in allowlist
+ checker.verify("http://localhost/example");
+
+ // Will return a ResolvedUrl if `github.com`
+ // resolves to a public IP
+ checker.verify("https://github.com/swagger-api/swagger-parser");
+
+ // Will throw a HostDeniedException as `*.example.com` is
+ // explicitly deny listed, even if it resolves to public IP
+ checker.verify("https://subdomain.example.com/somepage");
+
+ // Will return a `ResolvedUrl` as `mysite.local`
+ // is explicitly allowlisted
+ ResolvedUrl resolvedUrl = checker.verify("http://mysite.local/example");
+ System.out.println(resolvedUrl.getUrl()); // "http://127.0.0.1/example"
+ System.out.println(resolvedUrl.getHostHeader()); // "mysite.local"
+ } catch (HostDeniedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+```
\ No newline at end of file
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..838c1d7dd8
--- /dev/null
+++ b/modules/swagger-parser-safe-url-resolver/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ io.swagger
+ swagger-parser-project
+ 1.0.69-SNAPSHOT
+ ../..
+
+
+
+ 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..7fb5ca2b50
--- /dev/null
+++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/PermittedUrlsChecker.java
@@ -0,0 +1,106 @@
+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) {
+ if(allowlist != null) {
+ this.allowlistMatcher = new UrlPatternMatcher(allowlist);
+ } else {
+ this.allowlistMatcher = new UrlPatternMatcher(Collections.emptyList());
+ }
+
+ if(denylist != null) {
+ this.denylistMatcher = new UrlPatternMatcher(denylist);
+ } else {
+ this.denylistMatcher = new UrlPatternMatcher(Collections.emptyList());
+ }
+ }
+
+ 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/modules/swagger-parser/pom.xml b/modules/swagger-parser/pom.xml
index e7e5239f11..fd3018db63 100644
--- a/modules/swagger-parser/pom.xml
+++ b/modules/swagger-parser/pom.xml
@@ -87,5 +87,10 @@
+
+ io.swagger
+ swagger-parser-safe-url-resolver
+ ${project.version}
+
diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java b/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java
index e41f200199..2593a88454 100644
--- a/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java
+++ b/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java
@@ -13,10 +13,9 @@
import io.swagger.models.properties.RefProperty;
import io.swagger.models.refs.RefFormat;
import io.swagger.models.refs.RefType;
-import io.swagger.parser.util.DeserializationUtils;
-import io.swagger.parser.util.PathUtils;
-import io.swagger.parser.util.RefUtils;
-import io.swagger.parser.util.SwaggerDeserializer;
+import io.swagger.parser.util.*;
+import io.swagger.v3.parser.urlresolver.PermittedUrlsChecker;
+import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
@@ -53,6 +52,7 @@ public class ResolverCache {
private final Path parentDirectory;
private final String parentUrl;
private final String rootPath;
+ private final ParseOptions parseOptions;
private Map resolutionCache = new HashMap<>();
private Map externalFileCache = new HashMap<>();
private Set referencedModelKeys = new HashSet<>();
@@ -63,9 +63,14 @@ public class ResolverCache {
private Map renameCache = new ConcurrentHashMap<>();
public ResolverCache(Swagger swagger, List auths, String parentFileLocation) {
+ this(swagger, auths, parentFileLocation, new ParseOptions());
+ }
+
+ public ResolverCache(Swagger swagger, List auths, String parentFileLocation, ParseOptions parseOptions) {
this.swagger = swagger;
this.auths = auths;
this.rootPath = parentFileLocation;
+ this.parseOptions = parseOptions;
if(parentFileLocation != null) {
if(parentFileLocation.startsWith("http")) {
@@ -115,6 +120,10 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) {
String contents = externalFileCache.get(file);
if (contents == null) {
+ if(parseOptions.isSafelyResolveURL()){
+ checkUrlIsPermitted(file);
+ }
+
if(parentDirectory != null) {
contents = RefUtils.readExternalRef(file, refFormat, auths, parentDirectory);
}
@@ -291,6 +300,17 @@ private Object getFromMap(String ref, Map map, Pattern pattern) {
return null;
}
+ protected void checkUrlIsPermitted(String refSet) {
+ try {
+ PermittedUrlsChecker permittedUrlsChecker = new PermittedUrlsChecker(parseOptions.getRemoteRefAllowList(),
+ parseOptions.getRemoteRefBlockList());
+
+ permittedUrlsChecker.verify(refSet);
+ } catch (HostDeniedException exception) {
+ throw new RuntimeException(exception.getMessage());
+ }
+ }
+
public boolean hasReferencedKey(String modelKey) {
if(referencedModelKeys == null) {
return false;
diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java
index 3a8291f464..dd7f1afe9b 100644
--- a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java
+++ b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java
@@ -87,7 +87,7 @@ public Swagger read(String location, List auths, ParseOption
if (output != null) {
if (options != null) {
if (options.isResolve()) {
- output = new SwaggerResolver(output, auths, location).resolve();
+ output = new SwaggerResolver(output, auths, location, null, options).resolve();
}
if (options.isFlatten()) {
InlineModelResolver inlineModelResolver = new InlineModelResolver();
@@ -194,7 +194,7 @@ public Swagger read(JsonNode node, List authorizationValues,
if (output != null) {
if (options != null) {
if (options.isResolve()) {
- output = new SwaggerResolver(output, authorizationValues).resolve();
+ output = new SwaggerResolver(output, authorizationValues, null, null, options).resolve();
}
if (options.isFlatten()) {
InlineModelResolver inlineModelResolver = new InlineModelResolver();
diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java
index 316a87d3ff..536339ff98 100644
--- a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java
+++ b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java
@@ -11,6 +11,7 @@
import io.swagger.parser.processors.OperationProcessor;
import io.swagger.parser.processors.ParameterProcessor;
import io.swagger.parser.processors.PathsProcessor;
+import io.swagger.parser.util.ParseOptions;
import java.util.Arrays;
import java.util.List;
@@ -40,9 +41,13 @@ public SwaggerResolver(Swagger swagger, List auths, String p
}
public SwaggerResolver(Swagger swagger, List auths, String parentFileLocation, Settings settings) {
+ this(swagger, auths, parentFileLocation, settings, new ParseOptions());
+ }
+
+ public SwaggerResolver(Swagger swagger, List auths, String parentFileLocation, Settings settings, ParseOptions parseOptions) {
this.swagger = swagger;
this.settings = settings != null ? settings : new Settings();
- this.cache = new ResolverCache(swagger, auths, parentFileLocation);
+ this.cache = new ResolverCache(swagger, auths, parentFileLocation, parseOptions);
definitionsProcessor = new DefinitionsProcessor(cache, swagger);
pathProcessor = new PathsProcessor(cache, swagger, this.settings);
operationsProcessor = new OperationProcessor(cache, swagger);
diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java
index 64129f58cf..64e9946a9d 100644
--- a/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java
+++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java
@@ -1,8 +1,13 @@
package io.swagger.parser.util;
+import java.util.List;
+
public class ParseOptions {
private boolean resolve;
private boolean flatten;
+ private boolean safelyResolveURL;
+ private List remoteRefAllowList;
+ private List remoteRefBlockList;
public boolean isResolve() {
return resolve;
@@ -15,4 +20,28 @@ public void setResolve(boolean resolve) {
public boolean isFlatten() { return flatten; }
public void setFlatten(boolean flatten) { this.flatten = flatten; }
+
+ public boolean isSafelyResolveURL() {
+ return safelyResolveURL;
+ }
+
+ public void setSafelyResolveURL(boolean safelyResolveURL) {
+ this.safelyResolveURL = safelyResolveURL;
+ }
+
+ public List getRemoteRefAllowList() {
+ return remoteRefAllowList;
+ }
+
+ public void setRemoteRefAllowList(List remoteRefAllowList) {
+ this.remoteRefAllowList = remoteRefAllowList;
+ }
+
+ public List getRemoteRefBlockList() {
+ return remoteRefBlockList;
+ }
+
+ public void setRemoteRefBlockList(List remoteRefBlockList) {
+ this.remoteRefBlockList = remoteRefBlockList;
+ }
}
diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java
index 17b7767095..610bce9666 100644
--- a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java
+++ b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java
@@ -3,6 +3,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@@ -1790,4 +1791,89 @@ public void testInlineModelResolverByLocation() {
assertTrue(userAddress.getProperties().containsKey("city"));
assertTrue(userAddress.getProperties().containsKey("street"));
}
+
+ @Test(description = "Test safe resolving")
+ public void test20SafeURLResolving() throws IOException {
+ String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8");
+ JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class);
+
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ parseOptions.setSafelyResolveURL(true);
+ List allowList = Collections.emptyList();
+ List blockList = Collections.emptyList();
+ parseOptions.setRemoteRefAllowList(allowList);
+ parseOptions.setRemoteRefBlockList(blockList);
+
+ new SwaggerParser().read(jsonNodeSwagger, null, parseOptions);
+ }
+
+ @Test(description = "Test safe resolving with blocked URL")
+ public void test20SafeURLResolvingWithBlockedURL() throws IOException {
+ String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8");
+ JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class);
+
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ parseOptions.setSafelyResolveURL(true);
+ List allowList = Collections.emptyList();
+ List blockList = Arrays.asList("petstore3.swagger.io");
+ parseOptions.setRemoteRefAllowList(allowList);
+ parseOptions.setRemoteRefBlockList(blockList);
+
+ assertThrows(RuntimeException.class, () -> {
+ new SwaggerParser().read(jsonNodeSwagger, null, parseOptions);
+ });
+ }
+
+ @Test(description = "Test safe resolving with turned off safelyResolveURL option")
+ public void test20SafeURLResolvingWithTurnedOffSafeResolving() throws IOException {
+ String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8");
+ JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class);
+
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(false);
+ parseOptions.setSafelyResolveURL(true);
+ List allowList = Collections.emptyList();
+ List blockList = Arrays.asList("petstore3.swagger.io");
+ parseOptions.setRemoteRefAllowList(allowList);
+ parseOptions.setRemoteRefBlockList(blockList);
+
+ new SwaggerParser().read(jsonNodeSwagger, null, parseOptions);
+ }
+
+ @Test(description = "Test safe resolving with localhost and blocked url")
+ public void test20SafeURLResolvingWithLocalhostAndBlockedURL() throws IOException {
+ String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml").toPath()), "UTF-8");
+ JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class);
+
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ parseOptions.setSafelyResolveURL(true);
+ List allowList = Collections.emptyList();
+ List blockList = Arrays.asList("petstore.swagger.io");
+ parseOptions.setRemoteRefAllowList(allowList);
+ parseOptions.setRemoteRefBlockList(blockList);
+
+ assertThrows(RuntimeException.class, () -> {
+ new SwaggerParser().read(jsonNodeSwagger, null, parseOptions);
+ }); }
+
+ @Test(description = "Test safe resolving with localhost url")
+ public void test20SafeURLResolvingWithLocalhost() throws IOException {
+ String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml").toPath()), "UTF-8");
+ JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class);
+
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ parseOptions.setSafelyResolveURL(true);
+ List allowList = Collections.emptyList();
+ List blockList = Collections.emptyList();
+ parseOptions.setRemoteRefAllowList(allowList);
+ parseOptions.setRemoteRefBlockList(blockList);
+
+ assertThrows(RuntimeException.class, () -> {
+ new SwaggerParser().read(jsonNodeSwagger, null, parseOptions);
+ });
+ }
}
diff --git a/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml
new file mode 100644
index 0000000000..60ffcef8ce
--- /dev/null
+++ b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml
@@ -0,0 +1,26 @@
+swagger: '2.0'
+info:
+ version: "1.0.0"
+ title: ssrf-test
+
+consumes:
+ - application/json
+produces:
+ - application/json
+paths:
+ /devices:
+ get:
+ operationId: getDevices
+ responses:
+ '200':
+ description: All the devices
+ schema:
+ $ref: 'http://localhost/example'
+ /pets:
+ get:
+ operationId: getPets
+ responses:
+ '200':
+ description: All the pets
+ schema:
+ $ref: 'https://petstore.swagger.io/v2/swagger.json'
\ No newline at end of file
diff --git a/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml
new file mode 100644
index 0000000000..2982fd96a6
--- /dev/null
+++ b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml
@@ -0,0 +1,26 @@
+swagger: '2.0'
+info:
+ version: "1.0.0"
+ title: ssrf-test
+
+consumes:
+ - application/json
+produces:
+ - application/json
+paths:
+ /devices:
+ get:
+ operationId: getDevices
+ responses:
+ '200':
+ description: All the devices
+ schema:
+ $ref: 'https://petstore3.swagger.io/api/v3/openapi.json'
+ /pets:
+ get:
+ operationId: getPets
+ responses:
+ '200':
+ description: All the pets
+ schema:
+ $ref: 'https://petstore.swagger.io/v2/swagger.json'
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 70d580b7c6..f440ff69bc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -139,7 +139,7 @@
3.6.2
true
-
+
UTF-8
1g
@@ -367,6 +367,7 @@
modules/swagger-parser
modules/swagger-compat-spec-parser
+ modules/swagger-parser-safe-url-resolver