sharedb 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -31,23 +31,23 @@ function Agent(backend, stream) {
31
31
 
32
32
  // We need to track which documents are subscribed by the client. This is a
33
33
  // map of collection -> id -> stream
34
- this.subscribedDocs = {};
34
+ this.subscribedDocs = Object.create(null);
35
35
 
36
36
  // Map from queryId -> emitter
37
- this.subscribedQueries = {};
37
+ this.subscribedQueries = Object.create(null);
38
38
 
39
39
  // Track which documents are subscribed to presence by the client. This is a
40
40
  // map of channel -> stream
41
- this.subscribedPresences = {};
41
+ this.subscribedPresences = Object.create(null);
42
42
  // Highest seq received for a subscription request. Any seq lower than this
43
43
  // value is stale, and should be ignored. Used for keeping the subscription
44
44
  // state in sync with the client's desired state. Map of channel -> seq
45
- this.presenceSubscriptionSeq = {};
45
+ this.presenceSubscriptionSeq = Object.create(null);
46
46
  // Keep track of the last request that has been sent by each local presence
47
47
  // belonging to this agent. This is used to generate a new disconnection
48
48
  // request if the client disconnects ungracefully. This is a
49
49
  // map of channel -> id -> request
50
- this.presenceRequests = {};
50
+ this.presenceRequests = Object.create(null);
51
51
 
52
52
  // We need to track this manually to make sure we don't reply to messages
53
53
  // after the stream was closed.
@@ -56,7 +56,7 @@ function Agent(backend, stream) {
56
56
  // For custom use in middleware. The agent is a convenient place to cache
57
57
  // session state in memory. It is in memory only as long as the session is
58
58
  // active, and it is passed to each middleware call
59
- this.custom = {};
59
+ this.custom = Object.create(null);
60
60
 
61
61
  // The first message received over the connection. Stored to warn if messages
62
62
  // are being sent before the handshake.
@@ -95,19 +95,19 @@ Agent.prototype._cleanup = function() {
95
95
  stream.destroy();
96
96
  }
97
97
  }
98
- this.subscribedDocs = {};
98
+ this.subscribedDocs = Object.create(null);
99
99
 
100
100
  for (var channel in this.subscribedPresences) {
101
101
  this.subscribedPresences[channel].destroy();
102
102
  }
103
- this.subscribedPresences = {};
103
+ this.subscribedPresences = Object.create(null);
104
104
 
105
105
  // Clean up query subscription streams
106
106
  for (var id in this.subscribedQueries) {
107
107
  var emitter = this.subscribedQueries[id];
108
108
  emitter.destroy();
109
109
  }
110
- this.subscribedQueries = {};
110
+ this.subscribedQueries = Object.create(null);
111
111
  };
112
112
 
