Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

Commit 32750a2

Browse files
eogervladikoff
authored andcommitted
fix(push): fix push payload validation and disallow additional props (#57) r=vladikoff
1 parent cd78f13 commit 32750a2

File tree

3 files changed

+82
-50
lines changed

3 files changed

+82
-50
lines changed

docs/pushpayloads.schema.json

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
"description":"This schema defines what is acceptable to send as a payload data with the Push API from the FxA servers to a device connected to FxA",
44
"$schema":"http://json-schema.org/draft-04/schema#",
55
"type":"object",
6-
"allOf":[
7-
{
8-
"type":"object",
6+
"anyOf":[
7+
{ "$ref":"#/definitions/deviceConnected" },
8+
{ "$ref":"#/definitions/deviceDisconnected" },
9+
{ "$ref":"#/definitions/collectionsChanged" },
10+
{ "$ref":"#/definitions/passwordChanged" },
11+
{ "$ref":"#/definitions/passwordReset" }
12+
],
13+
"definitions":{
14+
"deviceConnected":{
915
"required":[
1016
"version",
11-
"command"
17+
"command",
18+
"data"
1219
],
1320
"properties":{
1421
"version":{
@@ -17,29 +24,7 @@
1724
},
1825
"command":{
1926
"type":"string",
20-
"description":"Helps the receiving device discriminate payloads"
21-
}
22-
}
23-
},
24-
{
25-
"type":"object",
26-
"anyOf":[
27-
{ "$ref":"#/definitions/deviceConnected" },
28-
{ "$ref":"#/definitions/deviceDisconnected" },
29-
{ "$ref":"#/definitions/collectionsChanged" },
30-
{ "$ref":"#/definitions/passwordChanged" },
31-
{ "$ref":"#/definitions/passwordReset" }
32-
]
33-
}
34-
],
35-
"definitions":{
36-
"deviceConnected":{
37-
"type":"object",
38-
"required":[
39-
"data"
40-
],
41-
"properties":{
42-
"command":{
27+
"description":"Helps the receiving device discriminate payloads",
4328
"enum":[
4429
"fxaccounts:device_connected"
4530
]
@@ -59,12 +44,17 @@
5944
}
6045
},
6146
"deviceDisconnected":{
62-
"type":"object",
6347
"required":[
48+
"version",
49+
"command",
6450
"data"
6551
],
6652
"properties":{
53+
"version":{
54+
"type":"integer"
55+
},
6756
"command":{
57+
"type":"string",
6858
"enum":[
6959
"fxaccounts:device_disconnected"
7060
]
@@ -84,12 +74,17 @@
8474
}
8575
},
8676
"collectionsChanged":{
87-
"type":"object",
8877
"required":[
78+
"version",
79+
"command",
8980
"data"
9081
],
9182
"properties":{
83+
"version":{
84+
"type":"integer"
85+
},
9286
"command":{
87+
"type":"string",
9388
"enum":[
9489
"sync:collection_changed"
9590
]
@@ -102,11 +97,11 @@
10297
"properties":{
10398
"collections":{
10499
"type":"array",
105-
"minItems": 1,
106-
"uniqueItems": true,
100+
"minItems":1,
101+
"uniqueItems":true,
107102
"description":"A list of collections that were changed",
108-
"items": {
109-
"enum": [ "addons", "bookmarks", "history", "forms", "prefs",
103+
"items":{
104+
"enum":[ "addons", "bookmarks", "history", "forms", "prefs",
110105
"tabs", "passwords", "clients" ]
111106
}
112107
}
@@ -115,19 +110,33 @@
115110
}
116111
},
117112
"passwordChanged":{
118-
"type":"object",
113+
"required":[
114+
"version",
115+
"command"
116+
],
119117
"properties":{
118+
"version":{
119+
"type":"integer"
120+
},
120121
"command":{
122+
"type":"string",
121123
"enum":[
122124
"fxaccounts:password_changed"
123125
]
124126
}
125127
}
126128
},
127129
"passwordReset":{
128-
"type":"object",
130+
"required":[
131+
"version",
132+
"command"
133+
],
129134
"properties":{
135+
"version":{
136+
"type":"integer"
137+
},
130138
"command":{
139+
"type":"string",
131140
"enum":[
132141
"fxaccounts:password_reset"
133142
]

lib/routes/account.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ var MS_ONE_WEEK = MS_ONE_DAY * 7
2121
var MS_ONE_MONTH = MS_ONE_DAY * 30
2222

2323
var path = require('path')
24-
var ajv = require('ajv')()
24+
var Ajv = require('ajv')
25+
var ajv = new Ajv({ removeAdditional: 'all' })
2526
var fs = require('fs')
2627
var butil = require('../crypto/butil')
2728
var userAgent = require('../userAgent')
@@ -50,7 +51,7 @@ module.exports = function (
5051
// Loads and compiles a json validator for the payloads received
5152
// in /account/devices/notify
5253
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
53-
var schema = fs.readFileSync(schemaPath)
54+
var schema = JSON.parse(fs.readFileSync(schemaPath))
5455
var validatePushPayload = ajv.compile(schema)
5556
var verificationReminder = require('../verification-reminders')(log, db)
5657
var getGeoData = require('../geodb')(log)

test/local/routes/account_devices.js

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -193,45 +193,33 @@ describe('/account/devices/notify', function () {
193193
}
194194
})
195195
var pushPayload = {
196-
isValid: true,
197196
version: 1,
198197
command: 'sync:collection_changed',
199198
data: {
200199
collections: ['clients']
201200
}
202201
}
203202
var mockPush = mocks.mockPush()
204-
var validate = sinon.spy(function (payload) { return payload.isValid })
205-
var mockAjv = function () {
206-
return {
207-
compile: function () {
208-
return validate
209-
}
210-
}
211-
}
212203
var sandbox = sinon.sandbox.create()
213204
var mockCustoms = mocks.mockCustoms()
214205
var accountRoutes = makeRoutes({
215206
config: config,
216207
customs: mockCustoms,
217208
push: mockPush
218-
}, {
219-
ajv: mockAjv
220209
})
221210
var route = getRoute(accountRoutes, '/account/devices/notify')
222211

223212
it('bad payload', function () {
224213
mockRequest.payload = {
225214
to: ['bogusid1'],
226215
payload: {
227-
isValid: false
216+
bogus: 'payload'
228217
}
229218
}
230219
return runTest(route, mockRequest, function () {
231220
assert(false, 'should have thrown')
232221
})
233222
.then(() => assert(false), function (err) {
234-
assert.equal(validate.callCount, 1, 'ajv validator function was called')
235223
assert.equal(mockPush.pushToDevices.callCount, 0, 'mockPush.pushToDevices was not called')
236224
assert.equal(err.errno, 107, 'Correct errno for invalid push payload')
237225
})
@@ -268,6 +256,34 @@ describe('/account/devices/notify', function () {
268256
})
269257
})
270258

259+
it('extra push payload properties are stripped', function () {
260+
var extraPropsPayload = JSON.parse(JSON.stringify(pushPayload))
261+
extraPropsPayload.extra = true
262+
extraPropsPayload.data.extra = true
263+
mockRequest.payload = {
264+
to: 'all',
265+
excluded: ['bogusid'],
266+
TTL: 60,
267+
payload: extraPropsPayload
268+
}
269+
// We don't wait on pushToAllDevices in the request handler, that's why
270+
// we have to wait on it manually by spying.
271+
var pushToAllDevicesPromise = P.defer()
272+
mockPush.pushToAllDevices = sinon.spy(function () {
273+
pushToAllDevicesPromise.resolve()
274+
return Promise.resolve()
275+
})
276+
return runTest(route, mockRequest, function (response) {
277+
return pushToAllDevicesPromise.promise.then(function () {
278+
assert.deepEqual(mockPush.pushToAllDevices.args[0][2], {
279+
data: Buffer.from(JSON.stringify(pushPayload)),
280+
excludedDeviceIds: ['bogusid'],
281+
TTL: 60
282+
}, 'third argument payload properties has no extra properties')
283+
})
284+
})
285+
})
286+
271287
it('specific devices', function () {
272288
mockCustoms.checkAuthenticated.reset()
273289
mockLog.activityEvent.reset()
@@ -351,6 +367,12 @@ describe('/account/devices/notify', function () {
351367
})
352368

353369
it('throws error if customs blocked the request', function () {
370+
mockRequest.payload = {
371+
to: 'all',
372+
excluded: ['bogusid'],
373+
TTL: 60,
374+
payload: pushPayload
375+
}
354376
config.deviceNotificationsEnabled = true
355377

356378
mockCustoms = mocks.mockCustoms({

0 commit comments

Comments
 (0)