Skip to content

Commit

Permalink
Add security considerations and mitigations (#1079)
Browse files Browse the repository at this point in the history
  • Loading branch information
justin-tay authored Jul 1, 2024
1 parent bbbbd1c commit a6f3ae1
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 8 deletions.
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());
}

}

0 comments on commit a6f3ae1

Please sign in to comment.