Skip to content

Commit 2e269a5

Browse files
authored
Merge pull request #157 from meteorrn/fix/collection-prototype-pollution
Fix: prototype pollution in collection
2 parents e0fad19 + 1d40aa6 commit 2e269a5

File tree

3 files changed

+123
-12
lines changed

3 files changed

+123
-12
lines changed

src/Collection.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { hasOwn, isPlainObject } from '../lib/utils.js';
99
* @private
1010
* @type {object}
1111
*/
12-
const observers = {};
12+
const observers = Object.create(null);
1313
/**
1414
* @private
1515
* @type {object}
1616
*/
17-
const observersByComp = {};
17+
const observersByComp = Object.create(null);
1818
/**
1919
* Get the list of callbacks for changes on a collection
2020
* @param {string} type - Type of change happening.
@@ -45,7 +45,7 @@ export function getObservers(type, collection, newDocument) {
4545
});
4646
}
4747
// Find the observers related to the specific query
48-
if (observersByComp[collection]) {
48+
if (observersByComp[collection] && !(collection in {})) {
4949
let keys = Object.keys(observersByComp[collection]);
5050
for (let i = 0; i < keys.length; i++) {
5151
observersByComp[collection][keys[i]].callbacks.forEach(
@@ -184,6 +184,17 @@ export class Collection {
184184
localCollections.push(name);
185185
}
186186

187+
// XXX: apparently using a name that occurs in Object prototype causes
188+
// Data.db[name] to return the full MemoryDb implementation from Minimongo
189+
// instead of a collection.
190+
// A respective issues has been opened: https://github.com/meteorrn/minimongo-cache
191+
// Additionally, this is subject to prototype pollution.
192+
if (name in {}) {
193+
throw new Error(
194+
`Object-prototype property ${name} is not a supported Collection name`
195+
);
196+
}
197+
187198
if (!Data.db[name]) Data.db.addCollection(name);
188199

189200
this._collection = Data.db[name];
@@ -232,14 +243,16 @@ export class Collection {
232243
// collection is changed it needs to be re-run
233244
if (Tracker.active && Tracker.currentComputation) {
234245
let id = Tracker.currentComputation._id;
235-
observersByComp[this._name] = observersByComp[this._name] || {};
246+
observersByComp[this._name] =
247+
observersByComp[this._name] || Object.create(null);
236248
if (!observersByComp[this._name][id]) {
237249
let item = {
238250
computation: Tracker.currentComputation,
239251
callbacks: [],
240252
};
241253
observersByComp[this._name][id] = item;
242254
}
255+
243256
let item = observersByComp[this._name][id];
244257

245258
item.callbacks.push({

test/src/Collection.tests.js

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
import { WebSocket } from 'mock-socket';
2-
import {
3-
Collection,
4-
localCollections,
5-
runObservers,
6-
} from '../../src/Collection';
2+
import Mongo from '../../src/Mongo';
3+
import { endpoint, props } from '../testHelpers';
4+
import { localCollections, runObservers } from '../../src/Collection';
75
import { expect } from 'chai';
86
import Data from '../../src/Data';
97
import DDP from '../../lib/ddp';
108
import Random from '../../lib/Random';
119
import { server } from '../hooks/mockServer';
10+
import Tracker from '../../src/Tracker';
11+
12+
const Collection = Mongo.Collection;
13+
const objectProps = props({});
14+
const defaultProps = [
15+
'_collection',
16+
'_name',
17+
'_transform',
18+
'constructor',
19+
'find',
20+
'findOne',
21+
'insert',
22+
'update',
23+
'remove',
24+
'helpers',
25+
'__defineGetter__',
26+
'__defineSetter__',
27+
'hasOwnProperty',
28+
'__lookupGetter__',
29+
'__lookupSetter__',
30+
'isPrototypeOf',
31+
'propertyIsEnumerable',
32+
'toString',
33+
'valueOf',
34+
'__proto__',
35+
'toLocaleString',
36+
];
1237

1338
describe('Collection', function () {
14-
const endpoint = 'ws://localhost:3000/websocket';
15-
1639
// for proper collection tests we need the server to be active
1740

1841
before(function () {
1942
if (!Data.ddp) {
2043
Data.ddp = new DDP({
2144
SocketConstructor: WebSocket,
2245
endpoint,
46+
autoConnect: false,
2347
});
2448
Data.ddp.socket.on('open', () => {
2549
Data.ddp.socket.emit('message:in', { msg: 'connected' });
@@ -46,12 +70,17 @@ describe('Collection', function () {
4670
});
4771

4872
describe('constructor', function () {
73+
it('is exported via Mongo', () => {
74+
expect(Mongo.Collection).to.equal(Collection);
75+
});
4976
it('creates a new collection and one in Minimongo', function () {
5077
const name = Random.id(6);
5178
const c = new Collection(name);
5279
expect(c._name).to.equal(name);
5380
expect(c._transform).to.equal(null);
54-
expect(Data.db.collections[name]).to.equal(c._collection);
81+
expect(c._collection).to.equal(Data.db.collections[name]);
82+
expect(c._collection.name).to.equal(name);
83+
expect(c._collection.constructor.name).to.equal('Collection');
5584
});
5685
it('creates a local collection and a random counterpart in minimongo', function () {
5786
const c = new Collection(null);
@@ -85,6 +114,14 @@ describe('Collection', function () {
85114
c = new Collection(Random.id(), { transform });
86115
expect(c._transform({ _id })).to.deep.equal({ _id, foo: 'bar' });
87116
});
117+
it('does not imply prototype pollution', () => {
118+
objectProps.forEach((name) => {
119+
expect(() => new Mongo.Collection(name)).to.throw(
120+
`Object-prototype property ${name} is not a supported Collection name`
121+
);
122+
expect(props({})).to.deep.equal(objectProps);
123+
});
124+
});
88125
});
89126

90127
describe('insert', function () {
@@ -173,6 +210,55 @@ describe('Collection', function () {
173210
done();
174211
});
175212
});
213+
it('triggers a reactive observer', (done) => {
214+
const collectionName = Random.id(6);
215+
const c = new Mongo.Collection(collectionName);
216+
217+
Tracker.autorun((comp) => {
218+
const doc = c.findOne({ foo: 1 });
219+
if (doc) {
220+
comp.stop();
221+
done();
222+
}
223+
});
224+
225+
setTimeout(() => c.insert({ foo: 1 }), 50);
226+
});
227+
it('does not imply prototype pollution', (done) => {
228+
const collectionName = Random.id(6);
229+
const c = new Mongo.Collection(collectionName);
230+
objectProps.forEach((name) => c.find(name).fetch());
231+
expect(props({})).to.deep.equal(objectProps);
232+
const insertDoc = {};
233+
objectProps.forEach((prop) => {
234+
insertDoc[prop] = 'foo';
235+
});
236+
237+
Tracker.autorun((comp) => {
238+
if (c.find().count() < 1) {
239+
return;
240+
}
241+
242+
c._name = '__proto__';
243+
try {
244+
c.find(insertDoc);
245+
} catch {}
246+
try {
247+
expect(props({})).to.deep.equal(objectProps);
248+
} catch (e) {
249+
comp.stop();
250+
return done(e);
251+
}
252+
253+
comp.stop();
254+
done();
255+
});
256+
257+
setTimeout(() => {
258+
c._name = collectionName;
259+
c.insert(insertDoc);
260+
}, 50);
261+
});
176262
});
177263

178264
describe('update', function () {

test/testHelpers.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sinon from 'sinon';
22
import Meteor from '../src/Meteor';
33

4+
export const endpoint = 'ws://localhost:3000/websocket';
5+
46
export const stubs = new Map();
57

68
export const stub = (target, name, handler) => {
@@ -47,3 +49,13 @@ export const awaitDisconnected = async () => {
4749
}, 100);
4850
});
4951
};
52+
53+
export const props = (obj) => {
54+
let p = [];
55+
for (; obj != null; obj = Object.getPrototypeOf(obj)) {
56+
let op = Object.getOwnPropertyNames(obj);
57+
for (let i = 0; i < op.length; i++)
58+
if (p.indexOf(op[i]) === -1) p.push(op[i]);
59+
}
60+
return p;
61+
};

0 commit comments

Comments
 (0)