Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and guard against prototype pollution issues #635

Merged
merged 1 commit into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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