Skip to content

Commit 7e6177e

Browse files
authored
Merge pull request #44 from athombv/fix/parser-robustness
fix(parser): defensive Bitmap copy and Struct prototype-chain guard
2 parents 1580f16 + e2e67db commit 7e6177e

4 files changed

Lines changed: 97 additions & 2 deletions

File tree

lib/DataTypes.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ class Bitmap {
103103

104104
static fromBuffer(buf, i, len, args) {
105105
i = i || 0;
106-
return new Bitmap(buf.slice(i, i + len), args);
106+
// Copy the slice so the resulting Bitmap owns its bytes. `Buffer.slice`
107+
// returns a view sharing memory with the source; without the copy, later
108+
// mutation of `buf` (e.g. a radio driver reusing a receive buffer) would
109+
// silently change the bits of an already-parsed Bitmap.
110+
return new Bitmap(Buffer.from(buf.slice(i, i + len)), args);
107111
}
108112

109113
static toBuffer(buf, i, length, args, v) {

lib/Struct.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,20 @@ function Struct(name, defs, opts) {
3535
constructor(props = {}) {
3636
// eslint-disable-next-line guard-for-in,no-restricted-syntax
3737
for (const key in props) {
38-
if (!defs[key]) throw new TypeError(`${this.constructor.name}: ${key} is an unexpected property`);
38+
// Use hasOwnProperty so prototype-chain keys like `constructor`,
39+
// `toString`, etc. are rejected. The previous `!defs[key]` check
40+
// walked the prototype chain and would accept `constructor` because
41+
// `defs.constructor` resolves to `Object` (truthy).
42+
//
43+
// Use `new.target.name` for the prefix rather than
44+
// `this.constructor.name`: classes constructed via this pattern can
45+
// be subclassed, and `new.target` is the actual subclass being
46+
// instantiated. Unlike `this.constructor`, `new.target` is a
47+
// syntactic binding and cannot be shadowed by a `constructor`
48+
// field that the caller may have set earlier in this same loop.
49+
if (!Object.prototype.hasOwnProperty.call(defs, key)) {
50+
throw new TypeError(`${new.target.name}: ${key} is an unexpected property`);
51+
}
3952
this[key] = props[key];
4053
}
4154
// eslint-disable-next-line no-restricted-syntax

test/DataTypes.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,23 @@ describe('DataType', function() {
2626
testMap.constructor.toBuffer(buffer, bufferOffset, testMap.length, bits, testMap);
2727
assert.deepEqual(buffer, expectedBuffer, 'Static toBuffer failed');
2828
});
29+
30+
describe('source buffer aliasing', function() {
31+
it('should not share memory with the source buffer after fromBuffer', function() {
32+
const source = Buffer.from([0xff]);
33+
const map = DataTypes.map8('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')
34+
.fromBuffer(source, 0);
35+
36+
// Snapshot bits, mutate source, read again. If the Bitmap aliases
37+
// source memory, the second read observes the mutation.
38+
const bitsBefore = map.getBits();
39+
source[0] = 0x00;
40+
const bitsAfter = map.getBits();
41+
42+
assert.deepEqual(bitsBefore, bitsAfter,
43+
'Bitmap must own its bytes; mutating source must not affect parsed value');
44+
assert.deepEqual(bitsAfter, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
45+
});
46+
});
2947
});
3048
});

test/Struct.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,64 @@ describe('Struct', function() {
152152
assert(refData.field6.bit9);
153153
assert.deepEqual(refData.field6.toArray(), ['bit2', 'bit9']);
154154
});
155+
156+
describe('prototype-chain property guard', function() {
157+
let S;
158+
before(function() {
159+
S = Struct('GuardedStruct', { a: DataTypes.uint8 });
160+
});
161+
162+
it('should reject `constructor` as a field name', function() {
163+
assert.throws(
164+
() => new S({ constructor: 'attacker-controlled' }),
165+
/unexpected property/,
166+
'constructor is on Object.prototype, not an own property of defs',
167+
);
168+
});
169+
170+
it('should reject `toString` as a field name', function() {
171+
assert.throws(
172+
() => new S({ toString: () => 'pwned' }),
173+
/unexpected property/,
174+
);
175+
});
176+
177+
it('should still accept declared own-property field names', function() {
178+
const instance = new S({ a: 42 });
179+
assert.strictEqual(instance.a, 42);
180+
});
181+
182+
it('should still reject undeclared field names', function() {
183+
assert.throws(
184+
() => new S({ b: 1 }),
185+
/unexpected property/,
186+
);
187+
});
188+
189+
it('should produce a stable error message even when a `constructor` field is declared', function() {
190+
// Edge case: if a struct legitimately declares a `constructor` field
191+
// and the caller sets it before an unexpected key is seen, the throw
192+
// path must not depend on `this.constructor.name`.
193+
const Weird = Struct('WeirdStruct', {
194+
constructor: DataTypes.uint8,
195+
a: DataTypes.uint8,
196+
});
197+
assert.throws(
198+
() => new Weird({ constructor: 1, badKey: 2 }),
199+
err => err instanceof TypeError && /^WeirdStruct: badKey is an unexpected property$/.test(err.message),
200+
);
201+
});
202+
203+
it('should report the actual subclass name in the error when subclassed', function() {
204+
// Struct-generated classes can be subclassed; the error message should
205+
// identify the actual class being instantiated, not the underlying
206+
// Struct name. This is what `new.target.name` gives us.
207+
const Base = Struct('BaseStruct', { a: DataTypes.uint8 });
208+
class Extended extends Base {}
209+
assert.throws(
210+
() => new Extended({ unexpected: 1 }),
211+
err => err instanceof TypeError && /^Extended: unexpected is an unexpected property$/.test(err.message),
212+
);
213+
});
214+
});
155215
});

0 commit comments

Comments
 (0)