fix(push): fix push payload validation and disallow additional props (#57) r=vladikoff
This commit is contained in:
Родитель
cd78f13133
Коммит
32750a290a
|
@ -3,12 +3,19 @@
|
||||||
"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",
|
"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",
|
||||||
"$schema":"http://json-schema.org/draft-04/schema#",
|
"$schema":"http://json-schema.org/draft-04/schema#",
|
||||||
"type":"object",
|
"type":"object",
|
||||||
"allOf":[
|
"anyOf":[
|
||||||
{
|
{ "$ref":"#/definitions/deviceConnected" },
|
||||||
"type":"object",
|
{ "$ref":"#/definitions/deviceDisconnected" },
|
||||||
|
{ "$ref":"#/definitions/collectionsChanged" },
|
||||||
|
{ "$ref":"#/definitions/passwordChanged" },
|
||||||
|
{ "$ref":"#/definitions/passwordReset" }
|
||||||
|
],
|
||||||
|
"definitions":{
|
||||||
|
"deviceConnected":{
|
||||||
"required":[
|
"required":[
|
||||||
"version",
|
"version",
|
||||||
"command"
|
"command",
|
||||||
|
"data"
|
||||||
],
|
],
|
||||||
"properties":{
|
"properties":{
|
||||||
"version":{
|
"version":{
|
||||||
|
@ -17,29 +24,7 @@
|
||||||
},
|
},
|
||||||
"command":{
|
"command":{
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"description":"Helps the receiving device discriminate payloads"
|
"description":"Helps the receiving device discriminate payloads",
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type":"object",
|
|
||||||
"anyOf":[
|
|
||||||
{ "$ref":"#/definitions/deviceConnected" },
|
|
||||||
{ "$ref":"#/definitions/deviceDisconnected" },
|
|
||||||
{ "$ref":"#/definitions/collectionsChanged" },
|
|
||||||
{ "$ref":"#/definitions/passwordChanged" },
|
|
||||||
{ "$ref":"#/definitions/passwordReset" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"definitions":{
|
|
||||||
"deviceConnected":{
|
|
||||||
"type":"object",
|
|
||||||
"required":[
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties":{
|
|
||||||
"command":{
|
|
||||||
"enum":[
|
"enum":[
|
||||||
"fxaccounts:device_connected"
|
"fxaccounts:device_connected"
|
||||||
]
|
]
|
||||||
|
@ -59,12 +44,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deviceDisconnected":{
|
"deviceDisconnected":{
|
||||||
"type":"object",
|
|
||||||
"required":[
|
"required":[
|
||||||
|
"version",
|
||||||
|
"command",
|
||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"properties":{
|
"properties":{
|
||||||
|
"version":{
|
||||||
|
"type":"integer"
|
||||||
|
},
|
||||||
"command":{
|
"command":{
|
||||||
|
"type":"string",
|
||||||
"enum":[
|
"enum":[
|
||||||
"fxaccounts:device_disconnected"
|
"fxaccounts:device_disconnected"
|
||||||
]
|
]
|
||||||
|
@ -84,12 +74,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"collectionsChanged":{
|
"collectionsChanged":{
|
||||||
"type":"object",
|
|
||||||
"required":[
|
"required":[
|
||||||
|
"version",
|
||||||
|
"command",
|
||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"properties":{
|
"properties":{
|
||||||
|
"version":{
|
||||||
|
"type":"integer"
|
||||||
|
},
|
||||||
"command":{
|
"command":{
|
||||||
|
"type":"string",
|
||||||
"enum":[
|
"enum":[
|
||||||
"sync:collection_changed"
|
"sync:collection_changed"
|
||||||
]
|
]
|
||||||
|
@ -102,11 +97,11 @@
|
||||||
"properties":{
|
"properties":{
|
||||||
"collections":{
|
"collections":{
|
||||||
"type":"array",
|
"type":"array",
|
||||||
"minItems": 1,
|
"minItems":1,
|
||||||
"uniqueItems": true,
|
"uniqueItems":true,
|
||||||
"description":"A list of collections that were changed",
|
"description":"A list of collections that were changed",
|
||||||
"items": {
|
"items":{
|
||||||
"enum": [ "addons", "bookmarks", "history", "forms", "prefs",
|
"enum":[ "addons", "bookmarks", "history", "forms", "prefs",
|
||||||
"tabs", "passwords", "clients" ]
|
"tabs", "passwords", "clients" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,9 +110,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"passwordChanged":{
|
"passwordChanged":{
|
||||||
"type":"object",
|
"required":[
|
||||||
|
"version",
|
||||||
|
"command"
|
||||||
|
],
|
||||||
"properties":{
|
"properties":{
|
||||||
|
"version":{
|
||||||
|
"type":"integer"
|
||||||
|
},
|
||||||
"command":{
|
"command":{
|
||||||
|
"type":"string",
|
||||||
"enum":[
|
"enum":[
|
||||||
"fxaccounts:password_changed"
|
"fxaccounts:password_changed"
|
||||||
]
|
]
|
||||||
|
@ -125,9 +127,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"passwordReset":{
|
"passwordReset":{
|
||||||
"type":"object",
|
"required":[
|
||||||
|
"version",
|
||||||
|
"command"
|
||||||
|
],
|
||||||
"properties":{
|
"properties":{
|
||||||
|
"version":{
|
||||||
|
"type":"integer"
|
||||||
|
},
|
||||||
"command":{
|
"command":{
|
||||||
|
"type":"string",
|
||||||
"enum":[
|
"enum":[
|
||||||
"fxaccounts:password_reset"
|
"fxaccounts:password_reset"
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,7 +21,8 @@ var MS_ONE_WEEK = MS_ONE_DAY * 7
|
||||||
var MS_ONE_MONTH = MS_ONE_DAY * 30
|
var MS_ONE_MONTH = MS_ONE_DAY * 30
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var ajv = require('ajv')()
|
var Ajv = require('ajv')
|
||||||
|
var ajv = new Ajv({ removeAdditional: 'all' })
|
||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var butil = require('../crypto/butil')
|
var butil = require('../crypto/butil')
|
||||||
var userAgent = require('../userAgent')
|
var userAgent = require('../userAgent')
|
||||||
|
@ -50,7 +51,7 @@ module.exports = function (
|
||||||
// Loads and compiles a json validator for the payloads received
|
// Loads and compiles a json validator for the payloads received
|
||||||
// in /account/devices/notify
|
// in /account/devices/notify
|
||||||
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
|
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
|
||||||
var schema = fs.readFileSync(schemaPath)
|
var schema = JSON.parse(fs.readFileSync(schemaPath))
|
||||||
var validatePushPayload = ajv.compile(schema)
|
var validatePushPayload = ajv.compile(schema)
|
||||||
var verificationReminder = require('../verification-reminders')(log, db)
|
var verificationReminder = require('../verification-reminders')(log, db)
|
||||||
var getGeoData = require('../geodb')(log)
|
var getGeoData = require('../geodb')(log)
|
||||||
|
|
|
@ -193,7 +193,6 @@ describe('/account/devices/notify', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var pushPayload = {
|
var pushPayload = {
|
||||||
isValid: true,
|
|
||||||
version: 1,
|
version: 1,
|
||||||
command: 'sync:collection_changed',
|
command: 'sync:collection_changed',
|
||||||
data: {
|
data: {
|
||||||
|
@ -201,22 +200,12 @@ describe('/account/devices/notify', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var mockPush = mocks.mockPush()
|
var mockPush = mocks.mockPush()
|
||||||
var validate = sinon.spy(function (payload) { return payload.isValid })
|
|
||||||
var mockAjv = function () {
|
|
||||||
return {
|
|
||||||
compile: function () {
|
|
||||||
return validate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sandbox = sinon.sandbox.create()
|
var sandbox = sinon.sandbox.create()
|
||||||
var mockCustoms = mocks.mockCustoms()
|
var mockCustoms = mocks.mockCustoms()
|
||||||
var accountRoutes = makeRoutes({
|
var accountRoutes = makeRoutes({
|
||||||
config: config,
|
config: config,
|
||||||
customs: mockCustoms,
|
customs: mockCustoms,
|
||||||
push: mockPush
|
push: mockPush
|
||||||
}, {
|
|
||||||
ajv: mockAjv
|
|
||||||
})
|
})
|
||||||
var route = getRoute(accountRoutes, '/account/devices/notify')
|
var route = getRoute(accountRoutes, '/account/devices/notify')
|
||||||
|
|
||||||
|
@ -224,14 +213,13 @@ describe('/account/devices/notify', function () {
|
||||||
mockRequest.payload = {
|
mockRequest.payload = {
|
||||||
to: ['bogusid1'],
|
to: ['bogusid1'],
|
||||||
payload: {
|
payload: {
|
||||||
isValid: false
|
bogus: 'payload'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return runTest(route, mockRequest, function () {
|
return runTest(route, mockRequest, function () {
|
||||||
assert(false, 'should have thrown')
|
assert(false, 'should have thrown')
|
||||||
})
|
})
|
||||||
.then(() => assert(false), function (err) {
|
.then(() => assert(false), function (err) {
|
||||||
assert.equal(validate.callCount, 1, 'ajv validator function was called')
|
|
||||||
assert.equal(mockPush.pushToDevices.callCount, 0, 'mockPush.pushToDevices was not called')
|
assert.equal(mockPush.pushToDevices.callCount, 0, 'mockPush.pushToDevices was not called')
|
||||||
assert.equal(err.errno, 107, 'Correct errno for invalid push payload')
|
assert.equal(err.errno, 107, 'Correct errno for invalid push payload')
|
||||||
})
|
})
|
||||||
|
@ -268,6 +256,34 @@ describe('/account/devices/notify', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('extra push payload properties are stripped', function () {
|
||||||
|
var extraPropsPayload = JSON.parse(JSON.stringify(pushPayload))
|
||||||
|
extraPropsPayload.extra = true
|
||||||
|
extraPropsPayload.data.extra = true
|
||||||
|
mockRequest.payload = {
|
||||||
|
to: 'all',
|
||||||
|
excluded: ['bogusid'],
|
||||||
|
TTL: 60,
|
||||||
|
payload: extraPropsPayload
|
||||||
|
}
|
||||||
|
// We don't wait on pushToAllDevices in the request handler, that's why
|
||||||
|
// we have to wait on it manually by spying.
|
||||||
|
var pushToAllDevicesPromise = P.defer()
|
||||||
|
mockPush.pushToAllDevices = sinon.spy(function () {
|
||||||
|
pushToAllDevicesPromise.resolve()
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
return runTest(route, mockRequest, function (response) {
|
||||||
|
return pushToAllDevicesPromise.promise.then(function () {
|
||||||
|
assert.deepEqual(mockPush.pushToAllDevices.args[0][2], {
|
||||||
|
data: Buffer.from(JSON.stringify(pushPayload)),
|
||||||
|
excludedDeviceIds: ['bogusid'],
|
||||||
|
TTL: 60
|
||||||
|
}, 'third argument payload properties has no extra properties')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('specific devices', function () {
|
it('specific devices', function () {
|
||||||
mockCustoms.checkAuthenticated.reset()
|
mockCustoms.checkAuthenticated.reset()
|
||||||
mockLog.activityEvent.reset()
|
mockLog.activityEvent.reset()
|
||||||
|
@ -351,6 +367,12 @@ describe('/account/devices/notify', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws error if customs blocked the request', function () {
|
it('throws error if customs blocked the request', function () {
|
||||||
|
mockRequest.payload = {
|
||||||
|
to: 'all',
|
||||||
|
excluded: ['bogusid'],
|
||||||
|
TTL: 60,
|
||||||
|
payload: pushPayload
|
||||||
|
}
|
||||||
config.deviceNotificationsEnabled = true
|
config.deviceNotificationsEnabled = true
|
||||||
|
|
||||||
mockCustoms = mocks.mockCustoms({
|
mockCustoms = mocks.mockCustoms({
|
||||||
|
|
Загрузка…
Ссылка в новой задаче