Dealing with JSON pollution

Let create a folder called json-validation, initialize it as a package, and create an index.js file:

$ mkdir json-validation
$ cd json-validation
$ npm init -y
$ touch index.js

The index.js should look like so:

const http = require('http')
const {STATUS_CODES} = http

const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.statusCode = 404
res.end(STATUS_CODES[res.statusCode])
return
}
if (req.url === '/register') {
register(req, res)
return
}
res.statusCode = 404
res.end(STATUS_CODES[res.statusCode])
})

function register (req, res) {
var data = ''
req.on('data', (chunk) => data += chunk)
req.on('end', () => {
try {
data = JSON.parse(data)
} catch (e) {
res.end('{"ok": false}')
return
}
// privileges can be multiple types, boolean, array, object, string,
// but the presence of the key means the user is an admin
if (data.hasOwnProperty('privileges')) {
createAdminUser(data)
res.end('{"ok": true, "admin": true}')
} else {
createUser(data)
res.end('{"ok": true, "admin": false}')
}
})
}

function createAdminUser (user) {
const key = user.id + user.name
// ...
}

function createUser (user) {
// ...
}

server.listen(3000)

Our server has a /register endpoint, which accepts POST requests to (hypothetically) add users to a system.

There are two ways we can cause the server to crash.

Let's try the following curl request:

$ curl -H "Content-Type: application/json" -X POST 
-d '{"hasOwnProperty": 0}' http://127.0.0.1:3000/register

This will cause our server to crash with "TypeError: data.hasOwnProperty is not a function".

If an object has a privileges property, the server infers that it's an admin user. Normal users don't have a privileges property. It uses the (often recommended in alternative scenarios) hasOwnProperty method to check for the property. This is because the (pretend) system requirements property allow for the privileges property to be false, which is an admin user with minimum permissions.

By sending a JSON payload with that key, we over-shadow the Object.prototype.hasOwnProperty method, setting it to 0, which is a number, not a function.

If we're checking for the existence of a value in an object that we know to be parsed JSON we can check if the property is undefined. Since undefined isn't a valid JSON value, this means we know for sure that the key doesn't exist.

So we could update the if statement if (data.hasOwnProperty('privileges')) to if (data.privileges !== undefined). However, this is more of a band-aid than a solution, what if our object is passed to another function, perhaps one in a module that we didn't even write, and the hasOwnProperty method is used there? Secondly, it's a specific work around, there are other more subtle ways to pollute a JSON payload.

Let's start our server again and run the following request:

$ curl -H "Content-Type: application/json" -X POST 
-d '{"privileges": false, "id": {"toString":0}, "name": "foo"}'
http://127.0.0.1:3000/register

This will cause our server to crash with the error TypeError: Cannot convert object to primitive value.

The createAdminUser function creates a key variable, by concatenating the id field with the name field from the JSON payload. Since the name field is a string, this causes id to be coerced (if necessary) to a string. Internally JavaScript achieves this by calling the toString method on the value (excepting null and undefined every primitive and object has the toString method on its prototype). Since we set the id field to an object, with a toString field set to 0 this overrides the prototypal toString function replacing it with the number 0.

This toString (and also valueOf) case is harder to protect against. To be safe, we need to check the type of every value in the JSON, to ensure that it's not an unexpected type. Rather than doing this manually we can use a schema validation library.

Generally, if JSON is being passed between backend services, we don't need to concern our selves too much with JSON pollution. However, if a service is public facing, we are vulnerable.

In the main, it's best practice to use schema validation for any public facing servers that accept JSON, doing so avoids these sorts of issues (and potentially other issues when the data passes to other environments such as databases).

Let's install ajv, a performance schema validator and copy the index.js file to index-fixed.js:

$ npm install --save ajv
$ cp index.js index-fixed.js

We'll make the top of index-fixed.js look as follows:

const http = require('http')
const Ajv = require('ajv')
const ajv = new Ajv
const schema = {
title: 'UserReg',
properties: {
id: {type: 'integer'},
name: {type: 'string'},
privileges: {
anyOf: [
{type: 'string'},
{type: 'boolean'},
{type: 'array', items: {type: 'string'}},
{type: 'object'}
]
}
},
additionalProperties: false,
required: ['id', 'name']
}
const validate = ajv.compile(schema)
const {STATUS_CODES} = http
JSONSchema
The ajv module uses the JSONSchema format for declaring object schemas. Find out more at http://json-schema.org.

The register function, we'll alter like so:

function register (req, res) {
var data = ''
req.on('data', (chunk) => data += chunk)
req.on('end', () => {
try {
data = JSON.parse(data)
} catch (e) {
res.end('{"ok": false}')
return
}
const valid = validate(data, schema)
if (!valid) {
console.error(validate.errors)
res.end('{"ok": false}')
return
}

if (data.hasOwnProperty('privileges')) {
createAdminUser(data)
res.end('{"ok": true, "admin": true}')
} else {
createUser(data)
res.end('{"ok": true, "admin": false}')
}
})
}

Now if we re-run the toString attack:

$ curl -H "Content-Type: application/json" -X POST -d '{"privileges": false, "id": {"toString": 0}, "name": "foo"}' http://127.0.0.1:3000/register 

Our server stays alive, but it logs a validation error:

[ { keyword: 'type',
dataPath: '[object Object].id',
schemaPath: '#/properties/id/type',
params: { type: 'integer' },
message: 'should be integer' } ]

Because we set additionalProperties to false on the schema, the hasOwnProperty attack also fails (request made with additional required fields):

$ curl -H "Content-Type: application/json" -X POST -d '{"hasOwnProperty": 0, "id": 10, "name": "foo"}' http://127.0.0.1:3000/register 

Our server stays alive, while an error message is logged:

[ { keyword: 'additionalProperties',
dataPath: '[object Object]',
schemaPath: '#/additionalProperties',
params: { additionalProperty: 'hasOwnProperty' },
message: 'should NOT have additional properties' } ]
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset