Skip to content

Commit d520ec2

Browse files
committed
fix(dereference): cache poisoning when dereferencing external schemas
1 parent 50414c3 commit d520ec2

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

lib/dereference.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,27 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
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.
223223
if (cache.circular) {
224-
return cache;
224+
// If both our cached value and our incoming `$ref` are the same then we can return what we
225+
// got out of the cache, otherwise we should re-process this value. We need to do this because
226+
// the current dereference caching mechanism doesn't take into account that `$ref` are neither
227+
// unique or reference the same file.
228+
//
229+
// For example if `schema.yaml` references `definitions/child.yaml` and
230+
// `definitions/parent.yaml` references `child.yaml` then `$ref: 'child.yaml'` may get cached
231+
// for `definitions/child.yaml`, resulting in `schema.yaml` being having an invalid reference
232+
// to `child.yaml`.
233+
//
234+
// This check is not perfect and the design of the dereference caching mechanism needs a total
235+
// overhaul.
236+
if (typeof cache.value === 'object' && '$ref' in cache.value && '$ref' in $ref) {
237+
if (cache.value.$ref === $ref.$ref) {
238+
return cache;
239+
} else {
240+
// no-op
241+
}
242+
} else {
243+
return cache;
244+
}
225245
}
226246

227247
const refKeys = Object.keys($ref);
@@ -238,8 +258,6 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
238258
value: Object.assign({}, cache.value, extraKeys),
239259
};
240260
}
241-
242-
return cache;
243261
}
244262

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

test/specs/circular-external/circular-external.spec.ts

Lines changed: 22 additions & 0 deletions
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)