From 224037e33ed65d423324f366a060181f61b958e6 Mon Sep 17 00:00:00 2001
From: Milosz Tarka <milosz.tarka.it@gmail.com>
Date: Wed, 3 Jan 2024 15:21:04 +0100
Subject: [PATCH] SWG-9288 utilizing safe url resolver for OAS 3.0 parsing

---
 .../io/swagger/v3/parser/ResolverCache.java   | 17 ++++
 .../parser/test/OAI31DeserializationTest.java | 10 +-
 .../v3/parser/test/OpenAPIV3ParserTest.java   | 95 +++++++++++++++++++
 .../oas30SafeUrlResolvingWithLocalhost.yaml   | 25 +++++
 .../oas30SafeUrlResolvingWithPetstore.yaml    | 25 +++++
 .../oas31SafeUrlResolvingWithLocalhost.yaml}  |  0
 .../oas31SafeUrlResolvingWithPetstore.yaml}   |  0
 7 files changed, 167 insertions(+), 5 deletions(-)
 create mode 100644 modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml
 create mode 100644 modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithPetstore.yaml
 rename modules/swagger-parser-v3/src/test/resources/{3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml => safeResolving/oas31SafeUrlResolvingWithLocalhost.yaml} (100%)
 rename modules/swagger-parser-v3/src/test/resources/{3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml => safeResolving/oas31SafeUrlResolvingWithPetstore.yaml} (100%)

diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java
index 382ede57be..73cfb051c6 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java
@@ -18,6 +18,8 @@
 import io.swagger.v3.parser.core.models.SwaggerParseResult;
 import io.swagger.v3.parser.models.RefFormat;
 import io.swagger.v3.parser.models.RefType;
+import io.swagger.v3.parser.urlresolver.PermittedUrlsChecker;
+import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException;
 import io.swagger.v3.parser.util.DeserializationUtils;
 import io.swagger.v3.parser.util.PathUtils;
 import io.swagger.v3.parser.util.RefUtils;
@@ -146,6 +148,10 @@ public <T> T loadRef(String ref, RefFormat refFormat, Class<T> expectedType) {
         String contents = externalFileCache.get(file);
 
         if (contents == null) {
+            if(parseOptions.isSafelyResolveURL()){
+                checkUrlIsPermitted(file);
+            }
+
             if(parentDirectory != null) {
                 contents = RefUtils.readExternalRef(file, refFormat, auths, parentDirectory);
             }
@@ -374,6 +380,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 e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
     public boolean hasReferencedKey(String modelKey) {
         if(referencedModelKeys == null) {
             return false;
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 ff515a16b3..91c14ec031 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
@@ -1045,7 +1045,7 @@ public void test31SafeURLResolving() {
         parseOptions.setRemoteRefAllowList(allowList);
         parseOptions.setRemoteRefBlockList(blockList);
 
-        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas31SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
         if (result.getMessages() != null) {
             for (String message : result.getMessages()) {
                 assertTrue(message.contains("Server returned HTTP response code: 403"));
@@ -1063,7 +1063,7 @@ public void test31SafeURLResolvingWithBlockedURL() {
         parseOptions.setRemoteRefAllowList(allowList);
         parseOptions.setRemoteRefBlockList(blockList);
 
-        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas31SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
 
         if (result.getMessages() != null) {
             for (String message : result.getMessages()) {
@@ -1084,7 +1084,7 @@ public void test31SafeURLResolvingWithTurnedOffSafeResolving() {
         parseOptions.setRemoteRefAllowList(allowList);
         parseOptions.setRemoteRefBlockList(blockList);
 
-        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas31SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
         if (result.getMessages() != null) {
             for (String message : result.getMessages()) {
                 assertTrue(message.contains("Server returned HTTP response code: 403"));
@@ -1098,7 +1098,7 @@ public void test31SafeURLResolvingWithLocalhostAndBlockedURL() {
         parseOptions.setResolveFully(true);
         parseOptions.setSafelyResolveURL(true);
 
-        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml", null, parseOptions);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas31SafeUrlResolvingWithLocalhost.yaml", null, parseOptions);
         if (result.getMessages() != null) {
             for (String message : result.getMessages()) {
                 assertTrue(
@@ -1117,7 +1117,7 @@ public void test31SafeURLResolvingWithLocalhost() {
         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);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas31SafeUrlResolvingWithLocalhost.yaml", null, parseOptions);
 
         if (result.getMessages() != null) {
             for (String message : result.getMessages()) {
diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java
index 8f4141292f..01f69e1b0c 100644
--- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java
+++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java
@@ -3162,4 +3162,99 @@ public void testIssue_1746_headers_relative_paths() {
         OpenAPI openAPI = parseResult.getOpenAPI();
         assertEquals(openAPI.getPaths().get("/pets").getGet().getResponses().get("200").getHeaders().get("x-next").get$ref(), "#/components/headers/LocationInHeaders");
     }
+
+    @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("safeResolving/oas30SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
+        if (result.getMessages() != null) {
+            for (String message : result.getMessages()) {
+                assertTrue(message.contains("Server returned HTTP response code: 403"));
+            }
+        }
+    }
+
+    @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);
+
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("safeResolving/oas30SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
+
+        if (result.getMessages() != null) {
+            for (String message : result.getMessages()) {
+                assertTrue(
+                        message.contains("Server returned HTTP response code: 403") ||
+                                message.contains("URL is part of the explicit denylist. URL [https://petstore3.swagger.io/api/v3/openapi.json]"));
+            }
+        }
+    }
+
+    @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("safeResolving/oas30SafeUrlResolvingWithPetstore.yaml", null, parseOptions);
+        if (result.getMessages() != null) {
+            for (String message : result.getMessages()) {
+                assertTrue(message.contains("Server returned HTTP response code: 403"));
+            }
+        }
+    }
+
+    @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("safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml", null, parseOptions);
+        if (result.getMessages() != null) {
+            for (String message : result.getMessages()) {
+                assertTrue(
+                        message.contains("Server returned HTTP response code: 403") ||
+                                message.contains("IP is restricted"));
+            }
+        }
+    }
+
+    @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("safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml", null, parseOptions);
+
+        if (result.getMessages() != null) {
+            for (String message : result.getMessages()) {
+                assertTrue(
+                        message.contains("Server returned HTTP response code: 403") ||
+                                message.contains("IP is restricted") ||
+                                message.contains(error)
+                );
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml b/modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml
new file mode 100644
index 0000000000..3c217a5072
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithLocalhost.yaml
@@ -0,0 +1,25 @@
+openapi: 3.0.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/safeResolving/oas30SafeUrlResolvingWithPetstore.yaml b/modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithPetstore.yaml
new file mode 100644
index 0000000000..03c4fc33db
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/safeResolving/oas30SafeUrlResolvingWithPetstore.yaml
@@ -0,0 +1,25 @@
+openapi: 3.0.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'
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/safeResolving/oas31SafeUrlResolvingWithLocalhost.yaml
similarity index 100%
rename from modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml
rename to modules/swagger-parser-v3/src/test/resources/safeResolving/oas31SafeUrlResolvingWithLocalhost.yaml
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/safeResolving/oas31SafeUrlResolvingWithPetstore.yaml
similarity index 100%
rename from modules/swagger-parser-v3/src/test/resources/3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml
rename to modules/swagger-parser-v3/src/test/resources/safeResolving/oas31SafeUrlResolvingWithPetstore.yaml