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

Add security considerations and mitigations #1079

Merged
merged 1 commit into from
Jul 1, 2024
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/AbsoluteIri.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> allowed;

public AllowRegularExpressionFactory(RegularExpressionFactory delegate, Predicate<String> 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());
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/networknt/schema/resource/AllowSchemaLoader.java
Original file line number Diff line number Diff line change
@@ -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<AbsoluteIri> allowed;

/**
* Constructor.
*
* @param allowed the predicate to determine which external resource is allowed
* to be loaded
*/
public AllowSchemaLoader(Predicate<AbsoluteIri> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,31 @@

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.function.Supplier;

import com.networknt.schema.AbsoluteIri;

/**
* Loads from classpath.
*/
public class ClasspathSchemaLoader implements SchemaLoader {
private final Supplier<ClassLoader> classLoaderSource;

/**
* Constructor.
*/
public ClasspathSchemaLoader() {
this(ClasspathSchemaLoader::getClassLoader);
}

/**
* Constructor.
*
* @param classLoaderSource the class loader source
*/
public ClasspathSchemaLoader(Supplier<ClassLoader> classLoaderSource) {
this.classLoaderSource = classLoaderSource;
}

@Override
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
Expand All @@ -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);
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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]);
}

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

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

}