Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SWG-7516 utilizing safeURLResolver in swagger-parser-v3 #1911

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'