Skip to content

Commit e5a23e8

Browse files
authored
Merge pull request #1911 from swagger-api/SWG-7516-utilizing-safe-url-resolving-in-swagger-parser
SWG-7516 utilizing safeURLResolver in swagger-parser-v3
2 parents 2d8cceb + 2742c96 commit e5a23e8

File tree

9 files changed

+215
-6
lines changed

9 files changed

+215
-6
lines changed

modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.swagger.v3.parser.core.models;
22

3+
import java.util.List;
4+
35
public class ParseOptions {
46
private boolean resolve;
57
private boolean resolveCombinators = true;
@@ -16,6 +18,10 @@ public class ParseOptions {
1618

1719
private boolean oaiAuthor;
1820
private boolean inferSchemaType = true;
21+
private boolean safelyResolveURL;
22+
private List<String> remoteRefAllowList;
23+
private List<String> remoteRefBlockList;
24+
1925

2026
public boolean isResolve() {
2127
return resolve;
@@ -131,4 +137,28 @@ public boolean isInferSchemaType() {
131137
public void setInferSchemaType(boolean inferSchemaType) {
132138
this.inferSchemaType = inferSchemaType;
133139
}
140+
141+
public boolean isSafelyResolveURL() {
142+
return safelyResolveURL;
143+
}
144+
145+
public void setSafelyResolveURL(boolean safelyResolveURL) {
146+
this.safelyResolveURL = safelyResolveURL;
147+
}
148+
149+
public List<String> getRemoteRefAllowList() {
150+
return remoteRefAllowList;
151+
}
152+
153+
public void setRemoteRefAllowList(List<String> remoteRefAllowList) {
154+
this.remoteRefAllowList = remoteRefAllowList;
155+
}
156+
157+
public List<String> getRemoteRefBlockList() {
158+
return remoteRefBlockList;
159+
}
160+
161+
public void setRemoteRefBlockList(List<String> remoteRefBlockList) {
162+
this.remoteRefBlockList = remoteRefBlockList;
163+
}
134164
}

modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/v3/parser/urlresolver/PermittedUrlsChecker.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ public PermittedUrlsChecker() {
2323
}
2424

2525
public PermittedUrlsChecker(List<String> allowlist, List<String> denylist) {
26-
this.allowlistMatcher = new UrlPatternMatcher(allowlist);
27-
this.denylistMatcher = new UrlPatternMatcher(denylist);
26+
if(allowlist != null) {
27+
this.allowlistMatcher = new UrlPatternMatcher(allowlist);
28+
} else {
29+
this.allowlistMatcher = new UrlPatternMatcher(Collections.emptyList());
30+
}
31+
32+
if(denylist != null) {
33+
this.denylistMatcher = new UrlPatternMatcher(denylist);
34+
} else {
35+
this.denylistMatcher = new UrlPatternMatcher(Collections.emptyList());
36+
}
2837
}
2938

3039
public ResolvedUrl verify(String url) throws HostDeniedException {

modules/swagger-parser-v3/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
<artifactId>swagger-parser-core</artifactId>
2727
<version>${project.parent.version}</version>
2828
</dependency>
29+
<dependency>
30+
<groupId>io.swagger.parser.v3</groupId>
31+
<artifactId>swagger-parser-safe-url-resolver</artifactId>
32+
<version>${project.version}</version>
33+
</dependency>
2934
<dependency>
3035
<groupId>org.jmockit</groupId>
3136
<artifactId>jmockit</artifactId>

modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer.java

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
package io.swagger.v3.parser.reference;
2-
32
import java.util.Iterator;
43

54
public interface OpenAPIDereferencer {

modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void dereference(DereferencerContext context, Iterator<OpenAPIDereference
6969
.auths(context.getAuths());
7070

7171
Traverser traverser = buildTraverser(context);
72-
Visitor referenceVisitor = buildReferenceVisitor(context, reference, traverser);
72+
ReferenceVisitor referenceVisitor = buildReferenceVisitorWithContext(context, reference, traverser);
7373
try {
7474
openAPI = traverser.traverse(context.getOpenApi(), referenceVisitor);
7575
} catch (Exception e){
@@ -80,6 +80,7 @@ public void dereference(DereferencerContext context, Iterator<OpenAPIDereference
8080
if (openAPI == null) {
8181
return;
8282
}
83+
8384
result.setOpenAPI(openAPI);
8485
result.getMessages().addAll(reference.getMessages());
8586
}
@@ -91,4 +92,9 @@ public Traverser buildTraverser(DereferencerContext context) {
9192
public Visitor buildReferenceVisitor(DereferencerContext context, Reference reference, Traverser traverser) {
9293
return new ReferenceVisitor(reference, (OpenAPI31Traverser)traverser, new HashSet<>(), new HashMap<>());
9394
}
95+
96+
public ReferenceVisitor buildReferenceVisitorWithContext(DereferencerContext context, Reference reference, Traverser traverser) {
97+
return new ReferenceVisitor(reference, (OpenAPI31Traverser)traverser, new HashSet<>(), new HashMap<>(), context);
98+
}
99+
94100
}

modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java

+35-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import io.swagger.v3.oas.models.responses.ApiResponse;
1515
import io.swagger.v3.oas.models.security.SecurityScheme;
1616
import io.swagger.v3.parser.core.models.AuthorizationValue;
17+
import io.swagger.v3.parser.urlresolver.PermittedUrlsChecker;
18+
import io.swagger.v3.parser.urlresolver.exceptions.HostDeniedException;
19+
import io.swagger.v3.parser.util.RemoteUrl;
1720
import org.apache.commons.lang3.StringUtils;
1821
import org.slf4j.LoggerFactory;
1922

@@ -31,6 +34,7 @@ public class ReferenceVisitor extends AbstractVisitor {
3134
protected HashMap<Object, Object> visitedMap;
3235
protected OpenAPI31Traverser openAPITraverser;
3336
protected Reference reference;
37+
protected DereferencerContext context;
3438

3539
public ReferenceVisitor(
3640
Reference reference,
@@ -41,6 +45,20 @@ public ReferenceVisitor(
4145
this.openAPITraverser = openAPITraverser;
4246
this.visited = visited;
4347
this.visitedMap = visitedMap;
48+
this.context = null;
49+
}
50+
51+
public ReferenceVisitor(
52+
Reference reference,
53+
OpenAPI31Traverser openAPITraverser,
54+
HashSet<Object> visited,
55+
HashMap<Object, Object> visitedMap,
56+
DereferencerContext context) {
57+
this.reference = reference;
58+
this.openAPITraverser = openAPITraverser;
59+
this.visited = visited;
60+
this.visitedMap = visitedMap;
61+
this.context = context;
4462
}
4563

4664
public String toBaseURI(String uri) throws Exception{
@@ -174,13 +192,21 @@ public Header visitHeader(Header header){
174192
return resolveRef(header, header.get$ref(), Header.class, openAPITraverser::traverseHeader);
175193
}
176194

195+
@Override
196+
public String readHttp(String uri, List<AuthorizationValue> auths) throws Exception {
197+
if(context.getParseOptions().isSafelyResolveURL()){
198+
checkUrlIsPermitted(uri);
199+
}
200+
return RemoteUrl.urlToString(uri, auths);
201+
}
202+
177203
public<T> T resolveRef(T visiting, String ref, Class<T> clazz, BiFunction<T, ReferenceVisitor, T> traverseFunction){
178204
try {
179205
Reference reference = toReference(ref);
180206
String fragment = ReferenceUtils.getFragment(ref);
181207
JsonNode node = ReferenceUtils.jsonPointerEvaluate(fragment, reference.getJsonNode(), ref);
182208
T resolved = openAPITraverser.deserializeFragment(node, clazz, ref, fragment, reference.getMessages());
183-
ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap);
209+
ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap, context);
184210
return traverseFunction.apply(resolved, visitor);
185211

186212
} catch (Exception e) {
@@ -232,7 +258,7 @@ public Schema resolveSchemaRef(Schema visiting, String ref, List<String> inherit
232258
if (isAnchor) {
233259
resolved.$anchor(null);
234260
}
235-
ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap);
261+
ReferenceVisitor visitor = new ReferenceVisitor(reference, openAPITraverser, this.visited, this.visitedMap, context);
236262
return openAPITraverser.traverseSchema(resolved, visitor, inheritedIds);
237263
} catch (Exception e) {
238264
LOGGER.error("Error resolving schema " + ref, e);
@@ -278,4 +304,11 @@ public JsonNode deserializeIntoTree(String content) throws Exception {
278304
public JsonNode parse(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
279305
return deserializeIntoTree(readURI(absoluteUri, auths));
280306
}
307+
308+
protected void checkUrlIsPermitted(String refSet) throws HostDeniedException {
309+
PermittedUrlsChecker permittedUrlsChecker = new PermittedUrlsChecker(context.getParseOptions().getRemoteRefAllowList(),
310+
context.getParseOptions().getRemoteRefBlockList());
311+
312+
permittedUrlsChecker.verify(refSet);
313+
}
281314
}

modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java

+77
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import io.swagger.v3.parser.core.models.SwaggerParseResult;
99
import org.testng.annotations.Test;
1010

11+
import java.util.Arrays;
12+
import java.util.Collections;
1113
import java.util.List;
1214

1315
import static org.testng.Assert.*;
@@ -987,4 +989,79 @@ public void test31Issue1821() {
987989
Schema id = (Schema)result.getOpenAPI().getComponents().getSchemas().get("Rule").getProperties().get("id");
988990
assertEquals(id.getTypes().iterator().next(), "string");
989991
}
992+
993+
@Test(description = "Test safe resolving")
994+
public void test31SafeURLResolving() {
995+
ParseOptions parseOptions = new ParseOptions();
996+
parseOptions.setResolveFully(true);
997+
parseOptions.setSafelyResolveURL(true);
998+
List<String> allowList = Collections.emptyList();
999+
List<String> blockList = Collections.emptyList();
1000+
parseOptions.setRemoteRefAllowList(allowList);
1001+
parseOptions.setRemoteRefBlockList(blockList);
1002+
1003+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
1004+
1005+
assertTrue(result.getMessages().isEmpty());
1006+
}
1007+
1008+
@Test(description = "Test safe resolving with blocked URL")
1009+
public void test31SafeURLResolvingWithBlockedURL() {
1010+
ParseOptions parseOptions = new ParseOptions();
1011+
parseOptions.setResolveFully(true);
1012+
parseOptions.setSafelyResolveURL(true);
1013+
List<String> allowList = Collections.emptyList();
1014+
List<String> blockList = Arrays.asList("petstore3.swagger.io");
1015+
parseOptions.setRemoteRefAllowList(allowList);
1016+
parseOptions.setRemoteRefBlockList(blockList);
1017+
1018+
List<String> errorList = Arrays.asList("URL is part of the explicit denylist. URL [https://petstore3.swagger.io/api/v3/openapi.json]");
1019+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
1020+
1021+
assertEquals(result.getMessages(), errorList);
1022+
assertEquals(result.getMessages().size(), 1);
1023+
}
1024+
1025+
@Test(description = "Test safe resolving with turned off safelyResolveURL option")
1026+
public void test31SafeURLResolvingWithTurnedOffSafeResolving() {
1027+
ParseOptions parseOptions = new ParseOptions();
1028+
parseOptions.setResolveFully(true);
1029+
parseOptions.setSafelyResolveURL(false);
1030+
List<String> allowList = Collections.emptyList();
1031+
List<String> blockList = Arrays.asList("petstore3.swagger.io");
1032+
parseOptions.setRemoteRefAllowList(allowList);
1033+
parseOptions.setRemoteRefBlockList(blockList);
1034+
1035+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithPetstore.yaml", null, parseOptions);
1036+
1037+
assertTrue(result.getMessages().isEmpty());
1038+
}
1039+
1040+
@Test(description = "Test safe resolving with localhost and blocked url")
1041+
public void test31SafeURLResolvingWithLocalhostAndBlockedURL() {
1042+
ParseOptions parseOptions = new ParseOptions();
1043+
parseOptions.setResolveFully(true);
1044+
parseOptions.setSafelyResolveURL(true);
1045+
1046+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml", null, parseOptions);
1047+
1048+
assertTrue(result.getMessages().get(0).contains("IP is restricted"));
1049+
assertEquals(result.getMessages().size(), 1);
1050+
}
1051+
1052+
@Test(description = "Test safe resolving with localhost url")
1053+
public void test31SafeURLResolvingWithLocalhost() {
1054+
ParseOptions parseOptions = new ParseOptions();
1055+
parseOptions.setResolveFully(true);
1056+
parseOptions.setSafelyResolveURL(true);
1057+
List<String> blockList = Arrays.asList("petstore.swagger.io");
1058+
parseOptions.setRemoteRefBlockList(blockList);
1059+
1060+
String error = "URL is part of the explicit denylist. URL [https://petstore.swagger.io/v2/swagger.json]";
1061+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("3.1.0/resolve/safeResolving/safeUrlResolvingWithLocalhost.yaml", null, parseOptions);
1062+
1063+
assertTrue(result.getMessages().get(0).contains("IP is restricted"));
1064+
assertEquals(result.getMessages().get(1), error);
1065+
assertEquals(result.getMessages().size(), 2);
1066+
}
9901067
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
openapi: 3.1.0
2+
info:
3+
version: "1.0.0"
4+
title: ssrf-test
5+
paths:
6+
/devices:
7+
get:
8+
operationId: getDevices
9+
responses:
10+
'200':
11+
description: All the devices
12+
content:
13+
application/json:
14+
schema:
15+
$ref: 'http://localhost/example'
16+
/pets:
17+
get:
18+
operationId: getPets
19+
responses:
20+
'200':
21+
description: All the pets
22+
content:
23+
application/json:
24+
schema:
25+
$ref: 'https://petstore.swagger.io/v2/swagger.json'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
openapi: 3.1.0
2+
info:
3+
version: "1.0.0"
4+
title: ssrf-test
5+
paths:
6+
/devices:
7+
get:
8+
operationId: getDevices
9+
responses:
10+
'200':
11+
description: All the devices
12+
content:
13+
application/json:
14+
schema:
15+
$ref: 'https://petstore3.swagger.io/api/v3/openapi.json'
16+
/pets:
17+
get:
18+
operationId: getPets
19+
responses:
20+
'200':
21+
description: All the pets
22+
content:
23+
application/json:
24+
schema:
25+
$ref: 'https://petstore.swagger.io/v2/swagger.json'

0 commit comments

Comments
 (0)