Repository: swagger-api/swagger-parser
Pull request #1911 (2023-04-14 13:01:41)

SWG-7516 utilizing safeURLResolver in swagger-parser-v3
This PR makes swagger-parser-v3 to utilizes safeUrlResolver when resolving OpenAPI 3.1 definitions.

The reason behind this change is described deeply under the following PR:
https://github.com/swagger-api/swagger-parser/pull/1910

The $refs are checked in ReferenceVisitor class, in readHTTP method.



Patch:
diff --git a/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java b/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java
index a3d44843..adc2ddb3 100644
--- a/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java
+++ b/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java
@@ -1,5 +1,7 @@
 package io.swagger.v3.parser.core.models;
 
+import java.util.List;
+
 public class ParseOptions {
     private boolean resolve;
     private boolean resolveCombinators = true;
@@ -16,6 +18,10 @@ public class ParseOptions {
 
     private boolean oaiAuthor;
     private boolean inferSchemaType = true;
+    private boolean safelyResolveURL;
+    private List<String> remoteRefAllowList;
+    private List<String> remoteRefBlockList;
+
 
     public boolean isResolve() {
         return resolve;
@@ -131,4 +137,28 @@ public class ParseOptions {
     public void setInferSchemaType(boolean inferSchemaType) {
         this.inferSchemaType = inferSchemaType;
     }
+
+    public boolean isSafelyResolveURL() {
+        return safelyResolveURL;
+    }
+
+    public void setSafelyResolveURL(boolean safelyResolveURL) {
+        this.safelyResolveURL = safelyResolveURL;
+    }
+
+    public List<String> getRemoteRefAllowList() {
+        return remoteRefAllowList;
+    }
+
+    public void setRemoteRefAllowList(List<String> remoteRefAllowList) {
+        this.remoteRefAllowList = remoteRefAllowList;
+    }
+
+    public List<String> getRemoteRefBlockList() {
+        return remoteRefBlockList;
+    }
+
+    public void setRemoteRefBlockList(List<String> remoteRefBlockList) {
+        this.remoteRefBlockList = remoteRefBlockList;
+    }
 }
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
index 70b4198a..7fb5ca2b 100644
--- 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
@@ -23,8 +23,17 @@ public class PermittedUrlsChecker {
     }
 
     public PermittedUrlsChecker(List<String> allowlist, List<String> denylist) {
-        this.allowlistMatcher = new UrlPatternMatcher(allowlist);
-        this.denylistMatcher = new UrlPatternMatcher(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 {
diff --git a/modules/swagger-parser-v3/pom.xml b/modules/swagger-parser-v3/pom.xml
index 31750f28..33655ef8 100644
--- a/modules/swagger-parser-v3/pom.xml
+++ b/modules/swagger-parser-v3/pom.xml
@@ -26,6 +26,11 @@
             <artifactId>swagger-parser-core</artifactId>
             <version>${project.parent.version}</version>
         </dependency>
+        <dependency>
+            <groupId>io.swagger.parser.v3</groupId>
+            <artifactId>swagger-parser-safe-url-resolver</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.jmockit</groupId>
             <artifactId>jmockit</artifactId>
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer.java
index 45c4fc44..b939ef20 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer.java
@@ -1,5 +1,4 @@
 package io.swagger.v3.parser.reference;
-
 import java.util.Iterator;
 
 public interface OpenAPIDereferencer {
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java
index 197ccbd6..7c349f5d 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java
@@ -69,7 +69,7 @@ public class OpenAPIDereferencer31 implements OpenAPIDereferencer {
                 .auths(context.getAuths());
 
         Traverser traverser = buildTraverser(context);
-        Visitor referenceVisitor = buildReferenceVisitor(context, reference, traverser);
+        ReferenceVisitor referenceVisitor = buildReferenceVisitorWithContext(context, reference, traverser);
         try {
             openAPI = traverser.traverse(context.getOpenApi(), referenceVisitor);
         } catch (Exception e){
@@ -80,6 +80,7 @@ public class OpenAPIDereferencer31 implements OpenAPIDereferencer {
         if (openAPI == null) {
             return;
         }
+
         result.setOpenAPI(openAPI);
         result.getMessages().addAll(reference.getMessages());
     }
@@ -91,4 +92,9 @@ public class OpenAPIDereferencer31 implements OpenAPIDereferencer {
     public Visitor buildReferenceVisitor(DereferencerContext context, Reference reference, Traverser traverser) {
         return new ReferenceVisitor(reference, (OpenAPI31Traverser)traverser, new HashSet<>(), new HashMap<>());
     }
+
+    public ReferenceVisitor buildReferenceVisitorWithContext(DereferencerContext context, Reference reference, Traverser traverser) {
+        return new ReferenceVisitor(reference, (OpenAPI31Traverser)traverser, new HashSet<>(), new HashMap<>(), context);
+    }
+
 }
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
index 9b9a6561..eb327e4f 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
@@ -14,6 +14,9 @@ import io.swagger.v3.oas.models.parameters.RequestBody;
 import io.swagger.v3.oas.models.responses.ApiResponse;
 import io.swagger.v3.oas.models.security.SecurityScheme;
 import io.swagger.v3.parser.core.models.AuthorizationValue;
+import io.swagger.v3.parser.urlresolver.PermittedUrlsChecker;
+import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException;
+import io.swagger.v3.parser.util.RemoteUrl;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.LoggerFactory;
 
@@ -31,6 +34,7 @@ public class ReferenceVisitor extends AbstractVisitor {
     protected HashMap<Object, Object> visitedMap;
     protected OpenAPI31Traverser openAPITraverser;
     protected Reference reference;
+    protected DereferencerContext context;
 
     public ReferenceVisitor(
             Reference reference,
@@ -41,6 +45,20 @@ public class ReferenceVisitor extends AbstractVisitor {
         this.openAPITraverser = openAPITraverser;
         this.visited = visited;
         this.visitedMap = visitedMap;
+        this.context = null;
+    }
+
+    public ReferenceVisitor(
+            Reference reference,
+            OpenAPI31Traverser openAPITraverser,
+            HashSet<Object> visited,
+            HashMap<Object, Object> visitedMap,
+            DereferencerContext context) {
+        this.reference = reference;
+        this.openAPITraverser = openAPITraverser;
+        this.visited = visited;
+        this.visitedMap = visitedMap;
+        this.context = context;
     }
 
     public String toBaseURI(String uri) throws Exception{
@@ -174,13 +192,21 @@ public class ReferenceVisitor extends AbstractVisitor {
         return resolveRef(header, header.get$ref(), Header.class, openAPITraverser::traverseHeader);
     }
 
+    @Override
+    public String readHttp(String uri, List<AuthorizationValue> auths) throws Exception {
+        if(context.getParseOptions().isSafelyResolveURL()){
+            checkUrlIsPermitted(uri);
+        }
+        return RemoteUrl.urlToString(uri, auths);
+    }
+
     public<T> T resolveRef(T visiting, String ref, Class<T> clazz, BiFunction<T, ReferenceVisitor, T> traverseFunction){
         try {
             Reference reference = toReference(ref);
             String fragment = ReferenceUtils.getFragment(ref);
             JsonNode node = ReferenceUtils.jsonPointerEvaluate(fragment, reference.getJsonNode(), ref);
             T resolved = openAPITraverser.deserializeFragment(node, clazz, ref, fragment, reference.getMessages());
-            ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap);
+            ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap, context);
             return traverseFunction.apply(resolved, visitor);
 
         } catch (Exception e) {
@@ -232,7 +258,7 @@ public class ReferenceVisitor extends AbstractVisitor {
             if (isAnchor) {
                 resolved.$anchor(null);
             }
-            ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap);
+            ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap, context);
             return openAPITraverser.traverseSchema(resolved, visitor, inheritedIds);
         } catch (Exception e) {
             LOGGER.error("Error resolving schema " + ref, e);
@@ -278,4 +304,11 @@ public class ReferenceVisitor extends AbstractVisitor {
     public JsonNode parse(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
         return deserializeIntoTree(readURI(absoluteUri, auths));
     }
+
+    protected void checkUrlIsPermitted(String refSet) throws HostDeniedException {
+        PermittedUrlsChecker permittedUrlsChecker = new PermittedUrlsChecker(context.getParseOptions().getRemoteRefAllowList(),
+                context.getParseOptions().getRemoteRefBlockList());
+
+        permittedUrlsChecker.verify(refSet);
+    }
 }


Test patch:
diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java
index 22c33482..64793660 100644
--- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java
+++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java
@@ -8,6 +8,8 @@ import io.swagger.v3.parser.core.models.ParseOptions;
 import io.swagger.v3.parser.core.models.SwaggerParseResult;
 import org.testng.annotations.Test;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import static org.testng.Assert.*;
@@ -987,4 +989,79 @@ public class OAI31DeserializationTest {
         Schema id = (Schema)result.getOpenAPI().getComponents().getSchemas().get("Rule").getProperties().get("id");
         assertEquals(id.getTypes().iterator().next(), "string");
     }
+
+    @Test(description = "Test safe resolving")
+    public void test31SafeURLResolving() {
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolveFully(true);
+        parseOptions.setSafelyResolveURL(true);
+        List<String> allowList = Collections.emptyList();
+        List<String> blockList = Collections.emptyList();
+        parseOptions.setRemoteRefAllowList(allowList);
+        parseOptions.setRemoteRefBlockList(blockList);
+
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+
+        assertTrue(result.getMessages().isEmpty());
+    }
+
+    @Test(description = "Test safe resolving with blocked URL")
+    public void test31SafeURLResolvingWithBlockedURL() {
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolveFully(true);
+        parseOptions.setSafelyResolveURL(true);
+        List<String> allowList = Collections.emptyList();
+        List<String> blockList = Arrays.asList("petstore3.swagger.io");
+        parseOptions.setRemoteRefAllowList(allowList);
+        parseOptions.setRemoteRefBlockList(blockList);
+
+        List<String> errorList = Arrays.asList("URL is part of the explicit denylist. URL [https://petstore3.swagger.io/api/v3/openapi.json]");
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+
+        assertEquals(result.getMessages(), errorList);
+        assertEquals(result.getMessages().size(), 1);
+    }
+
+    @Test(description = "Test safe resolving with turned off safelyResolveURL option")
+    public void test31SafeURLResolvingWithTurnedOffSafeResolving() {
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolveFully(true);
+        parseOptions.setSafelyResolveURL(false);
+        List<String> allowList = Collections.emptyList();
+        List<String> blockList = Arrays.asList("petstore3.swagger.io");
+        parseOptions.setRemoteRefAllowList(allowList);
+        parseOptions.setRemoteRefBlockList(blockList);
+
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+
+        assertTrue(result.getMessages().isEmpty());
+    }
+
+    @Test(description = "Test safe resolving with localhost and blocked url")
+    public void test31SafeURLResolvingWithLocalhostAndBlockedURL() {
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolveFully(true);
+        parseOptions.setSafelyResolveURL(true);
+
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml", null, parseOptions);
+
+        assertTrue(result.getMessages().get(0).contains("IP is restricted"));
+        assertEquals(result.getMessages().size(), 1);
+    }
+
+    @Test(description = "Test safe resolving with localhost url")
+    public void test31SafeURLResolvingWithLocalhost() {
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolveFully(true);
+        parseOptions.setSafelyResolveURL(true);
+        List<String> blockList = Arrays.asList("petstore.swagger.io");
+        parseOptions.setRemoteRefBlockList(blockList);
+
+        String error = "URL is part of the explicit denylist. URL [https://petstore.swagger.io/v2/swagger.json]";
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml", null, parseOptions);
+
+        assertTrue(result.getMessages().get(0).contains("IP is restricted"));
+        assertEquals(result.getMessages().get(1), error);
+        assertEquals(result.getMessages().size(), 2);
+    }
 }
diff --git a/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml b/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml
new file mode 100644
index 00000000..7c7c34b8
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml
@@ -0,0 +1,25 @@
+openapi: 3.1.0
+info:
+  version: "1.0.0"
+  title: ssrf-test
+paths:
+  /devices:
+    get:
+      operationId: getDevices
+      responses:
+        '200':
+          description: All the devices
+          content:
+            application/json:
+              schema:
+                $ref: 'http://localhost/example'
+  /pets:
+    get:
+      operationId: getPets
+      responses:
+        '200':
+          description: All the pets
+          content:
+            application/json:
+              schema:
+                $ref: 'https://petstore.swagger.io/v2/swagger.json'
\ No newline at end of file
diff --git a/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml b/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml
new file mode 100644
index 00000000..195008ff
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml
@@ -0,0 +1,25 @@
+openapi: 3.1.0
+info:
+  version: "1.0.0"
+  title: ssrf-test
+paths:
+  /devices:
+    get:
+      operationId: getDevices
+      responses:
+        '200':
+          description: All the devices
+          content:
+            application/json:
+              schema:
+                $ref: 'https://petstore3.swagger.io/api/v3/openapi.json'
+  /pets:
+    get:
+      operationId: getPets
+      responses:
+        '200':
+          description: All the pets
+          content:
+            application/json:
+              schema:
+                $ref: 'https://petstore.swagger.io/v2/swagger.json'