diff --git a/compile.js b/compile.js index aaf6cc8..47d1761 100644 --- a/compile.js +++ b/compile.js @@ -213,6 +213,15 @@ function validateIdentifier(name) { } } +// Per protobuf spec a tag is an integer in [1, 2^29 - 1] (accepting numeric strings too, as .proto text parser emits them). +// Validated to prevent code injection as Field tags are interpolated directly into generated JS +function validateTag(tag) { + const n = typeof tag === 'string' ? Number(tag) : tag; + if (!Number.isInteger(n) || n < 1 || n > 0x1fffffff) { + throw new Error(`Invalid protobuf field tag: ${JSON.stringify(tag)}`); + } +} + function buildContext(proto, parent, mapEntryField) { const obj = Object.create(parent); obj._proto = proto; @@ -233,6 +242,7 @@ function buildContext(proto, parent, mapEntryField) { for (const field of proto.fields ?? []) { validateIdentifier(field.name); if (field.oneof) validateIdentifier(field.oneof); + validateTag(field.tag); } for (const valueName of Object.keys(proto.values ?? {})) validateIdentifier(valueName); diff --git a/test/compile.test.js b/test/compile.test.js index 473f8db..4b90ac0 100644 --- a/test/compile.test.js +++ b/test/compile.test.js @@ -403,3 +403,42 @@ test('rejects proto schemas with invalid identifiers', () => { assert.equal(globalThis.__pbfPwned, undefined); }); + +test('rejects proto schemas with invalid field tags', () => { + const makeProto = tag => ({ + syntax: 3, + package: null, + imports: [], + enums: [], + messages: [{ + name: 'Foo', + enums: [], + messages: [], + extensions: null, + fields: [{ + name: 'a', + type: 'string', + tag, + map: null, + oneof: null, + required: false, + repeated: false, + options: {} + }] + }] + }); + + // code injection via a string tag interpolated into the generated source + assert.throws(() => compile(makeProto('(globalThis.__pbfTagPwned = true, 1)')), /Invalid protobuf field tag/); + assert.equal(globalThis.__pbfTagPwned, undefined); + + // out-of-range and non-integer tags + assert.throws(() => compile(makeProto(0)), /Invalid protobuf field tag/); + assert.throws(() => compile(makeProto(-1)), /Invalid protobuf field tag/); + assert.throws(() => compile(makeProto(1.5)), /Invalid protobuf field tag/); + assert.throws(() => compile(makeProto(0x20000000)), /Invalid protobuf field tag/); + + // valid numeric and numeric-string tags (the .proto text parser emits strings) still compile + assert.doesNotThrow(() => compile(makeProto(1))); + assert.doesNotThrow(() => compile(makeProto('2'))); +});