Skip to content

Commit

Permalink
Merge pull request #1911 from swagger-api/SWG-7516-utilizing-safe-url…
Browse files Browse the repository at this point in the history
…-resolving-in-swagger-parser

SWG-7516 utilizing safeURLResolver in swagger-parser-v3
  • Loading branch information
MiloszTarka authored Apr 24, 2023
2 parents 2d8cceb + 2742c96 commit e5a23e8
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ public 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 {
Expand Down
5 changes: 5 additions & 0 deletions modules/swagger-parser-v3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
package io.swagger.v3.parser.reference;

import java.util.Iterator;

public interface OpenAPIDereferencer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public void dereference(DereferencerContext context, Iterator<OpenAPIDereference
.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){
Expand All @@ -80,6 +80,7 @@ public void dereference(DereferencerContext context, Iterator<OpenAPIDereference
if (openAPI == null) {
return;
}

result.setOpenAPI(openAPI);
result.getMessages().addAll(reference.getMessages());
}
Expand All @@ -91,4 +92,9 @@ public Traverser buildTraverser(DereferencerContext context) {
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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<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{
Expand Down Expand Up @@ -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<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) {
Expand Down Expand Up @@ -232,7 +258,7 @@ public Schema resolveSchemaRef(Schema visiting, String ref, List<String> 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);
Expand Down Expand Up @@ -278,4 +304,11 @@ public JsonNode deserializeIntoTree(String content) throws Exception {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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<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);
}
}
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit e5a23e8

Please sign in to comment.