fix(push): fix push payload validation and disallow additional props (#57) r=vladikoff

This commit is contained in:
Edouard Oger 2017-03-08 12:37:50 -05:00 коммит произвёл Vlad Filippov
Родитель cd78f13133
Коммит 32750a290a
3 изменённых файлов: 82 добавлений и 50 удалений

Просмотреть файл

@ -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({