Skip to content

Commit

Permalink
Merge pull request #3882 from dominykas/validate-cookies-alt
Browse files Browse the repository at this point in the history
Add validation for cookies (alt impl)
  • Loading branch information
hueniverse committed Jan 14, 2019
2 parents 413290f + 04758ac commit a4f9121
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 6 deletions.
36 changes: 32 additions & 4 deletions API.md
Expand Up @@ -3419,14 +3419,14 @@ The default response payload validation rules (for all non-error responses) expr
- `false` - no payload allowed.

- a [**joi**](https://github.com/hapijs/joi) validation object. The [`options`](#route.options.response.options)
along with the request context (`{ headers, params, query, payload, app, auth }`) are passed to
along with the request context (`{ headers, params, query, payload, state, app, auth }`) are passed to
the validation function.

- a validation function using the signature `async function(value, options)` where:

- `value` - the pending response payload.
- `options` - The [`options`](#route.options.response.options) along with the request context
(`{ headers, params, query, payload, app, auth }`).
(`{ headers, params, query, payload, state, app, auth }`).

- if the function returns a value and [`modify`](#route.options.response.modify) is `true`,
the value is used as the new response. If the original response is an error, the return
Expand Down Expand Up @@ -3606,7 +3606,7 @@ If a custom validation function (see `headers`, `params`, `query`, or `payload`
then `options` can an arbitrary object that will be passed to this function as the second
parameter.

The values of the other inputs (i.e. `headers`, `query`, `params`, `payload`, `app`, and `auth`)
The values of the other inputs (i.e. `headers`, `query`, `params`, `payload`, `state`, `app`, and `auth`)
are added to the `options` object under the validation `context` (accessible in rules as
`Joi.ref('$query.key')`).

Expand Down Expand Up @@ -3649,6 +3649,7 @@ Default value: `true` (no validation).
Validation rules for incoming request payload (request body), where:

- `true` - any payload allowed (no validation performed).

- `false` - no payload allowed.

- a [**joi**](https://github.com/hapijs/joi) validation object.
Expand Down Expand Up @@ -3679,6 +3680,7 @@ Validation rules for incoming request URI query component (the key-value part of
[`request.query`](#request.query) prior to validation. Where:

- `true` - any query parameter value allowed (no validation performed).

- `false` - no query parameter value allowed.

- a [**joi**](https://github.com/hapijs/joi) validation object.
Expand All @@ -3695,6 +3697,28 @@ Validation rules for incoming request URI query component (the key-value part of

Note that changes to the query parameters will not be reflected in [`request.url`](#request.url).

#### <a name="route.options.validate.state" /> `route.options.validate.state`

Default value: `true` (no validation).

Validation rules for incoming cookies. The `cookie` header is parsed and decoded into the
[`request.state`](#request.state) prior to validation. Where:

- `true` - any cookie value allowed (no validation performed).

- `false` - no cookies allowed.

- a [**joi**](https://github.com/hapijs/joi) validation object.

- a validation function using the signature `async function(value, options)` where:

- `value` - the [`request.state`](#request.state) object containing all parsed cookie values.
- `options` - [`options`](#route.options.validate.options).
- if a value is returned, the value is used as the new [`request.state`](#request.state) value
and the original value is stored in [`request.orig.state`](#request.orig). Otherwise, the
cookie values are left unchanged. If an error is thrown, the error is handled according to
[`failAction`](#route.options.validate.failAction).

## Request lifecycle

Each incoming request passes through the request lifecycle. The specific steps vary based on the
Expand Down Expand Up @@ -3768,6 +3792,10 @@ the same. The following is the complete list of steps a request can go through:
- based on the route [`validate.payload`](#route.options.validate.payload) option.
- error handling based on [`failAction`](#route.options.validate.failAction).

- _**State validation**_
- based on the route [`validate.state`](#route.options.validate.state) option.
- error handling based on [`failAction`](#route.options.validate.failAction).

- _**onPreHandler**_

- _**Pre-handler methods**_
Expand Down Expand Up @@ -4776,7 +4804,7 @@ The parsed content-type header. Only available when payload parsing enabled and

Access: read only.

An object containing the values of `params`, `query`, and `payload` before any validation
An object containing the values of `params`, `query`, `payload` and `state` before any validation
modifications made. Only set when input validation is performed.

#### <a name="request.params" /> `request.params`
Expand Down
1 change: 1 addition & 0 deletions lib/config.js
Expand Up @@ -215,6 +215,7 @@ internals.routeBase = Joi.object({
params: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, true),
query: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, false, true),
payload: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, false, true),
state: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, false, true),
failAction: internals.failAction,
errorFields: Joi.object(),
options: Joi.object().default()
Expand Down
9 changes: 7 additions & 2 deletions lib/route.js
Expand Up @@ -123,7 +123,7 @@ exports = module.exports = internals.Route = class {

Hoek.assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters:', display);

['headers', 'params', 'query', 'payload'].forEach((type) => {
['headers', 'params', 'query', 'payload', 'state'].forEach((type) => {

validation[type] = Validation.compile(validation[type]);
});
Expand Down Expand Up @@ -160,6 +160,7 @@ exports = module.exports = internals.Route = class {
}

Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', display);
Hoek.assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled:', display);
Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', display);

// Authentication configuration
Expand Down Expand Up @@ -286,6 +287,10 @@ exports = module.exports = internals.Route = class {
this._cycle.push(Validation.payload);
}

if (this.settings.validate.state) {
this._cycle.push(Validation.state);
}

if (this._extensions.onPreHandler.nodes) {
this._cycle.push(this._extensions.onPreHandler);
}
Expand Down Expand Up @@ -460,7 +465,7 @@ internals.config = function (chain) {

let config = chain[0];
for (const item of chain) {
config = Hoek.applyToDefaultsWithShallow(config, item, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query']);
config = Hoek.applyToDefaultsWithShallow(config, item, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state']);
}

return config;
Expand Down
8 changes: 8 additions & 0 deletions lib/validation.js
Expand Up @@ -53,6 +53,12 @@ exports.query = function (request) {
};


exports.state = function (request) {

return internals.input('state', request);
};


internals.input = async function (source, request) {

const localOptions = {
Expand All @@ -61,6 +67,7 @@ internals.input = async function (source, request) {
params: request.params,
query: request.query,
payload: request.payload,
state: request.state,
auth: request.auth,
app: {
route: request.route.settings.app,
Expand Down Expand Up @@ -151,6 +158,7 @@ exports.response = async function (request) {
params: request.params,
query: request.query,
payload: request.payload,
state: request.state,
auth: request.auth,
app: {
route: request.route.settings.app,
Expand Down
94 changes: 94 additions & 0 deletions test/validation.js
Expand Up @@ -747,6 +747,100 @@ describe('validation', () => {
});
});

it('rejects invalid cookies', async () => {

const server = Hapi.server({
routes: {
validate: {
state: {
a: Joi.string().min(8)
},
failAction: (request, h, err) => err // Expose detailed error
}
}
});

server.route({
method: 'GET',
path: '/',
handler: () => 'ok'
});

const res = await server.inject({ method: 'GET', url: '/', headers: { 'cookie': 'a=abc' } });
expect(res.statusCode).to.equal(400);
expect(res.result.validation).to.equal({
source: 'state',
keys: ['a']
});
});

it('accepts valid cookies', async () => {

const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: (request) => request.state,
options: {
validate: {
state: {
a: Joi.string().min(8),
b: Joi.array().single().items(Joi.boolean()),
c: Joi.string().default('value')
},
failAction: (request, h, err) => err // Expose detailed error
}
}
});

const res = await server.inject({ method: 'GET', url: '/', headers: { 'cookie': 'a=abcdefghi; b=true' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({
a: 'abcdefghi',
b: [true],
c: 'value'
});
});

it('accepts all cookies', async () => {

const server = Hapi.server();

server.route({
method: 'GET',
path: '/',
handler: (request) => request.state,
options: {
validate: {
state: true
}
}
});

const res = await server.inject({ method: 'GET', url: '/', headers: { 'cookie': 'a=abc' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({ a: 'abc' });
});

it('rejects all cookies', async () => {

const server = Hapi.server();

server.route({
method: 'GET',
path: '/',
handler: (request) => request.state,
options: {
validate: {
state: false
}
}
});

const res = await server.inject({ method: 'GET', url: '/', headers: { 'cookie': 'a=abc' } });
expect(res.statusCode).to.equal(400);
});

it('validates valid header', async () => {

const server = Hapi.server();
Expand Down

0 comments on commit a4f9121

Please sign in to comment.