Skip to content

Commit b90b225

Browse files
committed
fix presence keys colliding with object prototype chain
1 parent a612100 commit b90b225

2 files changed

Lines changed: 56 additions & 4 deletions

File tree

assets/js/phoenix/presence.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default class Presence {
88

99
constructor(channel, opts = {}){
1010
let events = opts.events || {state: "presence_state", diff: "presence_diff"}
11-
this.state = {}
11+
this.state = Object.create(null)
1212
this.pendingDiffs = []
1313
this.channel = channel
1414
this.joinRef = null
@@ -66,9 +66,10 @@ export default class Presence {
6666
* @returns {Presence}
6767
*/
6868
static syncState(currentState, newState, onJoin, onLeave){
69-
let state = this.clone(currentState)
70-
let joins = {}
71-
let leaves = {}
69+
let state = this.toNullProtoObj(this.clone(currentState))
70+
newState = this.toNullProtoObj(newState)
71+
let joins = Object.create(null)
72+
let leaves = Object.create(null)
7273

7374
this.map(state, (key, presence) => {
7475
if(!newState[key]){
@@ -107,6 +108,7 @@ export default class Presence {
107108
* @returns {Presence}
108109
*/
109110
static syncDiff(state, diff, onJoin, onLeave){
111+
state = this.toNullProtoObj(state)
110112
let {joins, leaves} = this.clone(diff)
111113
if(!onJoin){ onJoin = function (){ } }
112114
if(!onLeave){ onLeave = function (){ } }
@@ -158,5 +160,20 @@ export default class Presence {
158160
return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))
159161
}
160162

163+
// Presence keys are chosen on the server and may collide with
164+
// Object.prototype properties ("__proto__", "constructor", ...), so any
165+
// object indexed by presence key must not have a prototype chain
166+
//
167+
// TODO: replace the null-prototype objects with Maps in Phoenix 2.0
168+
// (breaking change for the lower-level static API)
169+
static toNullProtoObj(obj){
170+
if(Object.getPrototypeOf(obj) === null){ return obj }
171+
let cleaned = Object.create(null)
172+
Object.getOwnPropertyNames(obj).forEach(key => {
173+
cleaned[key] = obj[key]
174+
})
175+
return cleaned
176+
}
177+
161178
static clone(obj){ return JSON.parse(JSON.stringify(obj)) }
162179
}

assets/test/presence_test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,41 @@ describe("syncDiff", () => {
132132
})
133133
})
134134

135+
describe("keys colliding with Object.prototype properties", () => {
136+
// built via JSON.parse because a "__proto__" key in an object literal would
137+
// set the prototype instead of creating an own property
138+
const protoState = JSON.parse(`{
139+
"__proto__": {"metas": [{"id": 1, "phx_ref": "1"}]},
140+
"constructor": {"metas": [{"id": 2, "phx_ref": "2"}]},
141+
"hasOwnProperty": {"metas": [{"id": 3, "phx_ref": "3"}]}
142+
}`)
143+
144+
it("syncState joins and leaves them without touching the prototype", () => {
145+
let joined = []
146+
let state = Presence.syncState({}, protoState, key => joined.push(key))
147+
148+
expect(Object.getPrototypeOf(state)).toBe(null)
149+
expect(joined).toEqual(["__proto__", "constructor", "hasOwnProperty"])
150+
expect(Presence.list(state, (key, {metas}) => metas[0].id)).toEqual([1, 2, 3])
151+
152+
let left = []
153+
state = Presence.syncState(state, {}, null, key => left.push(key))
154+
expect(left).toEqual(["__proto__", "constructor", "hasOwnProperty"])
155+
expect(Presence.list(state)).toEqual([])
156+
expect(Object.prototype.metas).toBe(undefined)
157+
})
158+
159+
it("syncDiff joins and leaves them without touching the prototype", () => {
160+
let diff = JSON.parse(`{"joins": {"__proto__": {"metas": [{"id": 1, "phx_ref": "1"}]}}, "leaves": {}}`)
161+
let state = Presence.syncDiff({}, diff)
162+
expect(Presence.list(state, (key, {metas}) => [key, metas[0].id])).toEqual([["__proto__", 1]])
163+
164+
state = Presence.syncDiff(state, JSON.parse(`{"joins": {}, "leaves": {"__proto__": {"metas": [{"id": 1, "phx_ref": "1"}]}}}`))
165+
expect(Presence.list(state)).toEqual([])
166+
expect(Object.prototype.metas).toBe(undefined)
167+
})
168+
})
169+
135170
describe("list", () => {
136171
it("lists full presence by default", () => {
137172
let state = fixtures.state()

0 commit comments

Comments
 (0)