diff --git a/README.md b/README.md index 865009e5c..e9c668d93 100644 --- a/README.md +++ b/README.md @@ -551,6 +551,16 @@ The earlier draft specifications contain less keywords that can potentially impa 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. +## Security Considerations + +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. + +| Issue | Description | Mitigation +|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------- +| 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. +| 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. +| 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. +| 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. ## [Quick Start](doc/quickstart.md) diff --git a/src/main/java/com/networknt/schema/AbsoluteIri.java b/src/main/java/com/networknt/schema/AbsoluteIri.java index 372807a5e..3c4075c08 100644 --- a/src/main/java/com/networknt/schema/AbsoluteIri.java +++ b/src/main/java/com/networknt/schema/AbsoluteIri.java @@ -182,7 +182,7 @@ public static String getScheme(String iri) { return ""; } // iri refers to root - int start = iri.indexOf(":"); + int start = iri.indexOf(':'); if (start == -1) { return ""; } else { diff --git a/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java new file mode 100644 index 000000000..b4bc84e7e --- /dev/null +++ b/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.regex; + +import java.util.function.Predicate; + +import com.networknt.schema.InvalidSchemaException; +import com.networknt.schema.ValidationMessage; + +/** + * {@link RegularExpressionFactory} that allows regular expressions to be used. + */ +public class AllowRegularExpressionFactory implements RegularExpressionFactory { + private final RegularExpressionFactory delegate; + private final Predicate allowed; + + public AllowRegularExpressionFactory(RegularExpressionFactory delegate, Predicate allowed) { + this.delegate = delegate; + this.allowed = allowed; + } + + @Override + public RegularExpression getRegularExpression(String regex) { + if (this.allowed.test(regex)) { + // Allowed to delegate + return this.delegate.getRegularExpression(regex); + } + throw new InvalidSchemaException(ValidationMessage.builder() + .message("Regular expression ''{1}'' is not allowed to be used.").arguments(regex).build()); + } +} diff --git a/src/main/java/com/networknt/schema/resource/AllowSchemaLoader.java b/src/main/java/com/networknt/schema/resource/AllowSchemaLoader.java new file mode 100644 index 000000000..a996585a7 --- /dev/null +++ b/src/main/java/com/networknt/schema/resource/AllowSchemaLoader.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.resource; + +import java.util.function.Predicate; + +import com.networknt.schema.AbsoluteIri; +import com.networknt.schema.InvalidSchemaException; +import com.networknt.schema.ValidationMessage; + +/** + * {@link SchemaLoader} that allows loading external resources. + */ +public class AllowSchemaLoader implements SchemaLoader { + private final Predicate allowed; + + /** + * Constructor. + * + * @param allowed the predicate to determine which external resource is allowed + * to be loaded + */ + public AllowSchemaLoader(Predicate allowed) { + this.allowed = allowed; + } + + @Override + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + if (this.allowed.test(absoluteIri)) { + // Allow to delegate to the next schema loader + return null; + } + throw new InvalidSchemaException(ValidationMessage.builder() + .message("Schema from ''{1}'' is not allowed to be loaded.").arguments(absoluteIri).build()); + } +} diff --git a/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java index 1807f1edd..9cb41b94c 100644 --- a/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java @@ -17,6 +17,7 @@ import java.io.FileNotFoundException; import java.io.InputStream; +import java.util.function.Supplier; import com.networknt.schema.AbsoluteIri; @@ -24,6 +25,23 @@ * Loads from classpath. */ public class ClasspathSchemaLoader implements SchemaLoader { + private final Supplier classLoaderSource; + + /** + * Constructor. + */ + public ClasspathSchemaLoader() { + this(ClasspathSchemaLoader::getClassLoader); + } + + /** + * Constructor. + * + * @param classLoaderSource the class loader source + */ + public ClasspathSchemaLoader(Supplier classLoaderSource) { + this.classLoaderSource = classLoaderSource; + } @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { @@ -35,19 +53,15 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { name = iri.substring(9); } if (name != null) { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - if (classLoader == null) { - classLoader = SchemaLoader.class.getClassLoader(); - } - ClassLoader loader = classLoader; + ClassLoader classLoader = this.classLoaderSource.get(); if (name.startsWith("//")) { name = name.substring(2); } String resource = name; return () -> { - InputStream result = loader.getResourceAsStream(resource); + InputStream result = classLoader.getResourceAsStream(resource); if (result == null) { - result = loader.getResourceAsStream(resource.substring(1)); + result = classLoader.getResourceAsStream(resource.substring(1)); } if (result == null) { throw new FileNotFoundException(iri); @@ -58,4 +72,11 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { return null; } + protected static ClassLoader getClassLoader() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = SchemaLoader.class.getClassLoader(); + } + return classLoader; + } } diff --git a/src/main/java/com/networknt/schema/resource/DisallowSchemaLoader.java b/src/main/java/com/networknt/schema/resource/DisallowSchemaLoader.java new file mode 100644 index 000000000..e2d81d18e --- /dev/null +++ b/src/main/java/com/networknt/schema/resource/DisallowSchemaLoader.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.resource; + +import com.networknt.schema.AbsoluteIri; +import com.networknt.schema.InvalidSchemaException; +import com.networknt.schema.ValidationMessage; + +/** + * {@link SchemaLoader} that disallows loading external resources. + */ +public class DisallowSchemaLoader implements SchemaLoader { + private static DisallowSchemaLoader INSTANCE = new DisallowSchemaLoader(); + + /** + * Disallows loading schemas from external resources. + * + * @return the disallow schema loader + */ + public static DisallowSchemaLoader getInstance() { + return INSTANCE; + } + + /** + * Constructor. + */ + private DisallowSchemaLoader() { + } + + @Override + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + throw new InvalidSchemaException(ValidationMessage.builder() + .message("Schema from ''{1}'' is not allowed to be loaded.").arguments(absoluteIri).build()); + } +} diff --git a/src/test/java/com/networknt/schema/regex/AllowRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/AllowRegularExpressionFactoryTest.java new file mode 100644 index 000000000..12f5d751c --- /dev/null +++ b/src/test/java/com/networknt/schema/regex/AllowRegularExpressionFactoryTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.regex; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.InvalidSchemaException; + +/** + * Test for AllowRegularExpressionFactory. + */ +class AllowRegularExpressionFactoryTest { + @Test + void getRegularExpression() { + boolean called[] = { false }; + RegularExpressionFactory delegate = (regex) -> { + called[0] = true; + return null; + }; + String allowed = "testing"; + RegularExpressionFactory factory = new AllowRegularExpressionFactory(delegate, allowed::equals); + InvalidSchemaException exception = assertThrows(InvalidSchemaException.class, () -> factory.getRegularExpression("hello")); + assertEquals("hello", exception.getValidationMessage().getArguments()[0]); + + assertDoesNotThrow(() -> factory.getRegularExpression(allowed)); + assertTrue(called[0]); + } + +} diff --git a/src/test/java/com/networknt/schema/resource/AllowSchemaLoaderTest.java b/src/test/java/com/networknt/schema/resource/AllowSchemaLoaderTest.java new file mode 100644 index 000000000..c8a6a8b26 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/AllowSchemaLoaderTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.InvalidSchemaException; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Test for AllowSchemaLoader. + */ +class AllowSchemaLoaderTest { + + @Test + void integration() { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders + .add(new AllowSchemaLoader(iri -> iri.toString().startsWith("classpath:"))))); + InvalidSchemaException invalidSchemaException = assertThrows(InvalidSchemaException.class, + () -> factory.getSchema(SchemaLocation.of("http://www.example.org/schema"))); + assertEquals("http://www.example.org/schema", + invalidSchemaException.getValidationMessage().getArguments()[0].toString()); + JsonSchema schema = factory.getSchema(SchemaLocation.of("classpath:schema/example-main.json")); + assertNotNull(schema); + } + +} diff --git a/src/test/java/com/networknt/schema/resource/DisallowSchemaLoaderTest.java b/src/test/java/com/networknt/schema/resource/DisallowSchemaLoaderTest.java new file mode 100644 index 000000000..c226a8612 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/DisallowSchemaLoaderTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.InvalidSchemaException; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Test for DisallowSchemaLoader. + */ +class DisallowSchemaLoaderTest { + + @Test + void integration() { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder + .schemaLoaders(schemaLoaders -> schemaLoaders.add(DisallowSchemaLoader.getInstance()))); + InvalidSchemaException invalidSchemaException = assertThrows(InvalidSchemaException.class, + () -> factory.getSchema(SchemaLocation.of("classpath:schema/example-main.json"))); + assertEquals("classpath:schema/example-main.json", + invalidSchemaException.getValidationMessage().getArguments()[0].toString()); + } + +}