Skip to content

Commit

Permalink
Merge pull request #635 from share/fix-proto-clobber
Browse files Browse the repository at this point in the history
Fix and guard against prototype pollution issues
  • Loading branch information
ericyhwang committed Dec 5, 2023
2 parents 3cb6a51 + 06cc387 commit cf33697
Show file tree
Hide file tree
Showing 20 changed files with 224 additions and 65 deletions.
57 changes: 39 additions & 18 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
};

/**
Expand All @@ -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];
Expand Down Expand Up @@ -373,25 +373,44 @@ 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 ||
request.a === ACTIONS.bulkSubscribe ||
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
Expand Down Expand Up @@ -483,7 +502,7 @@ function getQueryOptions(request) {
fetch = [id];
}
} else {
if (!fetchOps) fetchOps = {};
if (!fetchOps) fetchOps = Object.create(null);
fetchOps[id] = version;
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions lib/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
24 changes: 12 additions & 12 deletions lib/client/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,22 @@ 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)
this.nextQueryId = 1;
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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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() {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions lib/client/presence/doc-presence-emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions lib/client/presence/local-doc-presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -68,7 +68,7 @@ LocalDocPresence.prototype._sendPending = function() {
});

presence._pendingMessages = [];
presence._docDataVersionByPresenceVersion = {};
presence._docDataVersionByPresenceVersion = Object.create(null);
});
};

Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion lib/client/presence/local-presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function LocalPresence(presence, presenceId) {
this.value = null;

this._pendingMessages = [];
this._callbacksByPresenceVersion = {};
this._callbacksByPresenceVersion = Object.create(null);
}
emitter.mixin(LocalPresence);

Expand Down
8 changes: 4 additions & 4 deletions lib/client/presence/presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions lib/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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];
Expand Down Expand Up @@ -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 = [];
Expand Down
8 changes: 4 additions & 4 deletions lib/db/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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 {
Expand All @@ -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] = []);
};

Expand Down
Loading

0 comments on commit cf33697

Please sign in to comment.