Skip to content

Commit af622b2

Browse files
fix(core): only honor own-property brands in cross-bundle instanceof
A brand reachable through the prototype chain no longer satisfies the check, so polluting a shared prototype with the registry symbol cannot make arbitrary objects pass instanceof against the branded error classes. Real instances are stamped with an own property and are unaffected.
1 parent fc558aa commit af622b2

3 files changed

Lines changed: 27 additions & 7 deletions

File tree

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// AUTO-GENERATED — do not edit. Run `pnpm run generate:versions` to regenerate.
22
export const V2_PACKAGE_VERSIONS: Record<string, string> = {
3-
'@modelcontextprotocol/client': '^2.0.0-alpha.2',
4-
'@modelcontextprotocol/server': '^2.0.0-alpha.2',
5-
'@modelcontextprotocol/node': '^2.0.0-alpha.2',
6-
'@modelcontextprotocol/express': '^2.0.0-alpha.2',
7-
'@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2',
8-
'@modelcontextprotocol/core': '^2.0.0-alpha.0'
3+
'@modelcontextprotocol/client': '^2.0.0-alpha.3',
4+
'@modelcontextprotocol/server': '^2.0.0-alpha.3',
5+
'@modelcontextprotocol/node': '^2.0.0-alpha.3',
6+
'@modelcontextprotocol/express': '^2.0.0-alpha.3',
7+
'@modelcontextprotocol/server-legacy': '^2.0.0-alpha.3',
8+
'@modelcontextprotocol/core': '^2.0.0-alpha.1'
99
};

packages/core-internal/src/errors/crossBundleBrand.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ export function brandedHasInstance(cls: object, value: unknown): boolean {
5858
typeof value === 'object' &&
5959
value !== null &&
6060
Object.prototype.hasOwnProperty.call(cls, 'mcpBrand') &&
61-
typeof (cls as BrandedConstructor).mcpBrand === 'string'
61+
typeof (cls as BrandedConstructor).mcpBrand === 'string' &&
62+
// Own-property only: a brand inherited via the prototype chain is never
63+
// honored, so polluting Object.prototype with the registry symbol cannot
64+
// make arbitrary objects satisfy instanceof (real instances are stamped
65+
// with an own property by stampErrorBrands).
66+
Object.prototype.hasOwnProperty.call(value, BRANDS)
6267
) {
6368
const carried = (value as BrandCarrier)[BRANDS];
6469
if (carried && typeof carried.has === 'function' && carried.has((cls as BrandedConstructor).mcpBrand!)) {

packages/core-internal/test/errors/crossBundleBrand.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ describe('cross-bundle error instanceof (branded)', () => {
117117
expect(json).not.toContain('mcp.Sdk');
118118
});
119119

120+
it('ignores a brand inherited via the prototype chain (prototype-pollution hardening)', () => {
121+
const BRANDS = Symbol.for('mcp.sdk.errorBrands');
122+
// A brand reachable only through the prototype chain must not count —
123+
// otherwise polluting a shared prototype would make arbitrary objects
124+
// satisfy instanceof against every branded class.
125+
const polluted = Object.create({ [BRANDS]: new Set(['mcp.SdkError', 'mcp.SdkHttpError']) }) as object;
126+
expect(polluted instanceof SdkError).toBe(false);
127+
expect(polluted instanceof SdkHttpError).toBe(false);
128+
129+
// An own-property carrier (what stampErrorBrands produces) is honored.
130+
const own = Object.create(Error.prototype) as object;
131+
Object.defineProperty(own, BRANDS, { value: new Set(['mcp.SdkError']), enumerable: false });
132+
expect(own instanceof SdkError).toBe(true);
133+
});
134+
120135
it('tolerates primitives, null, and brandless objects', () => {
121136
for (const value of [null, undefined, 42, 'x', Symbol('s'), {}, new Error('plain')]) {
122137
expect((value as unknown) instanceof SdkError).toBe(false);

0 commit comments

Comments
 (0)