From 06cc3877fd9b53d1ddc3ecdd3df11f68fbdbb100 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 5 Dec 2023 10:07:03 -0800 Subject: [PATCH] Fix and guard against prototype pollution issues --- lib/agent.js | 57 +++++++---- lib/backend.js | 6 +- lib/client/connection.js | 24 ++--- lib/client/presence/doc-presence-emitter.js | 6 +- lib/client/presence/local-doc-presence.js | 6 +- lib/client/presence/local-presence.js | 2 +- lib/client/presence/presence.js | 8 +- lib/db/index.js | 6 +- lib/db/memory.js | 8 +- lib/logger/logger.js | 2 +- lib/milestone-db/memory.js | 5 +- lib/ot.js | 14 +++ lib/projections.js | 3 +- lib/pubsub/index.js | 6 +- lib/read-snapshots-request.js | 2 +- lib/submit-request.js | 2 +- lib/types.js | 2 +- lib/util.js | 20 +++- test/client/doc.js | 100 ++++++++++++++++++++ test/client/presence/presence.js | 10 ++ 20 files changed, 224 insertions(+), 65 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 5c74dab2f..294c53ffc 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -31,23 +31,23 @@ function Agent(backend, stream) { // We need to track which documents are subscribed by the client. This is a // map of collection -> id -> stream - this.subscribedDocs = {}; + this.subscribedDocs = Object.create(null); // Map from queryId -> emitter - this.subscribedQueries = {}; + this.subscribedQueries = Object.create(null); // Track which documents are subscribed to presence by the client. This is a // map of channel -> stream - this.subscribedPresences = {}; + this.subscribedPresences = Object.create(null); // Highest seq received for a subscription request. Any seq lower than this // value is stale, and should be ignored. Used for keeping the subscription // state in sync with the client's desired state. Map of channel -> seq - this.presenceSubscriptionSeq = {}; + this.presenceSubscriptionSeq = Object.create(null); // Keep track of the last request that has been sent by each local presence // belonging to this agent. This is used to generate a new disconnection // request if the client disconnects ungracefully. This is a // map of channel -> id -> request - this.presenceRequests = {}; + this.presenceRequests = Object.create(null); // We need to track this manually to make sure we don't reply to messages // after the stream was closed. @@ -56,7 +56,7 @@ function Agent(backend, stream) { // For custom use in middleware. The agent is a convenient place to cache // session state in memory. It is in memory only as long as the session is // active, and it is passed to each middleware call - this.custom = {}; + this.custom = Object.create(null); // The first message received over the connection. Stored to warn if messages // are being sent before the handshake. @@ -95,19 +95,19 @@ Agent.prototype._cleanup = function() { stream.destroy(); } } - this.subscribedDocs = {}; + this.subscribedDocs = Object.create(null); for (var channel in this.subscribedPresences) { this.subscribedPresences[channel].destroy(); } - this.subscribedPresences = {}; + this.subscribedPresences = Object.create(null); // Clean up query subscription streams for (var id in this.subscribedQueries) { var emitter = this.subscribedQueries[id]; emitter.destroy(); } - this.subscribedQueries = {}; + this.subscribedQueries = Object.create(null); }; /** @@ -117,7 +117,7 @@ Agent.prototype._cleanup = function() { Agent.prototype._subscribeToStream = function(collection, id, stream) { if (this.closed) return stream.destroy(); - var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = {}); + var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = Object.create(null)); // If already subscribed to this document, destroy the previously subscribed stream var previous = streams[id]; @@ -373,15 +373,25 @@ Agent.prototype._checkRequest = function(request) { request.a === ACTIONS.unsubscribe || request.a === ACTIONS.presence) { // Doc-based request. - if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; - if (request.d != null && typeof request.d !== 'string') return 'Invalid id'; + if (request.c != null) { + if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) { + return 'Invalid collection'; + } + } + if (request.d != null) { + if (typeof request.d !== 'string' || util.isDangerousProperty(request.d)) { + return 'Invalid id'; + } + } if (request.a === ACTIONS.op || request.a === ACTIONS.presence) { if (request.v != null && (typeof request.v !== 'number' || request.v < 0)) return 'Invalid version'; } if (request.a === ACTIONS.presence) { - if (typeof request.id !== 'string') return 'Missing presence ID'; + if (typeof request.id !== 'string' || util.isDangerousProperty(request.id)) { + return 'Invalid presence ID'; + } } } else if ( request.a === ACTIONS.bulkFetch || @@ -389,9 +399,18 @@ Agent.prototype._checkRequest = function(request) { request.a === ACTIONS.bulkUnsubscribe ) { // Bulk request - if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; + if (request.c != null) { + if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) { + return 'Invalid collection'; + } + } if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; } + if (request.ch != null) { + if (typeof request.ch !== 'string' || util.isDangerousProperty(request.ch)) { + return 'Invalid presence channel'; + } + } }; // Handle an incoming message from the client @@ -483,7 +502,7 @@ function getQueryOptions(request) { fetch = [id]; } } else { - if (!fetchOps) fetchOps = {}; + if (!fetchOps) fetchOps = Object.create(null); fetchOps[id] = version; } } @@ -570,7 +589,7 @@ function getResultsData(results) { } function getMapResult(snapshotMap) { - var data = {}; + var data = Object.create(null); for (var id in snapshotMap) { var mapValue = snapshotMap[id]; // fetchBulk / subscribeBulk map data can have either a Snapshot or an object @@ -769,7 +788,7 @@ Agent.prototype._src = function() { Agent.prototype._broadcastPresence = function(presence, callback) { var agent = this; var backend = this.backend; - var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = {}); + var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = Object.create(null)); var previousRequest = requests[presence.id]; if (!previousRequest || previousRequest.pv < presence.pv) { this.presenceRequests[presence.ch][presence.id] = presence; @@ -904,7 +923,9 @@ function createClientOp(request, clientId) { function shallowCopy(object) { var out = {}; for (var key in object) { - out[key] = object[key]; + if (util.hasOwn(object, key)) { + out[key] = object[key]; + } } return out; } diff --git a/lib/backend.js b/lib/backend.js index e35331e91..c69693a56 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -30,7 +30,7 @@ function Backend(options) { this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB(); // Map from projected collection -> {type, fields} - this.projections = {}; + this.projections = Object.create(null); this.suppressPublish = !!options.suppressPublish; this.maxSubmitRetries = options.maxSubmitRetries || null; @@ -45,7 +45,7 @@ function Backend(options) { } // Map from event name to a list of middleware - this.middleware = {}; + this.middleware = Object.create(null); // The number of open agents for monitoring and testing memory leaks this.agentsCount = 0; @@ -569,7 +569,7 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) { var projection = this.projections[index]; var collection = (projection) ? projection.target : index; var backend = this; - var streams = {}; + var streams = Object.create(null); var doFetch = Array.isArray(versions); var ids = (doFetch) ? versions : Object.keys(versions); var request = { diff --git a/lib/client/connection.js b/lib/client/connection.js index 896bd03e2..134931c40 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -40,7 +40,7 @@ function Connection(socket) { // Map of collection -> id -> doc object for created documents. // (created documents MUST BE UNIQUE) - this.collections = {}; + this.collections = Object.create(null); // Each query and snapshot request is created with an id that the server uses when it sends us // info about the request (updates, etc) @@ -48,14 +48,14 @@ function Connection(socket) { this.nextSnapshotRequestId = 1; // Map from query ID -> query object. - this.queries = {}; + this.queries = Object.create(null); // Maps from channel -> presence objects - this._presences = {}; + this._presences = Object.create(null); this._docPresenceEmitter = new DocPresenceEmitter(); // Map from snapshot request ID -> snapshot request - this._snapshotRequests = {}; + this._snapshotRequests = Object.create(null); // A unique message number for the given id this.seq = 1; @@ -221,7 +221,7 @@ Connection.prototype.handleMessage = function(message) { if (!query) return; if (err) return query._handleError(err); if (message.diff) query._handleDiff(message.diff); - if (message.hasOwnProperty('extra')) query._handleExtra(message.extra); + if (util.hasOwn(message, 'extra')) query._handleExtra(message.extra); return; case ACTIONS.bulkFetch: @@ -376,7 +376,7 @@ Connection.prototype._setState = function(newState, reason) { }; Connection.prototype.startBulk = function() { - if (!this.bulk) this.bulk = {}; + if (!this.bulk) this.bulk = Object.create(null); }; Connection.prototype.endBulk = function() { @@ -394,7 +394,7 @@ Connection.prototype.endBulk = function() { Connection.prototype._sendBulk = function(action, collection, values) { if (!values) return; var ids = []; - var versions = {}; + var versions = Object.create(null); var versionsCount = 0; var versionId; for (var id in values) { @@ -426,9 +426,9 @@ Connection.prototype._sendActions = function(action, doc, version) { this._addDoc(doc); if (this.bulk) { // Bulk subscribe - var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = {}); - var versions = actions[action] || (actions[action] = {}); - var isDuplicate = versions.hasOwnProperty(doc.id); + var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = Object.create(null)); + var versions = actions[action] || (actions[action] = Object.create(null)); + var isDuplicate = util.hasOwn(versions, doc.id); versions[doc.id] = version; return isDuplicate; } else { @@ -515,7 +515,7 @@ Connection.prototype.getExisting = function(collection, id) { */ Connection.prototype.get = function(collection, id) { var docs = this.collections[collection] || - (this.collections[collection] = {}); + (this.collections[collection] = Object.create(null)); var doc = docs[id]; if (!doc) { @@ -542,7 +542,7 @@ Connection.prototype._destroyDoc = function(doc) { Connection.prototype._addDoc = function(doc) { var docs = this.collections[doc.collection]; if (!docs) { - docs = this.collections[doc.collection] = {}; + docs = this.collections[doc.collection] = Object.create(null); } if (docs[doc.id] !== doc) { docs[doc.id] = doc; diff --git a/lib/client/presence/doc-presence-emitter.js b/lib/client/presence/doc-presence-emitter.js index 47ceb98fb..2a8797cea 100644 --- a/lib/client/presence/doc-presence-emitter.js +++ b/lib/client/presence/doc-presence-emitter.js @@ -12,9 +12,9 @@ var EVENTS = [ module.exports = DocPresenceEmitter; function DocPresenceEmitter() { - this._docs = {}; - this._forwarders = {}; - this._emitters = {}; + this._docs = Object.create(null); + this._forwarders = Object.create(null); + this._emitters = Object.create(null); } DocPresenceEmitter.prototype.addEventListener = function(doc, event, listener) { diff --git a/lib/client/presence/local-doc-presence.js b/lib/client/presence/local-doc-presence.js index ba752becf..fc693e661 100644 --- a/lib/client/presence/local-doc-presence.js +++ b/lib/client/presence/local-doc-presence.js @@ -13,7 +13,7 @@ function LocalDocPresence(presence, presenceId) { this._doc = this.connection.get(this.collection, this.id); this._emitter = this.connection._docPresenceEmitter; this._isSending = false; - this._docDataVersionByPresenceVersion = {}; + this._docDataVersionByPresenceVersion = Object.create(null); this._opHandler = this._transformAgainstOp.bind(this); this._createOrDelHandler = this._handleCreateOrDel.bind(this); @@ -68,7 +68,7 @@ LocalDocPresence.prototype._sendPending = function() { }); presence._pendingMessages = []; - presence._docDataVersionByPresenceVersion = {}; + presence._docDataVersionByPresenceVersion = Object.create(null); }); }; @@ -118,7 +118,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() { LocalDocPresence.prototype._handleLoad = function() { this.value = null; this._pendingMessages = []; - this._docDataVersionByPresenceVersion = {}; + this._docDataVersionByPresenceVersion = Object.create(null); }; LocalDocPresence.prototype._message = function() { diff --git a/lib/client/presence/local-presence.js b/lib/client/presence/local-presence.js index 204007617..ae70c76ab 100644 --- a/lib/client/presence/local-presence.js +++ b/lib/client/presence/local-presence.js @@ -18,7 +18,7 @@ function LocalPresence(presence, presenceId) { this.value = null; this._pendingMessages = []; - this._callbacksByPresenceVersion = {}; + this._callbacksByPresenceVersion = Object.create(null); } emitter.mixin(LocalPresence); diff --git a/lib/client/presence/presence.js b/lib/client/presence/presence.js index 03d16b92e..749350d10 100644 --- a/lib/client/presence/presence.js +++ b/lib/client/presence/presence.js @@ -19,11 +19,11 @@ function Presence(connection, channel) { this.wantSubscribe = false; this.subscribed = false; - this.remotePresences = {}; - this.localPresences = {}; + this.remotePresences = Object.create(null); + this.localPresences = Object.create(null); - this._remotePresenceInstances = {}; - this._subscriptionCallbacksBySeq = {}; + this._remotePresenceInstances = Object.create(null); + this._subscriptionCallbacksBySeq = Object.create(null); this._wantsDestroy = false; } emitter.mixin(Presence); diff --git a/lib/db/index.js b/lib/db/index.js index d7d47ce7d..043276bf4 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -26,7 +26,7 @@ DB.prototype.getSnapshot = function(collection, id, fields, options, callback) { }; DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callback) { - var results = {}; + var results = Object.create(null); var db = this; async.each(ids, function(id, eachCb) { db.getSnapshot(collection, id, fields, options, function(err, snapshot) { @@ -50,7 +50,7 @@ DB.prototype.getOpsToSnapshot = function(collection, id, from, snapshot, options }; DB.prototype.getOpsBulk = function(collection, fromMap, toMap, options, callback) { - var results = {}; + var results = Object.create(null); var db = this; async.forEachOf(fromMap, function(from, id, eachCb) { var to = toMap && toMap[id]; @@ -83,7 +83,7 @@ DB.prototype.query = function(collection, query, fields, options, callback) { }; DB.prototype.queryPoll = function(collection, query, options, callback) { - var fields = {}; + var fields = Object.create(null); this.query(collection, query, fields, options, function(err, snapshots, extra) { if (err) return callback(err); var ids = []; diff --git a/lib/db/memory.js b/lib/db/memory.js index 286e72dd7..568c52aa8 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -17,12 +17,12 @@ function MemoryDB(options) { DB.call(this, options); // Map from collection name -> doc id -> doc snapshot ({v:, type:, data:}) - this.docs = {}; + this.docs = Object.create(null); // Map from collection name -> doc id -> list of operations. Operations // don't store their version - instead their version is simply the index in // the list. - this.ops = {}; + this.ops = Object.create(null); this.closed = false; }; @@ -139,7 +139,7 @@ MemoryDB.prototype._writeOpSync = function(collection, id, op) { // object will be passed in with a type property. If there is no type property, // it should be considered a delete MemoryDB.prototype._writeSnapshotSync = function(collection, id, snapshot) { - var collectionDocs = this.docs[collection] || (this.docs[collection] = {}); + var collectionDocs = this.docs[collection] || (this.docs[collection] = Object.create(null)); if (!snapshot.type) { delete collectionDocs[id]; } else { @@ -165,7 +165,7 @@ MemoryDB.prototype._getSnapshotSync = function(collection, id, includeMetadata) }; MemoryDB.prototype._getOpLogSync = function(collection, id) { - var collectionOps = this.ops[collection] || (this.ops[collection] = {}); + var collectionOps = this.ops[collection] || (this.ops[collection] = Object.create(null)); return collectionOps[id] || (collectionOps[id] = []); }; diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 3c70e5a53..2e4858798 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -5,7 +5,7 @@ var SUPPORTED_METHODS = [ ]; function Logger() { - var defaultMethods = {}; + var defaultMethods = Object.create(null); SUPPORTED_METHODS.forEach(function(method) { // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628 defaultMethods[method] = console[method].bind(console); diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index efa332dc6..00622363a 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -20,7 +20,7 @@ function MemoryMilestoneDB(options) { MilestoneDB.call(this, options); // Map from collection name -> doc id -> array of milestone snapshots - this._milestoneSnapshots = {}; + this._milestoneSnapshots = Object.create(null); } MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); @@ -103,7 +103,8 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, br }; MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) { - var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {}); + var collectionSnapshots = this._milestoneSnapshots[collection] || + (this._milestoneSnapshots[collection] = Object.create(null)); return collectionSnapshots[id] || (collectionSnapshots[id] = []); }; diff --git a/lib/ot.js b/lib/ot.js index e20c77932..5cf349f60 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -108,6 +108,20 @@ function applyOpEdit(snapshot, edit) { var type = types.map[snapshot.type]; if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); + if (type.name === 'json0' && Array.isArray(edit)) { + for (var i = 0; i < edit.length; i++) { + var opComponent = edit[i]; + if (Array.isArray(opComponent.p)) { + for (var j = 0; j < opComponent.p.length; j++) { + var pathSegment = opComponent.p[j]; + if (util.isDangerousProperty(pathSegment)) { + return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, 'Invalid path segment'); + } + } + } + } + } + try { snapshot.data = type.apply(snapshot.data, edit); } catch (err) { diff --git a/lib/projections.js b/lib/projections.js index be4adbdab..b9a43c201 100644 --- a/lib/projections.js +++ b/lib/projections.js @@ -1,5 +1,6 @@ var json0 = require('ot-json0').type; var ShareDBError = require('./error'); +var util = require('./util'); var ERROR_CODE = ShareDBError.CODES; @@ -119,7 +120,7 @@ function projectData(fields, data) { // Shallow copy of each field var result = {}; for (var key in fields) { - if (data.hasOwnProperty(key)) { + if (util.hasOwn(data, key)) { result[key] = data[key]; } } diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index 67b51ebc4..5f8fad1f6 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -13,12 +13,12 @@ function PubSub(options) { this.nextStreamId = 1; this.streamsCount = 0; // Maps channel -> id -> stream - this.streams = {}; + this.streams = Object.create(null); // State for tracking subscriptions. We track this.subscribed separately from // the streams, since the stream gets added synchronously, and the subscribe // isn't complete until the callback returns from Redis // Maps channel -> true - this.subscribed = {}; + this.subscribed = Object.create(null); var pubsub = this; this._defaultCallback = function(err) { @@ -111,7 +111,7 @@ PubSub.prototype._createStream = function(channel) { }); this.streamsCount++; - var map = this.streams[channel] || (this.streams[channel] = {}); + var map = this.streams[channel] || (this.streams[channel] = Object.create(null)); stream.id = this.nextStreamId++; map[stream.id] = stream; diff --git a/lib/read-snapshots-request.js b/lib/read-snapshots-request.js index 7f2937f16..8a595698f 100644 --- a/lib/read-snapshots-request.js +++ b/lib/read-snapshots-request.js @@ -43,7 +43,7 @@ function ReadSnapshotsRequest(collection, snapshots, snapshotType) { */ ReadSnapshotsRequest.prototype.rejectSnapshotRead = function(snapshot, error) { if (!this._idToError) { - this._idToError = {}; + this._idToError = Object.create(null); } this._idToError[snapshot.id] = error; }; diff --git a/lib/submit-request.js b/lib/submit-request.js index 2c8ecf668..b1e1b066d 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -26,7 +26,7 @@ function SubmitRequest(backend, agent, index, id, op, options) { // Set as this request is sent through middleware this.action = null; // For custom use in middleware - this.custom = {}; + this.custom = Object.create(null); // Whether or not to store a milestone snapshot. If left as null, the milestone // snapshots are saved according to the interval provided to the milestone db diff --git a/lib/types.js b/lib/types.js index c74948058..f966ab9fe 100644 --- a/lib/types.js +++ b/lib/types.js @@ -1,7 +1,7 @@ exports.defaultType = require('ot-json0').type; -exports.map = {}; +exports.map = Object.create(null); exports.register = function(type) { if (type.name) exports.map[type.name] = type; diff --git a/lib/util.js b/lib/util.js index b49e67cb8..f991bbe41 100644 --- a/lib/util.js +++ b/lib/util.js @@ -7,6 +7,9 @@ exports.hasKeys = function(object) { return false; }; +var hasOwn; +exports.hasOwn = hasOwn = Object.hasOwn || Object.prototype.hasOwnProperty.call; + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill exports.isInteger = Number.isInteger || function(value) { return typeof value === 'number' && @@ -29,7 +32,7 @@ exports.dig = function() { var obj = arguments[0]; for (var i = 1; i < arguments.length; i++) { var key = arguments[i]; - obj = obj[key] || (i === arguments.length - 1 ? undefined : {}); + obj = hasOwn(obj, key) ? obj[key] : (i === arguments.length - 1 ? undefined : Object.create(null)); } return obj; }; @@ -39,8 +42,8 @@ exports.digOrCreate = function() { var createCallback = arguments[arguments.length - 1]; for (var i = 1; i < arguments.length - 1; i++) { var key = arguments[i]; - obj = obj[key] || - (obj[key] = i === arguments.length - 2 ? createCallback() : {}); + obj = hasOwn(obj, key) ? obj[key] : + (obj[key] = i === arguments.length - 2 ? createCallback() : Object.create(null)); } return obj; }; @@ -50,7 +53,7 @@ exports.digAndRemove = function() { var objects = [obj]; for (var i = 1; i < arguments.length - 1; i++) { var key = arguments[i]; - if (!obj.hasOwnProperty(key)) break; + if (!hasOwn(obj, key)) break; obj = obj[key]; objects.push(obj); }; @@ -101,3 +104,12 @@ exports.clone = function(obj) { return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj)); }; +var objectProtoPropNames = Object.create(null); +Object.getOwnPropertyNames(Object.prototype).forEach(function(prop) { + if (prop !== '__proto__') { + objectProtoPropNames[prop] = true; + } +}); +exports.isDangerousProperty = function(propName) { + return propName === '__proto__' || objectProtoPropNames[propName]; +}; diff --git a/test/client/doc.js b/test/client/doc.js index e5424d1ac..523561ec4 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -617,6 +617,106 @@ describe('Doc', function() { }); }); + describe('errors on ops that could cause prototype corruption', function() { + function expectReceiveError( + connection, + collectionName, + docId, + expectedError, + done + ) { + connection.on('receive', function(request) { + var message = request.data; + if (message.c === collectionName && message.d === docId) { + if ('error' in message) { + request.data = null; // Stop further processing of the message + if (message.error.message === expectedError) { + return done(); + } else { + return done('Unexpected ShareDB error: ' + message.error.message); + } + } else { + return done('Expected error on ' + collectionName + '.' + docId + ' but got no error'); + } + } + }); + } + + ['__proto__', 'constructor'].forEach(function(badProp) { + it('Rejects ops with collection ' + badProp, function(done) { + var collectionName = badProp; + var docId = 'test-doc'; + expectReceiveError(this.connection, collectionName, docId, 'Invalid collection', done); + this.connection.send({ + a: 'op', + c: collectionName, + d: docId, + v: 0, + seq: this.connection.seq++, + x: {}, + create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Test doc'}} + }); + }); + + it('Rejects ops with doc id ' + badProp, function(done) { + var collectionName = 'test-collection'; + var docId = badProp; + expectReceiveError(this.connection, collectionName, docId, 'Invalid id', done); + this.connection.send({ + a: 'op', + c: collectionName, + d: docId, + v: 0, + seq: this.connection.seq++, + x: {}, + create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Some doc'}} + }); + }); + + it('Rejects ops with ' + badProp + ' as first path segment', function(done) { + var connection = this.connection; + var collectionName = 'test-collection'; + var docId = 'test-doc'; + connection.get(collectionName, docId).create({id: docId}, function(err) { + if (err) { + return done(err); + } + expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done); + connection.send({ + a: 'op', + c: collectionName, + d: docId, + v: 1, + seq: connection.seq++, + x: {}, + op: [{p: [badProp, 'toString'], oi: 'oops'}] + }); + }); + }); + + it('Rejects ops with ' + badProp + ' as later path segment', function(done) { + var connection = this.connection; + var collectionName = 'test-collection'; + var docId = 'test-doc'; + connection.get(collectionName, docId).create({id: docId}, function(err) { + if (err) { + return done(err); + } + expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done); + connection.send({ + a: 'op', + c: collectionName, + d: docId, + v: 1, + seq: connection.seq++, + x: {}, + op: [{p: ['foo', badProp], oi: 'oops'}] + }); + }); + }); + }); + }); + describe('toSnapshot', function() { var doc; beforeEach(function(done) { diff --git a/test/client/presence/presence.js b/test/client/presence/presence.js index 40d4f4847..42d6b4780 100644 --- a/test/client/presence/presence.js +++ b/test/client/presence/presence.js @@ -466,6 +466,16 @@ describe('Presence', function() { }).to.throw(); }); + ['__proto__', 'constructor'].forEach(function(badProp) { + it('Rejects presence with channel ' + badProp, function(done) { + var presence = connection1.getPresence(badProp); + presence.subscribe(function(err) { + expect(err).to.be.an('error').to.haveOwnProperty('message', 'Invalid presence channel'); + done(); + }); + }); + }); + it('assigns an ID if one is not provided', function() { var localPresence = presence1.create(); expect(localPresence.presenceId).to.be.ok;