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 a3d448431b..adc2ddb308 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 remoteRefAllowList; + private List remoteRefBlockList; + public boolean isResolve() { return resolve; @@ -131,4 +137,28 @@ public boolean isInferSchemaType() { public void setInferSchemaType(boolean inferSchemaType) { this.inferSchemaType = inferSchemaType; } + + 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-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 70b4198ab2..7fb5ca2b50 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 PermittedUrlsChecker() { } public PermittedUrlsChecker(List allowlist, List 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 31750f28ae..33655ef85d 100644 --- a/modules/swagger-parser-v3/pom.xml +++ b/modules/swagger-parser-v3/pom.xml @@ -26,6 +26,11 @@ swagger-parser-core ${project.parent.version} + + io.swagger.parser.v3 + swagger-parser-safe-url-resolver + ${project.version} + org.jmockit jmockit 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 45c4fc4435..b939ef20a5 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 197ccbd650..7c349f5d3c 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 void dereference(DereferencerContext context, Iterator(), 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 9b9a656141..eb327e4f95 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.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 visitedMap; protected OpenAPI31Traverser openAPITraverser; protected Reference reference; + protected DereferencerContext context; public ReferenceVisitor( Reference reference, @@ -41,6 +45,20 @@ public ReferenceVisitor( this.openAPITraverser = openAPITraverser; this.visited = visited; this.visitedMap = visitedMap; + this.context = null; + } + + public ReferenceVisitor( + Reference reference, + OpenAPI31Traverser openAPITraverser, + HashSet visited, + HashMap 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 Header visitHeader(Header header){ return resolveRef(header, header.get$ref(), Header.class, openAPITraverser::traverseHeader); } + @Override + public String readHttp(String uri, List auths) throws Exception { + if(context.getParseOptions().isSafelyResolveURL()){ + checkUrlIsPermitted(uri); + } + return RemoteUrl.urlToString(uri, auths); + } + public T resolveRef(T visiting, String ref, Class clazz, BiFunction 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 Schema resolveSchemaRef(Schema visiting, String ref, List inherit 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 JsonNode deserializeIntoTree(String content) throws Exception { public JsonNode parse(String absoluteUri, List 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); + } } 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 22c3348262..6479366007 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.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 void test31Issue1821() { 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 allowList = Collections.emptyList(); + List 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 allowList = Collections.emptyList(); + List blockList = Arrays.asList("petstore3.swagger.io"); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + List 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 allowList = Collections.emptyList(); + List 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 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 0000000000..7c7c34b8d7 --- /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 0000000000..195008ffc2 --- /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'