113
113
  /**
@@ -117,7 +117,7 @@ Agent.prototype._cleanup = function() {
117
117
  Agent.prototype._subscribeToStream = function(collection, id, stream) {
118
118
  if (this.closed) return stream.destroy();
119
119
 
120
- var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = {});
120
+ var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = Object.create(null));
121
121
 
122
122
  // If already subscribed to this document, destroy the previously subscribed stream
123
123
  var previous = streams[id];
@@ -373,15 +373,25 @@ Agent.prototype._checkRequest = function(request) {
373
373
  request.a === ACTIONS.unsubscribe ||
374
374
  request.a === ACTIONS.presence) {
375
375
  // Doc-based request.
376
- if (request.c != null && typeof request.c !== 'string') return 'Invalid collection';
377
- if (request.d != null && typeof request.d !== 'string') return 'Invalid id';
376
+ if (request.c != null) {
377
+ if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) {
378
+ return 'Invalid collection';
379
+ }
380
+ }
381
+ if (request.d != null) {
382
+ if (typeof request.d !== 'string' || util.isDangerousProperty(request.d)) {
383
+ return 'Invalid id';
384
+ }
385
+ }
378
386
 
379
387
  if (request.a === ACTIONS.op || request.a === ACTIONS.presence) {
380
388
  if (request.v != null && (typeof request.v !== 'number' || request.v < 0)) return 'Invalid version';
381
389
  }
382
390
 
383
391
  if (request.a === ACTIONS.presence) {
384
- if (typeof request.id !== 'string') return 'Missing presence ID';
392
+ if (typeof request.id !== 'string' || util.isDangerousProperty(request.id)) {
393
+ return 'Invalid presence ID';
394
+ }
385
395
  }
386
396
  } else if (
387
397
  request.a === ACTIONS.bulkFetch ||
@@ -389,9 +399,18 @@ Agent.prototype._checkRequest = function(request) {
389
399
  request.a === ACTIONS.bulkUnsubscribe
390
400
  ) {
391
401
  // Bulk request
392
- if (request.c != null && typeof request.c !== 'string') return 'Invalid collection';
402
+ if (request.c != null) {
403
+ if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) {
404
+ return 'Invalid collection';
405
+ }
406
+ }
393
407
  if (typeof request.b !== 'object') return 'Invalid bulk subscribe data';
394
408
  }
409
+ if (request.ch != null) {
410
+ if (typeof request.ch !== 'string' || util.isDangerousProperty(request.ch)) {
411
+ return 'Invalid presence channel';
412
+ }
413
+ }
395
414
  };
396
415
 
397
416
  // Handle an incoming message from the client
@@ -483,7 +502,7 @@ function getQueryOptions(request) {
483
502
  fetch = [id];
484
503
  }
485
504
  } else {
486
- if (!fetchOps) fetchOps = {};
505
+ if (!fetchOps) fetchOps = Object.create(null);
487
506
  fetchOps[id] = version;
488
507
  }
489
508
  }
@@ -570,7 +589,7 @@ function getResultsData(results) {
570
589
  }
571
590
 
572
591
  function getMapResult(snapshotMap) {
573
- var data = {};
592
+ var data = Object.create(null);
574
593
  for (var id in snapshotMap) {
575
594
  var mapValue = snapshotMap[id];
576
595
  // fetchBulk / subscribeBulk map data can have either a Snapshot or an object
@@ -769,11 +788,7 @@ Agent.prototype._src = function() {
769
788
  Agent.prototype._broadcastPresence = function(presence, callback) {
770
789
  var agent = this;
771
790
  var backend = this.backend;
772
- var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = {});
773
- var previousRequest = requests[presence.id];
774
- if (!previousRequest || previousRequest.pv < presence.pv) {
775
- this.presenceRequests[presence.ch][presence.id] = presence;
776
- }
791
+ var presenceRequests = this.presenceRequests;
777
792
  var context = {
778
793
  presence: presence,
779
794
  collection: presence.c
@@ -781,6 +796,11 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
781
796
  var start = Date.now();
782
797
  backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
783
798
  if (error) return callback(error);
799
+ var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
800
+ var previousRequest = requests[presence.id];
801
+ if (!previousRequest || previousRequest.pv < presence.pv) {
802
+ presenceRequests[presence.ch][presence.id] = presence;
803
+ }
784
804
  backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
785
805
  if (error) return callback(error);
786
806
  var channel = agent._getPresenceChannel(presence.ch);
@@ -904,7 +924,9 @@ function createClientOp(request, clientId) {
904
924
  function shallowCopy(object) {
905
925
  var out = {};
906
926
  for (var key in object) {
907
- out[key] = object[key];
927
+ if (util.hasOwn(object, key)) {
928
+ out[key] = object[key];
929
+ }
908
930
  }
909
931
  return out;
910
932
  }
package/lib/backend.js CHANGED
@@ -30,7 +30,7 @@ function Backend(options) {
30
30
  this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB();
31
31
 
32
32
  // Map from projected collection -> {type, fields}
33
- this.projections = {};
33
+ this.projections = Object.create(null);
34
34
 
35
35
  this.suppressPublish = !!options.suppressPublish;
36
36
  this.maxSubmitRetries = options.maxSubmitRetries || null;
@@ -45,7 +45,7 @@ function Backend(options) {
45
45
  }
46
46
 
47
47
  // Map from event name to a list of middleware
48
- this.middleware = {};
48
+ this.middleware = Object.create(null);
49
49
 
50
50
  // The number of open agents for monitoring and testing memory leaks
51
51
  this.agentsCount = 0;
@@ -569,7 +569,7 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) {
569
569
  var projection = this.projections[index];
570
570
  var collection = (projection) ? projection.target : index;
571
571
  var backend = this;
572
- var streams = {};
572
+ var streams = Object.create(null);
573
573
  var doFetch = Array.isArray(versions);
574
574
  var ids = (doFetch) ? versions : Object.keys(versions);
575
575
  var request = {
@@ -40,7 +40,7 @@ function Connection(socket) {
40
40
 
41
41
  // Map of collection -> id -> doc object for created documents.
42
42
  // (created documents MUST BE UNIQUE)
43
- this.collections = {};
43
+ this.collections = Object.create(null);
44
44
 
45
45
  // Each query and snapshot request is created with an id that the server uses when it sends us
46
46
  // info about the request (updates, etc)
@@ -48,14 +48,14 @@ function Connection(socket) {
48
48
  this.nextSnapshotRequestId = 1;
49
49
 
50
50
  // Map from query ID -> query object.
51
- this.queries = {};
51
+ this.queries = Object.create(null);
52
52
 
53
53
  // Maps from channel -> presence objects
54
- this._presences = {};
54
+ this._presences = Object.create(null);
55
55
  this._docPresenceEmitter = new DocPresenceEmitter();
56
56
 
57
57
  // Map from snapshot request ID -> snapshot request
58
- this._snapshotRequests = {};
58
+ this._snapshotRequests = Object.create(null);
59
59
 
60
60
  // A unique message number for the given id
61
61
  this.seq = 1;
@@ -221,7 +221,7 @@ Connection.prototype.handleMessage = function(message) {
221
221
  if (!query) return;
222
222
  if (err) return query._handleError(err);
223
223
  if (message.diff) query._handleDiff(message.diff);
224
- if (message.hasOwnProperty('extra')) query._handleExtra(message.extra);
224
+ if (util.hasOwn(message, 'extra')) query._handleExtra(message.extra);
225
225
  return;
226
226
 
227
227
  case ACTIONS.bulkFetch:
@@ -376,7 +376,7 @@ Connection.prototype._setState = function(newState, reason) {
376
376
  };
377
377
 
378
378
  Connection.prototype.startBulk = function() {
379
- if (!this.bulk) this.bulk = {};
379
+ if (!this.bulk) this.bulk = Object.create(null);
380
380
  };
381
381
 
382
382
  Connection.prototype.endBulk = function() {
@@ -394,7 +394,7 @@ Connection.prototype.endBulk = function() {
394
394
  Connection.prototype._sendBulk = function(action, collection, values) {
395
395
  if (!values) return;
396
396
  var ids = [];
397
- var versions = {};
397
+ var versions = Object.create(null);
398
398
  var versionsCount = 0;
399
399
  var versionId;
400
400
  for (var id in values) {
@@ -426,9 +426,9 @@ Connection.prototype._sendActions = function(action, doc, version) {
426
426
  this._addDoc(doc);
427
427
  if (this.bulk) {
428
428
  // Bulk subscribe
429
- var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = {});
430
- var versions = actions[action] || (actions[action] = {});
431
- var isDuplicate = versions.hasOwnProperty(doc.id);
429
+ var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = Object.create(null));
430
+ var versions = actions[action] || (actions[action] = Object.create(null));
431
+ var isDuplicate = util.hasOwn(versions, doc.id);
432
432
  versions[doc.id] = version;
433
433
  return isDuplicate;
434
434
  } else {
@@ -515,7 +515,7 @@ Connection.prototype.getExisting = function(collection, id) {
515
515
  */
