Skip to content

Commit cd82bc6

Browse files
stevehujustin-tay
andauthored
Backport master fixes (#1254)
* fixes #1248 validating draft4 schemas (#1249) * fixes #1248 validating draft4 schemas * Address regex factory review comments * Rename regex specification version variable * fixes #1174 TextNodes as schema seem to validate any value (#1250) * fixes #1174 TextNodes as schema seem to validate any value * Address loaded schema validation for issue 1174 * Address schema node validation review comments * Preserve mapped schema override path * Validate referenced document fragment schemas * Remove duplicate schema type validation * Validate loaded schemas for anchor refs * fixes #1252 problem with additionalProperties schema in jsconfig schema (#1253) * Add method to SpecificationVersion to determine from schema node (#1221) * Manual adjustments for Jackson 2 compatibility and CVE fix --------- Co-authored-by: Justin Tay <49700559+justin-tay@users.noreply.github.com>
1 parent ad0f41b commit cd82bc6

24 files changed

Lines changed: 625 additions & 67 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ The specification allows for the `$schema` keyword not to be specified, in which
346346
The following example creates a `SchemaRegistry` that does not specify a default dialect and will throw a `MissingSchemaKeywordException` if the schema does not specify a dialect using the `$schema` keyword.
347347

348348
```java
349-
SchemaRegistry registry = SchemaRegistry.builder().dialectRegistry(new BasicDialectRegistry(Dialects.getDraft202012())).build();
349+
SchemaRegistry registry = SchemaRegistry.withDefaultDialectId(null);
350350
```
351351

352352
### Results and output formats

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
7474

7575
<version.itu>1.14.0</version.itu>
76-
<version.jackson>2.18.3</version.jackson>
76+
<version.jackson>2.18.4</version.jackson>
7777
<version.joni>2.2.6</version.joni>
7878
<version.logback>1.3.14</version.logback> <!-- 1.4.x and above is not Java 8 compatible -->
7979
<version.slf4j>2.0.17</version.slf4j>

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

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,26 @@ public static SchemaRegistry withDefaultDialect(SpecificationVersion specificati
282282
* Creates a new schema registry with a default schema dialect. The schema
283283
* dialect will only be used if the input does not specify a $schema.
284284
* <p>
285+
* If the dialectId is null then the $schema is mandatory.
286+
* <p>
287+
* This uses a dialect registry that contains all the supported standard
288+
* specification dialects, Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft
289+
* 2020-12.
290+
*
291+
* @param dialectId the default dialect id used when the schema does not
292+
* specify the $schema keyword
293+
* @return the factory
294+
*/
295+
public static SchemaRegistry withDefaultDialectId(String dialectId) {
296+
return withDefaultDialectId(dialectId, null);
297+
}
298+
299+
/**
300+
* Creates a new schema registry with a default schema dialect. The schema
301+
* dialect will only be used if the input does not specify a $schema.
302+
* <p>
303+
* If the dialectId is null then the $schema is mandatory.
304+
* <p>
285305
* This uses a dialect registry that contains all the supported standard
286306
* specification dialects, Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft
287307
* 2020-12.
@@ -449,9 +469,10 @@ public SchemaLoader getSchemaLoader() {
449469
* @return the schema
450470
*/
451471
protected Schema newSchema(SchemaLocation schemaUri, JsonNode schemaNode) {
472+
SchemaLocation schemaLocation = getSchemaLocation(schemaUri);
473+
validateSchemaNodeNotNull(schemaLocation, schemaNode);
452474
final SchemaContext schemaContext = createSchemaContext(schemaNode);
453-
Schema jsonSchema = doCreate(schemaContext, getSchemaLocation(schemaUri),
454-
schemaNode, null, false);
475+
Schema jsonSchema = doCreate(schemaContext, schemaLocation, schemaNode, null, false);
455476
preload(jsonSchema);
456477
return jsonSchema;
457478
}
@@ -487,10 +508,46 @@ public Schema create(SchemaContext schemaContext, SchemaLocation schemaLocation,
487508

488509
private Schema doCreate(SchemaContext schemaContext, SchemaLocation schemaLocation,
489510
JsonNode schemaNode, Schema parentSchema, boolean suppressSubSchemaRetrieval) {
490-
return Schema.from(withDialect(schemaContext, schemaNode), schemaLocation, schemaNode,
511+
return doCreate(schemaContext, schemaLocation, schemaNode, parentSchema, suppressSubSchemaRetrieval, true);
512+
}
513+
514+
private Schema doCreate(SchemaContext schemaContext, SchemaLocation schemaLocation,
515+
JsonNode schemaNode, Schema parentSchema, boolean suppressSubSchemaRetrieval,
516+
boolean validateSchemaNodeType) {
517+
validateSchemaNodeNotNull(schemaLocation, schemaNode);
518+
SchemaContext schemaContextToUse = withDialect(schemaContext, schemaNode);
519+
if (validateSchemaNodeType) {
520+
validateSchemaNodeType(schemaLocation, schemaNode, schemaContextToUse);
521+
}
522+
return Schema.from(schemaContextToUse, schemaLocation, schemaNode,
491523
parentSchema, suppressSubSchemaRetrieval);
492524
}
493525

526+
private void validateSchemaNodeNotNull(SchemaLocation schemaLocation, JsonNode schemaNode) {
527+
if (schemaNode == null) {
528+
throw new SchemaException("Schema at " + schemaLocation + " must not be null");
529+
}
530+
}
531+
532+
private void validateSchemaNodeType(SchemaLocation schemaLocation, JsonNode schemaNode,
533+
SchemaContext schemaContext) {
534+
if (schemaNode.isObject()) {
535+
return;
536+
}
537+
if (schemaNode.isBoolean() && supportsBooleanSchema(schemaContext)) {
538+
return;
539+
}
540+
String expected = supportsBooleanSchema(schemaContext) ? "object or boolean" : "object";
541+
throw new SchemaException("Schema at " + schemaLocation + " must be " + expected + " but was "
542+
+ schemaNode.getNodeType());
543+
}
544+
545+
private boolean supportsBooleanSchema(SchemaContext schemaContext) {
546+
return schemaContext != null
547+
&& schemaContext.getDialect().getSpecificationVersion().getOrder() >= SpecificationVersion.DRAFT_6
548+
.getOrder();
549+
}
550+
494551
/**
495552
* Determines the schema context to use for the schema given the parent schema
496553
* context.
@@ -647,6 +704,7 @@ public Schema getSchema(final InputStream schemaStream, InputFormat inputFormat)
647704
*/
648705
public Schema getSchema(final SchemaLocation schemaUri) {
649706
Schema schema = loadSchema(schemaUri);
707+
validateSchemaNodeType(schema.getSchemaLocation(), schema.getSchemaNode(), schema.getSchemaContext());
650708
preload(schema);
651709
return schema;
652710
}
@@ -722,6 +780,20 @@ public Schema getSchema(final JsonNode jsonNode) {
722780
* @return the schema
723781
*/
724782
public Schema loadSchema(final SchemaLocation schemaUri) {
783+
return loadSchema(schemaUri, true);
784+
}
785+
786+
/**
787+
* Loads the schema.
788+
*
789+
* @param schemaUri the absolute IRI of the schema which can map to the
790+
* retrieval IRI.
791+
* @param validateLoadedSchema true to validate that the loaded node is a schema;
792+
* false when loading a document container to resolve a
793+
* fragment before using it as a schema
794+
* @return the schema
795+
*/
796+
public Schema loadSchema(final SchemaLocation schemaUri, boolean validateLoadedSchema) {
725797
if (schemaCacheEnabled) {
726798
// ConcurrentHashMap computeIfAbsent does not allow calls that result in a
727799
// recursive update to the map.
@@ -731,19 +803,28 @@ public Schema loadSchema(final SchemaLocation schemaUri) {
731803
synchronized (this) { // acquire lock on shared registry object to prevent deadlock
732804
cachedUriSchema = schemaCache.get(schemaUri);
733805
if (cachedUriSchema == null) {
734-
cachedUriSchema = getMappedSchema(schemaUri);
806+
cachedUriSchema = validateLoadedSchema ? getMappedSchema(schemaUri)
807+
: getMappedSchema(schemaUri, false);
735808
if (cachedUriSchema != null) {
736809
schemaCache.put(schemaUri, cachedUriSchema);
737810
}
738811
}
739812
}
740813
}
814+
if (validateLoadedSchema && cachedUriSchema != null) {
815+
validateSchemaNodeType(cachedUriSchema.getSchemaLocation(), cachedUriSchema.getSchemaNode(),
816+
cachedUriSchema.getSchemaContext());
817+
}
741818
return cachedUriSchema;
742819
}
743-
return getMappedSchema(schemaUri);
820+
return validateLoadedSchema ? getMappedSchema(schemaUri) : getMappedSchema(schemaUri, false);
744821
}
745822

746823
protected Schema getMappedSchema(final SchemaLocation schemaUri) {
824+
return getMappedSchema(schemaUri, true);
825+
}
826+
827+
protected Schema getMappedSchema(final SchemaLocation schemaUri, boolean validateLoadedSchema) {
747828
InputStreamSource inputStreamSource = this.schemaLoader.getSchemaResource(schemaUri.getAbsoluteIri());
748829
if (inputStreamSource != null) {
749830
try (InputStream inputStream = inputStreamSource.getInputStream()) {
@@ -762,13 +843,13 @@ protected Schema getMappedSchema(final SchemaLocation schemaUri) {
762843
// Schema without fragment
763844
SchemaContext schemaContext = new SchemaContext(dialect, this);
764845
return doCreate(schemaContext, schemaUri, schemaNode, null,
765-
true /* retrieved via id, resolving will not change anything */);
846+
true /* retrieved via id, resolving will not change anything */, validateLoadedSchema);
766847
} else {
767848
// Schema with fragment pointing to sub schema
768849
final SchemaContext schemaContext = createSchemaContext(schemaNode);
769850
SchemaLocation documentLocation = new SchemaLocation(schemaUri.getAbsoluteIri());
770851
Schema document = doCreate(schemaContext, documentLocation, schemaNode, null,
771-
false);
852+
false, false);
772853
return document.getRefSchema(schemaUri.getFragment());
773854
}
774855
} catch (IOException e) {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import com.networknt.schema.dialect.DialectId;
2121

22+
import com.fasterxml.jackson.databind.JsonNode;
23+
2224
/**
2325
* The version of the JSON Schema specification that defines the standard
2426
* dialects.
@@ -90,4 +92,20 @@ public static Optional<SpecificationVersion> fromDialectId(String dialectId) {
9092
}
9193
return Optional.empty();
9294
}
95+
96+
/**
97+
* Gets the specification version that matches the dialect id indicated by
98+
* $schema keyword. The dialect id is an IRI that identifies the meta schema
99+
* used to validate the dialect.
100+
*
101+
* @param schemaNode the schema
102+
* @return the specification version if it matches the dialect id
103+
*/
104+
public static Optional<SpecificationVersion> fromSchemaNode(JsonNode schemaNode) {
105+
JsonNode schema = schemaNode.get("$schema");
106+
if (schema != null && schema.isTextual()) {
107+
return fromDialectId(schema.textValue());
108+
}
109+
return Optional.empty();
110+
}
93111
}

src/main/java/com/networknt/schema/keyword/AdditionalPropertiesValidator.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,6 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo
109109
for (Iterator<Entry<String, JsonNode>> it = node.fields(); it.hasNext(); ) {
110110
Entry<String, JsonNode> entry = it.next();
111111
String pname = entry.getKey();
112-
// skip the context items
113-
if (pname.startsWith("#")) {
114-
continue;
115-
}
116112
if (!allowedProperties.contains(pname) && !handledByPatternProperties(pname)) {
117113
if (!allowAdditionalProperties) {
118114
executionContext.addError(error().instanceNode(node).property(pname)
@@ -156,10 +152,6 @@ public void walk(ExecutionContext executionContext, JsonNode node, JsonNode root
156152
// Else continue walking.
157153
for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
158154
String pname = it.next();
159-
// skip the context items
160-
if (pname.startsWith("#")) {
161-
continue;
162-
}
163155
if (!allowedProperties.contains(pname) && !handledByPatternProperties(pname)) {
164156
if (allowAdditionalProperties) {
165157
if (additionalPropertiesSchema != null) {

src/main/java/com/networknt/schema/keyword/RefValidator.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,28 +60,30 @@ static SchemaRef getRefSchema(Schema parentSchema, SchemaContext schemaContext,
6060
refUri = refValue;
6161
}
6262

63-
// This will determine the correct absolute uri for the refUri. This decision will take into
64-
// account the current uri of the parent schema.
65-
String schemaUriFinal = resolve(parentSchema, refUri);
66-
SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal);
67-
// This should retrieve schemas regardless of the protocol that is in the uri.
68-
return new SchemaRef(getSupplier(() -> {
69-
Schema schemaResource = schemaContext.getSchemaResources().get(schemaUriFinal);
70-
if (schemaResource == null) {
71-
schemaResource = schemaContext.getSchemaRegistry().loadSchema(schemaLocation);
72-
if (schemaResource != null) {
73-
copySchemaResources(schemaContext, schemaResource);
74-
}
75-
}
63+
// This will determine the correct absolute uri for the refUri. This decision will take into
64+
// account the current uri of the parent schema.
65+
String schemaUriFinal = resolve(parentSchema, refUri);
66+
SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal);
67+
String fragment = index < 0 ? null : refValue.substring(index);
68+
boolean validateLoadedSchema = fragment == null || !SchemaLocation.Fragment.isJsonPointerFragment(fragment);
69+
// This should retrieve schemas regardless of the protocol that is in the uri.
70+
return new SchemaRef(getSupplier(() -> {
71+
Schema schemaResource = schemaContext.getSchemaResources().get(schemaUriFinal);
72+
if (schemaResource == null) {
73+
schemaResource = schemaContext.getSchemaRegistry().loadSchema(schemaLocation, validateLoadedSchema);
74+
if (schemaResource != null) {
75+
copySchemaResources(schemaContext, schemaResource);
76+
}
77+
}
7678
if (index < 0) {
7779
if (schemaResource == null) {
7880
return null;
79-
}
80-
return schemaResource;
81-
} else {
82-
String newRefValue = refValue.substring(index);
83-
String find = schemaLocation.getAbsoluteIri() + newRefValue;
84-
Schema findSchemaResource = schemaContext.getSchemaResources().get(find);
81+
}
82+
return schemaResource;
83+
} else {
84+
String newRefValue = fragment;
85+
String find = schemaLocation.getAbsoluteIri() + newRefValue;
86+
Schema findSchemaResource = schemaContext.getSchemaResources().get(find);
8587
if (findSchemaResource == null) {
8688
findSchemaResource = schemaContext.getDynamicAnchors().get(find);
8789
}

src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.networknt.schema.InvalidSchemaException;
2121
import com.networknt.schema.Error;
22+
import com.networknt.schema.SchemaContext;
2223

2324
/**
2425
* {@link RegularExpressionFactory} that allows regular expressions to be used.
@@ -34,9 +35,16 @@ public AllowRegularExpressionFactory(RegularExpressionFactory delegate, Predicat
3435

3536
@Override
3637
public RegularExpression getRegularExpression(String regex) {
38+
return getRegularExpression(regex, null);
39+
}
40+
41+
@Override
42+
public RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) {
3743
if (this.allowed.test(regex)) {
3844
// Allowed to delegate
39-
return this.delegate.getRegularExpression(regex);
45+
return schemaContext != null
46+
? this.delegate.getRegularExpression(regex, schemaContext)
47+
: this.delegate.getRegularExpression(regex);
4048
}
4149
throw new InvalidSchemaException(Error.builder()
4250
.message("Regular expression ''{0}'' is not allowed to be used.").arguments(regex).build());

src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.networknt.schema.regex;
1717

18+
import com.networknt.schema.SchemaContext;
1819
import com.networknt.schema.utils.Classes;
1920

2021
/**
@@ -45,4 +46,9 @@ public static ECMAScriptRegularExpressionFactory getInstance() {
4546
public RegularExpression getRegularExpression(String regex) {
4647
return DELEGATE.getRegularExpression(regex);
4748
}
49+
50+
@Override
51+
public RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) {
52+
return DELEGATE.getRegularExpression(regex, schemaContext);
53+
}
4854
}

src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ class GraalJSRegularExpression implements RegularExpression {
2828
private final Value function;
2929

3030
GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context) {
31+
this(regex, context, true);
32+
}
33+
34+
GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context, boolean unicode) {
3135
this.context = context;
3236
synchronized(context.getContext()) {
33-
this.function = context.getRegExpBuilder().execute(regex);
37+
this.function = context.getRegExpBuilder(unicode).execute(regex);
3438
}
3539
}
3640

@@ -40,4 +44,4 @@ public boolean matches(String value) {
4044
return !function.execute(value).isNull();
4145
}
4246
}
43-
}
47+
}

src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
* GraalJSRegularExpressionContext.
2323
*/
2424
public class GraalJSRegularExpressionContext {
25-
private static final String SOURCE = "pattern => {\n"
26-
+ " const regex = new RegExp(pattern, 'u');\n"
25+
private static final String SOURCE = "flags => pattern => {\n"
26+
+ " const regex = new RegExp(pattern, flags);\n"
2727
+ " return text => text.match(regex)\n"
2828
+ "};";
2929

3030
private final Context context;
31+
private final Value nonUnicodeRegExpBuilder;
3132
private final Value regExpBuilder;
3233

3334
/**
@@ -41,7 +42,9 @@ public class GraalJSRegularExpressionContext {
4142
public GraalJSRegularExpressionContext(Context context) {
4243
this.context = context;
4344
synchronized(this.context) {
44-
this.regExpBuilder = this.context.eval("js", SOURCE);
45+
Value builder = this.context.eval("js", SOURCE);
46+
this.nonUnicodeRegExpBuilder = builder.execute("");
47+
this.regExpBuilder = builder.execute("u");
4548
}
4649
}
4750

@@ -63,4 +66,14 @@ public Context getContext() {
6366
public Value getRegExpBuilder() {
6467
return regExpBuilder;
6568
}
69+
70+
/**
71+
* Gets the RegExp builder.
72+
*
73+
* @param unicode whether the unicode flag should be used
74+
* @return the regexp builder
75+
*/
76+
public Value getRegExpBuilder(boolean unicode) {
77+
return unicode ? regExpBuilder : nonUnicodeRegExpBuilder;
78+
}
6679
}

0 commit comments

Comments
 (0)