Skip to content

Commit ce38c3b

Browse files
authored
Fix infinite $ref resolution loops (#1195)
* fix infinite $ref resolution loops Signed-off-by: Morgan Chang <shin19991207@gmail.com> * fix linter error --------- Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent f1f5a94 commit ce38c3b

File tree

2 files changed

+66
-10
lines changed

2 files changed

+66
-10
lines changed

src/languageservice/services/yamlSchemaService.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ export class YAMLSchemaService extends JSONSchemaService {
643643
dialect?: SchemaDialect;
644644
recursiveAnchorBase?: string;
645645
inheritedDynamicScope?: Map<string, JSONSchema[]>;
646+
siblingRefCycleKeys?: Set<string>;
646647
};
647648
const toWalk: WalkItem[] = [{ node, baseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope }];
648649
const seen = new WeakSet<JSONSchema>(); // prevents re-walking the same schema object graph
@@ -656,7 +657,8 @@ export class YAMLSchemaService extends JSONSchemaService {
656657
nodeBaseURL: string,
657658
nodeDialect: SchemaDialect,
658659
recursiveAnchorBase?: string,
659-
inheritedDynamicScope?: Map<string, JSONSchema[]>
660+
inheritedDynamicScope?: Map<string, JSONSchema[]>,
661+
siblingRefCycleKeys?: Set<string>
660662
): void => {
661663
const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseURL);
662664

@@ -708,28 +710,57 @@ export class YAMLSchemaService extends JSONSchemaService {
708710
};
709711

710712
const seenRefs = new Set<string>();
713+
714+
const _mergeIfResourceAlreadyInResolutionStack = (ref: string, resolvedResource: string, frag: string): boolean => {
715+
if (!resolutionStack.has(resolvedResource)) return false;
716+
if (!seenRefs.has(ref)) {
717+
const source = resourceIndexByUri.get(resolvedResource)?.root;
718+
if (source && typeof source === 'object') {
719+
_merge(next, source, resolvedResource, frag, !!recursiveAnchorBase);
720+
}
721+
seenRefs.add(ref);
722+
}
723+
return true;
724+
};
725+
711726
while (next.$dynamicRef || next.$recursiveRef || next.$ref) {
712727
const isDynamicRef = typeof next.$dynamicRef === 'string';
713728
const isRecursiveRef = !isDynamicRef && typeof next.$recursiveRef === 'string';
714729
const rawRef = next.$dynamicRef ?? next.$recursiveRef ?? next.$ref;
715730
if (typeof rawRef !== 'string') break;
716731
next._$ref = rawRef;
717732

733+
// parse ref into base URI and fragment
734+
const ref = decodeURIComponent(rawRef);
735+
const segments = ref.split('#', 2);
736+
const baseUri = segments[0];
737+
const frag = segments.length > 1 ? segments[1] : '';
738+
const resolvedRefKey = `${baseUri ? _resolveAgainstBase(nodeBaseURL, baseUri) : nodeBaseURL}#${frag}`;
739+
718740
if (_hasRefSiblings(next)) {
719741
// Draft-07 and earlier: ignore siblings
720742
if (nodeDialect === SchemaDialect.draft04 || nodeDialect === SchemaDialect.draft07) {
721743
_stripRefSiblings(next);
722744
} else {
745+
if (siblingRefCycleKeys?.has(resolvedRefKey)) break;
746+
723747
// Draft-2019+: support sibling keywords
724748
_rewriteRefWithSiblingsToAllOf(next);
725749
if (Array.isArray(next.allOf)) {
726-
for (const entry of next.allOf) {
750+
for (let i = 0; i < next.allOf.length; i++) {
751+
const entry = next.allOf[i];
727752
if (entry && typeof entry === 'object') {
753+
let nextSiblingRefCycleKeys: Set<string> | undefined;
754+
if (i === 0) {
755+
nextSiblingRefCycleKeys = new Set(siblingRefCycleKeys);
756+
nextSiblingRefCycleKeys.add(resolvedRefKey);
757+
}
728758
toWalk.push({
729759
node: entry as JSONSchema,
730760
baseURL: nodeBaseURL,
731761
recursiveAnchorBase,
732762
inheritedDynamicScope: currentDynamicScope,
763+
siblingRefCycleKeys: nextSiblingRefCycleKeys,
733764
});
734765
}
735766
}
@@ -738,16 +769,10 @@ export class YAMLSchemaService extends JSONSchemaService {
738769
}
739770
}
740771

741-
const ref = decodeURIComponent(rawRef);
742772
delete next.$dynamicRef;
743773
delete next.$recursiveRef;
744774
delete next.$ref;
745775

746-
// parse ref into base URI and fragment
747-
const segments = ref.split('#', 2);
748-
const baseUri = segments[0];
749-
const frag = segments.length > 1 ? segments[1] : '';
750-
751776
// Draft-2019+: $recursiveRef
752777
if (isRecursiveRef && (ref === '#' || ref === '')) {
753778
const targetRoot = resourceIndexByUri.get(nodeBaseURL)?.root;
@@ -796,6 +821,7 @@ export class YAMLSchemaService extends JSONSchemaService {
796821
}
797822

798823
if (baseUri.length > 0 || targetHasDynamicAnchor) {
824+
if (_mergeIfResourceAlreadyInResolutionStack(ref, resolveResource, frag)) continue;
799825
openPromises.push(
800826
resolveExternalLink(
801827
next,
@@ -813,6 +839,8 @@ export class YAMLSchemaService extends JSONSchemaService {
813839
}
814840
// normal $ref with external baseUri
815841
else if (baseUri.length > 0) {
842+
const resolvedBaseUri = _resolveAgainstBase(nodeBaseURL, baseUri);
843+
if (_mergeIfResourceAlreadyInResolutionStack(ref, resolvedBaseUri, frag)) continue;
816844
// resolve relative to this node's base URL
817845
openPromises.push(
818846
resolveExternalLink(
@@ -831,7 +859,7 @@ export class YAMLSchemaService extends JSONSchemaService {
831859

832860
// local $ref or $dynamicRef
833861
if (!seenRefs.has(ref)) {
834-
_merge(next, parentSchema, nodeBaseURL, frag, !!currentDynamicScope);
862+
_merge(next, parentSchema, nodeBaseURL, frag, isDynamicRef && !!currentDynamicScope);
835863
seenRefs.add(ref);
836864
}
837865
}
@@ -902,7 +930,7 @@ export class YAMLSchemaService extends JSONSchemaService {
902930
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseURL : undefined);
903931
if (seen.has(next)) continue;
904932
seen.add(next);
905-
_handleRef(next, nodeBaseURL, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope);
933+
_handleRef(next, nodeBaseURL, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope, item.siblingRefCycleKeys);
906934
}
907935
return Promise.all(openPromises);
908936
};

test/schema2020Validation.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,5 +2227,33 @@ hey`);
22272227
expect(result).to.have.length(1);
22282228
expect(result[0].message).to.include('String is longer than the maximum length of 2.');
22292229
});
2230+
2231+
it('does not infinite loop on cyclic $ref with siblings', async () => {
2232+
const schema = {
2233+
$schema: 'https://json-schema.org/draft/2020-12/schema',
2234+
$id: 'https://example.com/cyclic-ref-with-siblings.json',
2235+
$defs: {
2236+
a: {
2237+
$ref: '#/$defs/b',
2238+
properties: {
2239+
a: { type: 'string' },
2240+
},
2241+
required: ['a'],
2242+
},
2243+
b: {
2244+
$ref: '#/$defs/a',
2245+
properties: {
2246+
b: { type: 'string' },
2247+
},
2248+
required: ['b'],
2249+
},
2250+
},
2251+
$ref: '#/$defs/a',
2252+
} as JSONSchema;
2253+
schemaProvider.addSchema(SCHEMA_ID, schema);
2254+
const result = await parseSetup(`a: hello`);
2255+
expect(result).to.have.length(1);
2256+
expect(result[0].message).to.include('Missing property "b".');
2257+
});
22302258
});
22312259
});

0 commit comments

Comments
 (0)