516
516
  Connection.prototype.get = function(collection, id) {
517
517
  var docs = this.collections[collection] ||
518
- (this.collections[collection] = {});
518
+ (this.collections[collection] = Object.create(null));
519
519
 
520
520
  var doc = docs[id];
521
521
  if (!doc) {
@@ -542,7 +542,7 @@ Connection.prototype._destroyDoc = function(doc) {
542
542
  Connection.prototype._addDoc = function(doc) {
543
543
  var docs = this.collections[doc.collection];
544
544
  if (!docs) {
545
- docs = this.collections[doc.collection] = {};
545
+ docs = this.collections[doc.collection] = Object.create(null);
546
546
  }
547
547
  if (docs[doc.id] !== doc) {
548
548
  docs[doc.id] = doc;
@@ -12,9 +12,9 @@ var EVENTS = [
12
12
  module.exports = DocPresenceEmitter;
13
13
 
14
14
  function DocPresenceEmitter() {
15
- this._docs = {};
16
- this._forwarders = {};
17
- this._emitters = {};
15
+ this._docs = Object.create(null);
16
+ this._forwarders = Object.create(null);
17
+ this._emitters = Object.create(null);
18
18
  }
19
19
 
20
20
  DocPresenceEmitter.prototype.addEventListener = function(doc, event, listener) {
@@ -13,7 +13,7 @@ function LocalDocPresence(presence, presenceId) {
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
14
  this._emitter = this.connection._docPresenceEmitter;
15
15
  this._isSending = false;
16
- this._docDataVersionByPresenceVersion = {};
16
+ this._docDataVersionByPresenceVersion = Object.create(null);
17
17
 
18
18
  this._opHandler = this._transformAgainstOp.bind(this);
19
19
  this._createOrDelHandler = this._handleCreateOrDel.bind(this);
@@ -68,7 +68,7 @@ LocalDocPresence.prototype._sendPending = function() {
68
68
  });
69
69
 
70
70
  presence._pendingMessages = [];
71
- presence._docDataVersionByPresenceVersion = {};
71
+ presence._docDataVersionByPresenceVersion = Object.create(null);
72
72
  });
73
73
  };
74
74
 
@@ -118,7 +118,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() {
118
118
  LocalDocPresence.prototype._handleLoad = function() {
119
119
  this.value = null;
120
120
  this._pendingMessages = [];
121
- this._docDataVersionByPresenceVersion = {};
121
+ this._docDataVersionByPresenceVersion = Object.create(null);
122
122
  };
123
123
 
124
124
  LocalDocPresence.prototype._message = function() {
@@ -18,7 +18,7 @@ function LocalPresence(presence, presenceId) {
18
18
  this.value = null;
19
19
 
20
20
  this._pendingMessages = [];
21
- this._callbacksByPresenceVersion = {};
21
+ this._callbacksByPresenceVersion = Object.create(null);
22
22
  }
23
23
  emitter.mixin(LocalPresence);
24
24
 
@@ -19,11 +19,11 @@ function Presence(connection, channel) {
19
19
 
20
20
  this.wantSubscribe = false;
21
21
  this.subscribed = false;
22
- this.remotePresences = {};
23
- this.localPresences = {};
22
+ this.remotePresences = Object.create(null);
23
+ this.localPresences = Object.create(null);
24
24
 
25
- this._remotePresenceInstances = {};
26
- this._subscriptionCallbacksBySeq = {};
25
+ this._remotePresenceInstances = Object.create(null);
26
+ this._subscriptionCallbacksBySeq = Object.create(null);
27
27
  this._wantsDestroy = false;
28
28
  }
29
29
  emitter.mixin(Presence);
package/lib/db/index.js CHANGED
@@ -26,7 +26,7 @@ DB.prototype.getSnapshot = function(collection, id, fields, options, callback) {
26
26
  };
27
27
 
28
28
  DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callback) {
29
- var results = {};
29
+ var results = Object.create(null);
30
30
  var db = this;
31
31
  async.each(ids, function(id, eachCb) {
32
32
  db.getSnapshot(collection, id, fields, options, function(err, snapshot) {
@@ -50,7 +50,7 @@ DB.prototype.getOpsToSnapshot = function(collection, id, from, snapshot, options
50
50
  };
51
51
 
52
52
  DB.prototype.getOpsBulk = function(collection, fromMap, toMap, options, callback) {
53
- var results = {};
53
+ var results = Object.create(null);
54
54
  var db = this;
55
55
  async.forEachOf(fromMap, function(from, id, eachCb) {
56
56
  var to = toMap && toMap[id];
@@ -83,7 +83,7 @@ DB.prototype.query = function(collection, query, fields, options, callback) {
83
83
  };
84
84
 
85
85
  DB.prototype.queryPoll = function(collection, query, options, callback) {
86
- var fields = {};
86
+ var fields = Object.create(null);
87
87
  this.query(collection, query, fields, options, function(err, snapshots, extra) {
88
88
  if (err) return callback(err);
89
89
  var ids = [];
package/lib/db/memory.js CHANGED
@@ -17,12 +17,12 @@ function MemoryDB(options) {
17
17
  DB.call(this, options);
18
18
 
19
19
  // Map from collection name -> doc id -> doc snapshot ({v:, type:, data:})
20
- this.docs = {};
20
+ this.docs = Object.create(null);
21
21
 
22
22
  // Map from collection name -> doc id -> list of operations. Operations
23
23
  // don't store their version - instead their version is simply the index in
24
24
  // the list.
25
- this.ops = {};
25
+ this.ops = Object.create(null);
26
26
 
27
27
  this.closed = false;
28
28
  };
@@ -139,7 +139,7 @@ MemoryDB.prototype._writeOpSync = function(collection, id, op) {
139
139
  // object will be passed in with a type property. If there is no type property,
140
140
  // it should be considered a delete
141
141
  MemoryDB.prototype._writeSnapshotSync = function(collection, id, snapshot) {
142
- var collectionDocs = this.docs[collection] || (this.docs[collection] = {});
142
+ var collectionDocs = this.docs[collection] || (this.docs[collection] = Object.create(null));
143
143
  if (!snapshot.type) {
144
144
  delete collectionDocs[id];
145
145
  } else {
@@ -165,7 +165,7 @@ MemoryDB.prototype._getSnapshotSync = function(collection, id, includeMetadata)
165
165
  };
166
166
 
167
167
  MemoryDB.prototype._getOpLogSync = function(collection, id) {
168
- var collectionOps = this.ops[collection] || (this.ops[collection] = {});
168
+ var collectionOps = this.ops[collection] || (this.ops[collection] = Object.create(null));
169
169
  return collectionOps[id] || (collectionOps[id] = []);
170
170
  };
171
171
 
@@ -5,7 +5,7 @@ var SUPPORTED_METHODS = [
5
5
  ];
6
6
 
7
7
  function Logger() {
8
- var defaultMethods = {};
8
+ var defaultMethods = Object.create(null);
9
9
  SUPPORTED_METHODS.forEach(function(method) {
10
10
  // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628
11
11
  defaultMethods[method] = console[method].bind(console);
@@ -20,7 +20,7 @@ function MemoryMilestoneDB(options) {
20
20
  MilestoneDB.call(this, options);
21
21
 
22
22
  // Map from collection name -> doc id -> array of milestone snapshots
23
- this._milestoneSnapshots = {};
23
+ this._milestoneSnapshots = Object.create(null);
24
24
  }
25
25
 
26
26
  MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype);
@@ -103,7 +103,8 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, br
103
103
  };
104
104
 
105
105
  MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) {
106
- var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {});
106
+ var collectionSnapshots = this._milestoneSnapshots[collection] ||
107
+ (this._milestoneSnapshots[collection] = Object.create(null));
107
108
  return collectionSnapshots[id] || (collectionSnapshots[id] = []);
108
109
  };
109
110
 
package/lib/ot.js CHANGED
@@ -108,6 +108,20 @@ function applyOpEdit(snapshot, edit) {
108
108
  var type = types.map[snapshot.type];
109
109
  if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
110
110
 
111
+ if (type.name === 'json0' && Array.isArray(edit)) {
112
+ for (var i = 0; i < edit.length; i++) {
113
+ var opComponent = edit[i];
114
+ if (Array.isArray(opComponent.p)) {
115
+ for (var j = 0; j < opComponent.p.length; j++) {
116
+ var pathSegment = opComponent.p[j];
117
+ if (util.isDangerousProperty(pathSegment)) {
118
+ return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, 'Invalid path segment');
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
111
125
  try {
112
126
  snapshot.data = type.apply(snapshot.data, edit);
113
127
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  var json0 = require('ot-json0').type;
2
2
  var ShareDBError = require('./error');
3
+ var util = require('./util');
3
4
 
4
5
  var ERROR_CODE = ShareDBError.CODES;
5
6
 
@@ -119,7 +120,7 @@ function projectData(fields, data) {
119
120
  // Shallow copy of each field
120
121
  var result = {};
121
122
  for (var key in fields) {
122
- if (data.hasOwnProperty(key)) {
123
+ if (util.hasOwn(data, key)) {
123
124
  result[key] = data[key];
124
125
  }
125
126
  }
@@ -13,12 +13,12 @@ function PubSub(options) {
13
13
  this.nextStreamId = 1;
14
14
  this.streamsCount = 0;
15
15
  // Maps channel -> id -> stream
16
- this.streams = {};
16
+ this.streams = Object.create(null);
17
17
  // State for tracking subscriptions. We track this.subscribed separately from
18
18
  // the streams, since the stream gets added synchronously, and the subscribe
19
19
  // isn't complete until the callback returns from Redis
20
20
  // Maps channel -> true
21
- this.subscribed = {};
21
+ this.subscribed = Object.create(null);
22
22
 
23
23
  var pubsub = this;
24
24
  this._defaultCallback = function(err) {
@@ -111,7 +111,7 @@ PubSub.prototype._createStream = function(channel) {
111
111
  });
112
112
 
113
113
  this.streamsCount++;
114
- var map = this.streams[channel] || (this.streams[channel] = {});
114
+ var map = this.streams[channel] || (this.streams[channel] = Object.create(null));
115
115
  stream.id = this.nextStreamId++;
116
116
  map[stream.id] = stream;
117
117
 
@@ -43,7 +43,7 @@ function ReadSnapshotsRequest(collection, snapshots, snapshotType) {
43
43
  */
44
44
  ReadSnapshotsRequest.prototype.rejectSnapshotRead = function(snapshot, error) {
45
45
  if (!this._idToError) {
46
- this._idToError = {};
46
+ this._idToError = Object.create(null);
47
47
  }
48
48
  this._idToError[snapshot.id] = error;
49
49
  };
@@ -26,7 +26,7 @@ function SubmitRequest(backend, agent, index, id, op, options) {
26
26
  // Set as this request is sent through middleware
27
27
  this.action = null;
28
28
  // For custom use in middleware
29
- this.custom = {};
29
+ this.custom = Object.create(null);
30
30
 
31
31
  // Whether or not to store a milestone snapshot. If left as null, the milestone
32
32
  // snapshots are saved according to the interval provided to the milestone db
package/lib/types.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  exports.defaultType = require('ot-json0').type;
3
3
 
4
- exports.map = {};
4
+ exports.map = Object.create(null);
5
5
 
6
6
  exports.register = function(type) {
7
7
  if (type.name) exports.map[type.name] = type;
package/lib/util.js CHANGED
@@ -7,6 +7,9 @@ exports.hasKeys = function(object) {
7
7
  return false;
8
8
  };
9
9
 
10
+ var hasOwn;
11
+ exports.hasOwn = hasOwn = Object.hasOwn || Object.prototype.hasOwnProperty.call;
12
+
10
13
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
11
14
  exports.isInteger = Number.isInteger || function(value) {
12
15
  return typeof value === 'number' &&
@@ -29,7 +32,7 @@ exports.dig = function() {
29
32
  var obj = arguments[0];
30
33
  for (var i = 1; i < arguments.length; i++) {
31
34
  var key = arguments[i];
32
- obj = obj[key] || (i === arguments.length - 1 ? undefined : {});
35
+ obj = hasOwn(obj, key) ? obj[key] : (i === arguments.length - 1 ? undefined : Object.create(null));
33
36
  }
34
37
  return obj;
35
38
  };
@@ -39,8 +42,8 @@ exports.digOrCreate = function() {
39
42
  var createCallback = arguments[arguments.length - 1];
40
43
  for (var i = 1; i < arguments.length - 1; i++) {
41
44
  var key = arguments[i];
42
- obj = obj[key] ||
43
- (obj[key] = i === arguments.length - 2 ? createCallback() : {});
45
+ obj = hasOwn(obj, key) ? obj[key] :
46
+ (obj[key] = i === arguments.length - 2 ? createCallback() : Object.create(null));
44
47
  }
45
48
  return obj;
46
49
  };
@@ -50,7 +53,7 @@ exports.digAndRemove = function() {
50
53
  var objects = [obj];
51
54
  for (var i = 1; i < arguments.length - 1; i++) {
52
55
  var key = arguments[i];
53
- if (!obj.hasOwnProperty(key)) break;
56
+ if (!hasOwn(obj, key)) break;
54
57
  obj = obj[key];
55
58
  objects.push(obj);
56
59
  };
@@ -101,3 +104,12 @@ exports.clone = function(obj) {
101
104
  return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
102
105
  };
103
106
 
107
+ var objectProtoPropNames = Object.create(null);
108
+ Object.getOwnPropertyNames(Object.prototype).forEach(function(prop) {
109
+ if (prop !== '__proto__') {
110
+ objectProtoPropNames[prop] = true;
111
+ }
112
+ });
113
+ exports.isDangerousProperty = function(propName) {
114
+ return propName === '__proto__' || objectProtoPropNames[propName];
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -617,6 +617,106 @@ describe('Doc', function() {
617
617
  });
618
618
  });
619
619
 
620
+ describe('errors on ops that could cause prototype corruption', function() {
621
+ function expectReceiveError(
622
+ connection,
623
+ collectionName,
624
+ docId,
625
+ expectedError,
626
+ done
627
+ ) {
628
+ connection.on('receive', function(request) {
629
+ var message = request.data;
630
+ if (message.c === collectionName && message.d === docId) {
631
+ if ('error' in message) {
632
+ request.data = null; // Stop further processing of the message
633
+ if (message.error.message === expectedError) {
634
+ return done();
635
+ } else {
636
+ return done('Unexpected ShareDB error: ' + message.error.message);
637
+ }
638
+ } else {
639
+ return done('Expected error on ' + collectionName + '.' + docId + ' but got no error');
640
+ }
641
+ }
642
+ });
643
+ }
644
+
645
+ ['__proto__', 'constructor'].forEach(function(badProp) {
646
+ it('Rejects ops with collection ' + badProp, function(done) {
647
+ var collectionName = badProp;
648
+ var docId = 'test-doc';
649
+ expectReceiveError(this.connection, collectionName, docId, 'Invalid collection', done);
650
+ this.connection.send({
651
+ a: 'op',
652
+ c: collectionName,
653
+ d: docId,
654
+ v: 0,
655
+ seq: this.connection.seq++,
656
+ x: {},
657
+ create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Test doc'}}
658
+ });
659
+ });
660
+
661
+ it('Rejects ops with doc id ' + badProp, function(done) {
662
+ var collectionName = 'test-collection';
663
+ var docId = badProp;
664
+ expectReceiveError(this.connection, collectionName, docId, 'Invalid id', done);
665
+ this.connection.send({
666
+ a: 'op',
667
+ c: collectionName,
668
+ d: docId,
669
+ v: 0,
670
+ seq: this.connection.seq++,
671
+ x: {},
672
+ create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Some doc'}}
673
+ });
674
+ });
675
+
676
+ it('Rejects ops with ' + badProp + ' as first path segment', function(done) {
677
+ var connection = this.connection;
678
+ var collectionName = 'test-collection';
679
+ var docId = 'test-doc';
680
+ connection.get(collectionName, docId).create({id: docId}, function(err) {
681
+ if (err) {
682
+ return done(err);
683
+ }
684
+ expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done);
685
+ connection.send({
686
+ a: 'op',
687
+ c: collectionName,
688
+ d: docId,
689
+ v: 1,
690
+ seq: connection.seq++,
691
+ x: {},
692
+ op: [{p: [badProp, 'toString'], oi: 'oops'}]
693
+ });
694
+ });
695
+ });
696
+
697
+ it('Rejects ops with ' + badProp + ' as later path segment', function(done) {
698
+ var connection = this.connection;
699
+ var collectionName = 'test-collection';
700
+ var docId = 'test-doc';
701
+ connection.get(collectionName, docId).create({id: docId}, function(err) {
702
+ if (err) {
703
+ return done(err);
704
+ }
705
+ expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done);
706
+ connection.send({
707
+ a: 'op',
708
+ c: collectionName,
709
+ d: docId,
710
+ v: 1,
711
+ seq: connection.seq++,
712
+ x: {},
713
+ op: [{p: ['foo', badProp], oi: 'oops'}]
714
+ });
715
+ });
716
+ });
717
+ });
718
+ });
719
+
620
720
  describe('toSnapshot', function() {
621
721
  var doc;
622
722
  beforeEach(function(done) {
@@ -466,6 +466,16 @@ describe('Presence', function() {
466
466
  }).to.throw();
467
467
  });
468
468
 
469
+ ['__proto__', 'constructor'].forEach(function(badProp) {
470
+ it('Rejects presence with channel ' + badProp, function(done) {
471
+ var presence = connection1.getPresence(badProp);
472
+ presence.subscribe(function(err) {
473
+ expect(err).to.be.an('error').to.haveOwnProperty('message', 'Invalid presence channel');
474
+ done();
475
+ });
476
+ });
477
+ });
478
+
469
479
  it('assigns an ID if one is not provided', function() {
470
480
  var localPresence = presence1.create();
471
481
  expect(localPresence.presenceId).to.be.ok;
@@ -659,6 +669,7 @@ describe('Presence', function() {
659
669
  function(next) {
660
670
  presence2.on('receive', function(id, value) {
661
671
  expect(value).to.eql({index: 6});
672
+ expect(connection1.agent.presenceRequests['test-channel']['presence-1'].p).to.eql({index: 6});
662
673
  next();
663
674
  });
664
675
  localPresence1.submit({index: 5}, errorHandler(done));
@@ -681,6 +692,7 @@ describe('Presence', function() {
681
692
  });
682
693
  localPresence1.submit({index: 5}, function(error) {
683
694
  expect(error.message).to.contain('bad!');
695
+ expect(connection1.agent.presenceRequests['test-channel']).not.to.be.ok;
684
696
  next();
685
697
  });
686
698
  }