Skip to content

Commit a6f3ae1

Browse files
authored
Add security considerations and mitigations (#1079)
1 parent bbbbd1c commit a6f3ae1

File tree

9 files changed

+318
-8
lines changed

9 files changed

+318
-8
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,16 @@ The earlier draft specifications contain less keywords that can potentially impa
551551

552552
This does not mean that using a schema with a later draft specification will automatically cause a performance impact. For instance, the `properties` validator will perform checks to determine if annotations need to be collected, and checks if the meta-schema contains the `unevaluatedProperties` keyword and whether the `unevaluatedProperties` keyword exists adjacent the evaluation path.
553553

554+
## Security Considerations
555+
556+
The library assumes that the schemas being loaded are trusted. This security model assumes the use case where the schemas are bundled with the application on the classpath.
557+
558+
| Issue | Description | Mitigation
559+
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------
560+
| Schema Loading | The library by default will load schemas from the classpath and over the internet if needed. | A `DisallowSchemaLoader` can be configured to not allow schema retrieval. Alternatively an `AllowSchemaLoader` can be configured to restrict the retrieval IRIs that are allowed.
561+
| Schema Caching | The library by default preloads and caches references when loading schemas. While there is a max nesting depth when preloading schemas it is still possible to construct a schema that has a fan out that consumes a lot of memory from the server. | Set `cacheRefs` option in `SchemaValidatorsConfig` to false.
562+
| Regular Expressions | The library does not validate if a given regular expression is susceptable to denial of service ([ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)). | An `AllowRegularExpressionFactory` can be configured to perform validation on the regular expressions that are allowed.
563+
| Validation Errors | The library by default attempts to return all validation errors. The use of applicators such as `allOf` with a large number of schemas may result in a large number of validation errors taking up memory. | Set `failFast` option in `SchemaValidatorsConfig` to immediately return when the first error is encountered. The `OutputFormat.BOOLEAN` or `OutputFormat.FLAG` also can be used.
554564

555565
## [Quick Start](doc/quickstart.md)
556566

src/main/java/com/networknt/schema/AbsoluteIri.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public static String getScheme(String iri) {
182182
return "";
183183
}
184184
// iri refers to root
185-
int start = iri.indexOf(":");
185+
int start = iri.indexOf(':');
186186
if (start == -1) {
187187
return "";
188188
} else {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.regex;
17+
18+
import java.util.function.Predicate;
19+
20+
import com.networknt.schema.InvalidSchemaException;
21+
import com.networknt.schema.ValidationMessage;
22+
23+
/**
24+
* {@link RegularExpressionFactory} that allows regular expressions to be used.
25+
*/
26+
public class AllowRegularExpressionFactory implements RegularExpressionFactory {
27+
private final RegularExpressionFactory delegate;
28+
private final Predicate<String> allowed;
29+
30+
public AllowRegularExpressionFactory(RegularExpressionFactory delegate, Predicate<String> allowed) {
31+
this.delegate = delegate;
32+
this.allowed = allowed;
33+
}
34+
35+
@Override
36+
public RegularExpression getRegularExpression(String regex) {
37+
if (this.allowed.test(regex)) {
38+
// Allowed to delegate
39+
return this.delegate.getRegularExpression(regex);
40+
}
41+
throw new InvalidSchemaException(ValidationMessage.builder()
42+
.message("Regular expression ''{1}'' is not allowed to be used.").arguments(regex).build());
43+
}
44+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.resource;
17+
18+
import java.util.function.Predicate;
19+
20+
import com.networknt.schema.AbsoluteIri;
21+
import com.networknt.schema.InvalidSchemaException;
22+
import com.networknt.schema.ValidationMessage;
23+
24+
/**
25+
* {@link SchemaLoader} that allows loading external resources.
26+
*/
27+
public class AllowSchemaLoader implements SchemaLoader {
28+
private final Predicate<AbsoluteIri> allowed;
29+
30+
/**
31+
* Constructor.
32+
*
33+
* @param allowed the predicate to determine which external resource is allowed
34+
* to be loaded
35+
*/
36+
public AllowSchemaLoader(Predicate<AbsoluteIri> allowed) {
37+
this.allowed = allowed;
38+
}
39+
40+
@Override
41+
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
42+
if (this.allowed.test(absoluteIri)) {
43+
// Allow to delegate to the next schema loader
44+
return null;
45+
}
46+
throw new InvalidSchemaException(ValidationMessage.builder()
47+
.message("Schema from ''{1}'' is not allowed to be loaded.").arguments(absoluteIri).build());
48+
}
49+
}

src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,31 @@
1717

1818
import java.io.FileNotFoundException;
1919
import java.io.InputStream;
20+
import java.util.function.Supplier;
2021

2122
import com.networknt.schema.AbsoluteIri;
2223

2324
/**
2425
* Loads from classpath.
2526
*/
2627
public class ClasspathSchemaLoader implements SchemaLoader {
28+
private final Supplier<ClassLoader> classLoaderSource;
29+
30+
/**
31+
* Constructor.
32+
*/
33+
public ClasspathSchemaLoader() {
34+
this(ClasspathSchemaLoader::getClassLoader);
35+
}
36+
37+
/**
38+
* Constructor.
39+
*
40+
* @param classLoaderSource the class loader source
41+
*/
42+
public ClasspathSchemaLoader(Supplier<ClassLoader> classLoaderSource) {
43+
this.classLoaderSource = classLoaderSource;
44+
}
2745

2846
@Override
2947
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
@@ -35,19 +53,15 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
3553
name = iri.substring(9);
3654
}
3755
if (name != null) {
38-
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
39-
if (classLoader == null) {
40-
classLoader = SchemaLoader.class.getClassLoader();
41-
}
42-
ClassLoader loader = classLoader;
56+
ClassLoader classLoader = this.classLoaderSource.get();
4357
if (name.startsWith("//")) {
4458
name = name.substring(2);
4559
}
4660
String resource = name;
4761
return () -> {
48-
InputStream result = loader.getResourceAsStream(resource);
62+
InputStream result = classLoader.getResourceAsStream(resource);
4963
if (result == null) {
50-
result = loader.getResourceAsStream(resource.substring(1));
64+
result = classLoader.getResourceAsStream(resource.substring(1));
5165
}
5266
if (result == null) {
5367
throw new FileNotFoundException(iri);
@@ -58,4 +72,11 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
5872
return null;
5973
}
6074

75+
protected static ClassLoader getClassLoader() {
76+
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
77+
if (classLoader == null) {
78+
classLoader = SchemaLoader.class.getClassLoader();
79+
}
80+
return classLoader;
81+
}
6182
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.resource;
17+
18+
import com.networknt.schema.AbsoluteIri;
19+
import com.networknt.schema.InvalidSchemaException;
20+
import com.networknt.schema.ValidationMessage;
21+
22+
/**
23+
* {@link SchemaLoader} that disallows loading external resources.
24+
*/
25+
public class DisallowSchemaLoader implements SchemaLoader {
26+
private static DisallowSchemaLoader INSTANCE = new DisallowSchemaLoader();
27+
28+
/**
29+
* Disallows loading schemas from external resources.
30+
*
31+
* @return the disallow schema loader
32+
*/
33+
public static DisallowSchemaLoader getInstance() {
34+
return INSTANCE;
35+
}
36+
37+
/**
38+
* Constructor.
39+
*/
40+
private DisallowSchemaLoader() {
41+
}
42+
43+
@Override
44+
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
45+
throw new InvalidSchemaException(ValidationMessage.builder()
46+
.message("Schema from ''{1}'' is not allowed to be loaded.").arguments(absoluteIri).build());
47+
}
48+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.regex;
17+
18+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import com.networknt.schema.InvalidSchemaException;
26+
27+
/**
28+
* Test for AllowRegularExpressionFactory.
29+
*/
30+
class AllowRegularExpressionFactoryTest {
31+
@Test
32+
void getRegularExpression() {
33+
boolean called[] = { false };
34+
RegularExpressionFactory delegate = (regex) -> {
35+
called[0] = true;
36+
return null;
37+
};
38+
String allowed = "testing";
39+
RegularExpressionFactory factory = new AllowRegularExpressionFactory(delegate, allowed::equals);
40+
InvalidSchemaException exception = assertThrows(InvalidSchemaException.class, () -> factory.getRegularExpression("hello"));
41+
assertEquals("hello", exception.getValidationMessage().getArguments()[0]);
42+
43+
assertDoesNotThrow(() -> factory.getRegularExpression(allowed));
44+
assertTrue(called[0]);
45+
}
46+
47+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.resource;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import com.networknt.schema.InvalidSchemaException;
25+
import com.networknt.schema.JsonSchema;
26+
import com.networknt.schema.JsonSchemaFactory;
27+
import com.networknt.schema.SchemaLocation;
28+
import com.networknt.schema.SpecVersion.VersionFlag;
29+
30+
/**
31+
* Test for AllowSchemaLoader.
32+
*/
33+
class AllowSchemaLoaderTest {
34+
35+
@Test
36+
void integration() {
37+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
38+
builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders
39+
.add(new AllowSchemaLoader(iri -> iri.toString().startsWith("classpath:")))));
40+
InvalidSchemaException invalidSchemaException = assertThrows(InvalidSchemaException.class,
41+
() -> factory.getSchema(SchemaLocation.of("http://www.example.org/schema")));
42+
assertEquals("http://www.example.org/schema",
43+
invalidSchemaException.getValidationMessage().getArguments()[0].toString());
44+
JsonSchema schema = factory.getSchema(SchemaLocation.of("classpath:schema/example-main.json"));
45+
assertNotNull(schema);
46+
}
47+
48+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.resource;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertThrows;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import com.networknt.schema.InvalidSchemaException;
24+
import com.networknt.schema.JsonSchemaFactory;
25+
import com.networknt.schema.SchemaLocation;
26+
import com.networknt.schema.SpecVersion.VersionFlag;
27+
28+
/**
29+
* Test for DisallowSchemaLoader.
30+
*/
31+
class DisallowSchemaLoaderTest {
32+
33+
@Test
34+
void integration() {
35+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder
36+
.schemaLoaders(schemaLoaders -> schemaLoaders.add(DisallowSchemaLoader.getInstance())));
37+
InvalidSchemaException invalidSchemaException = assertThrows(InvalidSchemaException.class,
38+
() -> factory.getSchema(SchemaLocation.of("classpath:schema/example-main.json")));
39+
assertEquals("classpath:schema/example-main.json",
40+
invalidSchemaException.getValidationMessage().getArguments()[0].toString());
41+
}
42+
43+
}

0 commit comments

Comments
 (0)