Skip to content

Commit 69cfcf6

Browse files
authored
fix(dereference): cache poisoning when dereferencing external schemas (#381)
* fix(dereference): cache poisoning when dereferencing external schemas * fix: restructuring some of this confusing cache logic
1 parent 50414c3 commit 69cfcf6

File tree

2 files changed

+57
-15
lines changed

2 files changed

+57
-15
lines changed

Diff for: lib/dereference.ts

+35-15
Original file line numberDiff line numberDiff line change
@@ -220,26 +220,46 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
220220
//
221221
// If the cached object however is _not_ circular and there are additional keys alongside our
222222
// `$ref` pointer here we should merge them back in and return that.
223-
if (cache.circular) {
223+
if (!cache.circular) {
224+
const refKeys = Object.keys($ref);
225+
if (refKeys.length > 1) {
226+
const extraKeys = {};
227+
for (const key of refKeys) {
228+
if (key !== "$ref" && !(key in cache.value)) {
229+
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
230+
extraKeys[key] = $ref[key];
231+
}
232+
}
233+
return {
234+
circular: cache.circular,
235+
value: Object.assign({}, cache.value, extraKeys),
236+
};
237+
}
238+
224239
return cache;
225240
}
226241

227-
const refKeys = Object.keys($ref);
228-
if (refKeys.length > 1) {
229-
const extraKeys = {};
230-
for (const key of refKeys) {
231-
if (key !== "$ref" && !(key in cache.value)) {
232-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
233-
extraKeys[key] = $ref[key];
234-
}
242+
// If both our cached value and our incoming `$ref` are the same then we can return what we
243+
// got out of the cache, otherwise we should re-process this value. We need to do this because
244+
// the current dereference caching mechanism doesn't take into account that `$ref` are neither
245+
// unique or reference the same file.
246+
//
247+
// For example if `schema.yaml` references `definitions/child.yaml` and
248+
// `definitions/parent.yaml` references `child.yaml` then `$ref: 'child.yaml'` may get cached
249+
// for `definitions/child.yaml`, resulting in `schema.yaml` being having an invalid reference
250+
// to `child.yaml`.
251+
//
252+
// This check is not perfect and the design of the dereference caching mechanism needs a total
253+
// overhaul.
254+
if (typeof cache.value === 'object' && '$ref' in cache.value && '$ref' in $ref) {
255+
if (cache.value.$ref === $ref.$ref) {
256+
return cache;
257+
} else {
258+
// no-op
235259
}
236-
return {
237-
circular: cache.circular,
238-
value: Object.assign({}, cache.value, extraKeys),
239-
};
260+
} else {
261+
return cache;
240262
}
241-
242-
return cache;
243263
}
244264

245265
const pointer = $refs._resolve($refPath, path, options);

Diff for: test/specs/circular-external/circular-external.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ describe("Schema with circular (recursive) external $refs", () => {
5353
expect(schema.definitions.child.properties.parents.items).to.equal(schema.definitions.parent);
5454
});
5555

56+
it('should throw an error if "options.dereference.circular" is "ignore"', async () => {
57+
const parser = new $RefParser();
58+
59+
const schema = await parser.dereference(path.rel("test/specs/circular-external/circular-external.yaml"), {
60+
dereference: { circular: 'ignore' },
61+
});
62+
63+
expect(schema).to.equal(parser.schema);
64+
expect(schema).not.to.deep.equal(dereferencedSchema);
65+
expect(parser.$refs.circular).to.equal(true);
66+
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
67+
expect(schema.definitions.pet.title).to.equal('pet');
68+
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
69+
expect(schema.definitions.thing).to.deep.equal({ $ref: 'circular-external.yaml#/definitions/thing'});
70+
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
71+
expect(schema.definitions.person).to.deep.equal({ $ref: 'definitions/person.yaml'});
72+
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
73+
expect(schema.definitions.parent).to.deep.equal({ $ref: 'definitions/parent.yaml'});
74+
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
75+
expect(schema.definitions.child).to.deep.equal({ $ref: 'definitions/child.yaml'});
76+
});
77+
5678
it('should throw an error if "options.dereference.circular" is false', async () => {
5779
const parser = new $RefParser();
5880

0 commit comments

Comments
 (0)