├── test ├── templates │ └── index.html ├── fixtures │ ├── views.js │ └── cert.json ├── esm.js └── index.js ├── .gitignore ├── example ├── templates │ ├── message.html │ └── index.html ├── package.json ├── server.js └── restful.js ├── .github └── workflows │ └── ci-plugin.yml ├── package.json ├── README.md ├── LICENSE.md ├── API.md └── lib └── index.js /test/templates/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | # @hapi/crumb
4 |
5 | #### CSRF crumb generation and validation for hapi.
6 |
7 | **crumb** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
8 |
9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
10 |
11 | ## Useful resources
12 |
13 | - [Documentation and API](https://hapi.dev/family/crumb/)
14 | - [Version status](https://hapi.dev/resources/status/#crumb) (builds, dependencies, node versions, licenses, eol)
15 | - [Changelog](https://hapi.dev/family/crumb/changelog/)
16 | - [Project policies](https://hapi.dev/policies/)
17 | - [Free and commercial support options](https://hapi.dev/support/)
18 |
--------------------------------------------------------------------------------
/example/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Crumb = require('..');
4 | const Hapi = require('@hapi/hapi');
5 | const Vision = require('@hapi/vision');
6 |
7 |
8 | const server = Hapi.server({
9 | host: '127.0.0.1',
10 | port: 8000
11 | });
12 |
13 | const plugins = [
14 | Vision,
15 | {
16 | plugin: Crumb,
17 | options: {
18 | cookieOptions: {
19 | isSecure: false
20 | }
21 | }
22 | }
23 | ];
24 |
25 | (async () => {
26 |
27 | await server.register(plugins);
28 |
29 | server.views({
30 | relativeTo: __dirname,
31 | path: 'templates',
32 | engines: {
33 | html: require('handlebars')
34 | }
35 | });
36 |
37 | server.route({
38 | method: 'get',
39 | path: '/',
40 | handler: function (request, h) {
41 |
42 | return h.view('index', { title: 'test', message: 'hi' });
43 | }
44 | });
45 |
46 | server.route({
47 | method: 'post',
48 | path: '/',
49 | handler: function (request, h) {
50 |
51 | return h.view('message', { title: 'test', message: request.payload.message });
52 | }
53 | });
54 |
55 | await server.start();
56 |
57 | console.log('Example server running at:', server.info.uri);
58 | })();
59 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2022, Project contributors
2 | Copyright (c) 2013-2020, Sideway Inc
3 | Copyright (c) 2013-2014, Walmart.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/example/restful.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Crumb = require('..');
4 | const Hapi = require('@hapi/hapi');
5 | const Vision = require('@hapi/vision');
6 |
7 |
8 | const server = Hapi.server({
9 | host: '127.0.0.1',
10 | port: 8000
11 | });
12 |
13 | const plugins = [
14 | Vision,
15 | {
16 | plugin: Crumb,
17 | options: {
18 | restful: true,
19 | cookieOptions: {
20 | isSecure: false
21 | }
22 | }
23 | }
24 | ];
25 |
26 | // Add Crumb plugin
27 |
28 | (async () => {
29 |
30 | await server.register(plugins);
31 |
32 | server.route([
33 |
34 | // a "crumb" cookie should be set with any request
35 | // for cross-origin requests, set CORS "credentials" to true
36 | // a route returning the crumb can be created like this
37 |
38 | {
39 | method: 'GET',
40 | path: '/generate',
41 | handler: function (request, h) {
42 |
43 | return {
44 | crumb: server.plugins.crumb.generate(request, h)
45 | };
46 | }
47 | },
48 |
49 | // request header "X-CSRF-Token" with crumb value must be set in request for this route
50 |
51 | {
52 | method: 'PUT',
53 | path: '/crumbed',
54 | handler: function (request, h) {
55 |
56 | return 'Crumb route';
57 | }
58 | }
59 | ]);
60 |
61 | await server.start();
62 |
63 | console.log('Example restful server running at:', server.info.uri);
64 | })();
65 |
--------------------------------------------------------------------------------
/test/fixtures/cert.json:
--------------------------------------------------------------------------------
1 | {
2 | "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n",
3 | "cert": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n"
4 | }
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 |
2 | ### About
3 |
4 | Crumb is used to diminish CSRF attacks using a random unique token that is validated on the server side.
5 |
6 | Crumb may be used whenever you want to prevent malicious code to execute system commands, that are performed by HTTP requests. For example, if users are able to publish code on your website, malicious code added by a user could force every other user who opens the page, to load and execute code from a third party website e.g. via an HTML image tag. With Crumb implemented into your hapi.js application, you are able to verify requests with unique tokens and prevent the execution of malicious requests.
7 |
8 | ### CORS
9 |
10 | Crumb has been refactored to securely work with CORS, as [OWASP](https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Cross_Origin_Resource_Sharing) recommends using CSRF protection with CORS.
11 |
12 | **It is highly discouraged to have a production servers `cors.origin` setting set to "[\*]" or "true" with Crumb as it will leak the crumb token to potentially malicious sites**
13 |
14 |
15 | ## Usage
16 |
17 | ```js
18 | const Hapi = require('@hapi/hapi');
19 | const Crumb = require('@hapi/crumb');
20 |
21 | const server = new Hapi.Server({ port: 8000 });
22 |
23 | (async () => {
24 | await server.register({
25 | plugin: Crumb,
26 |
27 | // plugin options
28 | options: {}
29 | });
30 |
31 | server.route({
32 | path: '/login',
33 | method: 'GET',
34 | options: {
35 | plugins: {
36 | // route specific options
37 | crumb: {}
38 | },
39 | handler(request, h) {
40 | // this requires to have a view engine configured
41 | return h.view('some-view');
42 | }
43 | }
44 | });
45 | })();
46 | ```
47 |
48 | For a complete example see [the examples folder](https://github.com/hapijs/crumb/tree/master/example).
49 |
50 | ## Options
51 |
52 | The following options are available when registering the plugin.
53 |
54 | ### Registration options
55 |
56 | * `key` - the name of the cookie to store the csrf crumb into. Defaults to `crumb`.
57 | * `size` - the length of the crumb to generate. Defaults to `43`, which is 256 bits, see [cryptile](https://hapi.dev/module/cryptiles) for more information.
58 | * `autoGenerate` - whether to automatically generate a new crumb for requests. Defaults to `true`.
59 | * `addToViewContext` - whether to automatically add the crumb to view contexts as the given key. Defaults to `true`.
60 | * `cookieOptions` - storage options for the cookie containing the crumb, see the [server.state()](https://hapi.dev/api#server.state()) documentation of hapi for more information. Default to `cookieOptions.path=/` . **Note that the cookie is not set as secure by default. It should be set as 'secure:true' for production use.**
61 | * `headerName` - specify the name of the custom CSRF header. Defaults to `X-CSRF-Token`.
62 | * `restful` - RESTful mode that validates crumb tokens from *"X-CSRF-Token"* request header for **POST**, **PUT**, **PATCH** and **DELETE** server routes. Disables payload/query crumb validation. Defaults to `false`.
63 | * `skip` - a function with the signature of `function (request, h) {}`, which when provided, is called for every request. If the provided function returns true, validation and generation of crumb is skipped. Defaults to `false`.
64 | * `enforce` - defaults to true, using enforce with false will set the CSRF header cookie but won't execute the validation
65 | * `logUnauthorized` - whether to add to the request log with tag 'crumb' and data 'validation failed' (defaults to false)
66 |
67 | ### Routes configuration
68 |
69 | Additionally, some configuration can be passed on a per-route basis. Disable Crumb for a particular route by passing `false` instead of a configuration object.
70 |
71 | * `key` - the key used in the view contexts and payloads for the crumb. Defaults to `plugin.key`.
72 | * `source` - can be either `payload` or `query` specifying how the crumb will be sent in requests. Defaults to `payload`.
73 | * `restful` - an override for the server's 'restful' setting. Defaults to `plugin.restful`.
74 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Stream = require('stream');
4 |
5 | const Boom = require('@hapi/boom');
6 | const Cryptiles = require('@hapi/cryptiles');
7 | const Hoek = require('@hapi/hoek');
8 | const Validate = require('@hapi/validate');
9 |
10 |
11 | const internals = {
12 | restfulValidatedMethods: ['post', 'put', 'patch', 'delete']
13 | };
14 |
15 |
16 | internals.schema = Validate.object().keys({
17 | key: Validate.string().optional(),
18 | size: Validate.number().optional(),
19 | autoGenerate: Validate.boolean().optional(),
20 | addToViewContext: Validate.boolean().optional(),
21 | cookieOptions: Validate.object().keys(null),
22 | headerName: Validate.string().optional(),
23 | restful: Validate.boolean().optional(),
24 | skip: Validate.func().optional(),
25 | enforce: Validate.boolean().optional(),
26 | logUnauthorized: Validate.boolean().optional()
27 | });
28 |
29 |
30 | internals.defaults = {
31 | key: 'crumb',
32 | size: 43, // Equal to 256 bits
33 | autoGenerate: true, // If false, must call request.plugins.crumb.generate() manually before usage
34 | addToViewContext: true, // If response is a view, add crumb to context
35 | cookieOptions: { // Cookie options (i.e. hapi server.state)
36 | path: '/'
37 | },
38 | headerName: 'X-CSRF-Token', // Specify the name of the custom CSRF header
39 | restful: false, // Set to true for custom header crumb validation. Disables payload/query validation
40 | skip: false, // Set to a function which returns true when to skip crumb generation and validation,
41 | enforce: true, // Set to true for setting the CSRF cookie while not performing validation
42 | logUnauthorized: false // Set to true for crumb to write an event to the request log
43 | };
44 |
45 |
46 | exports.plugin = {
47 | pkg: require('../package.json'),
48 | requirements: {
49 | hapi: '>=20.0.0'
50 | },
51 | register: function (server, options) {
52 |
53 | Validate.assert(options, internals.schema);
54 |
55 | const settings = Hoek.applyToDefaults(internals.defaults, options);
56 |
57 | const routeDefaults = {
58 | key: settings.key,
59 | restful: settings.restful,
60 | source: 'payload'
61 | };
62 |
63 | server.state(settings.key, settings.cookieOptions);
64 |
65 | server.ext('onPostAuth', (request, h) => {
66 |
67 | const unauthorizedLogger = () => {
68 |
69 | if (settings.logUnauthorized) {
70 | request.log(['crumb', 'unauthorized'], 'validation failed');
71 | }
72 | };
73 |
74 | const getCrumbValue = () => {
75 |
76 | let crumbValue = request.plugins.crumb;
77 |
78 | if (Array.isArray(crumbValue)) {
79 | request.log(['crumb'], 'multiple cookies found');
80 | crumbValue = request.plugins.crumb[0];
81 | }
82 |
83 | return crumbValue;
84 | };
85 |
86 | // If skip function enabled, invoke and if returns true, do not attempt to do anything with crumb
87 |
88 | if (settings.skip &&
89 | settings.skip(request, h)) {
90 |
91 | return h.continue;
92 | }
93 |
94 | // Get crumb settings for this route if crumb is enabled on route
95 |
96 | if (request.route.settings.plugins._crumb === undefined) {
97 | if (request.route.settings.plugins.crumb ||
98 | !request.route.settings.plugins.hasOwnProperty('crumb')) {
99 |
100 | request.route.settings.plugins._crumb = Hoek.applyToDefaults(routeDefaults, request.route.settings.plugins.crumb ?? {});
101 | }
102 | else {
103 | request.route.settings.plugins._crumb = false;
104 | }
105 | }
106 |
107 | if (!request.route.settings.cors ||
108 | checkCORS(request)) {
109 |
110 | // Read crumb value from cookie if crumb enabled for this route
111 |
112 | if (request.route.settings.plugins._crumb) {
113 | request.plugins.crumb = request.state[settings.key];
114 | }
115 |
116 | // Generate crumb value if autoGenerate enabled or crumb specifically enabled on route
117 |
118 | if (settings.autoGenerate ||
119 | request.route.settings.plugins.crumb) {
120 |
121 | generate(request, h);
122 | }
123 | }
124 |
125 | // Skip validation on dry run
126 |
127 | if (!settings.enforce) {
128 | return h.continue;
129 | }
130 |
131 | // Validate crumb
132 |
133 | const restful = request.route.settings.plugins._crumb ? request.route.settings.plugins._crumb.restful : settings.restful;
134 | if (restful) {
135 | if (!internals.restfulValidatedMethods.includes(request.method) || !request.route.settings.plugins._crumb) {
136 | return h.continue;
137 | }
138 |
139 | const header = request.headers[settings.headerName.toLowerCase()];
140 |
141 | if (!header) {
142 | unauthorizedLogger();
143 | throw Boom.forbidden();
144 | }
145 |
146 | if (header !== getCrumbValue()) {
147 | unauthorizedLogger();
148 | throw Boom.forbidden();
149 | }
150 |
151 | return h.continue;
152 | }
153 |
154 | // Not restful
155 |
156 | if (!request.route.settings.plugins._crumb ||
157 | request.method !== 'post') {
158 |
159 | return h.continue;
160 | }
161 |
162 | const content = request[request.route.settings.plugins._crumb.source];
163 | if (!content ||
164 | content instanceof Stream) {
165 |
166 | unauthorizedLogger();
167 | throw Boom.forbidden();
168 | }
169 |
170 | if (content[request.route.settings.plugins._crumb.key] !== getCrumbValue()) {
171 | unauthorizedLogger();
172 | throw Boom.forbidden();
173 | }
174 |
175 | // Remove crumb
176 |
177 | delete request[request.route.settings.plugins._crumb.source][request.route.settings.plugins._crumb.key];
178 | return h.continue;
179 | });
180 |
181 | server.ext('onPreResponse', (request, h) => {
182 |
183 | // Add to view context
184 |
185 | const response = request.response;
186 |
187 | if (settings.addToViewContext &&
188 | request.plugins.crumb &&
189 | request.route.settings.plugins._crumb &&
190 | !response.isBoom &&
191 | response.variety === 'view') {
192 |
193 | response.source.context = response.source.context ?? {};
194 | response.source.context[request.route.settings.plugins._crumb.key] = request.plugins.crumb;
195 | }
196 |
197 | return h.continue;
198 | });
199 |
200 | const checkCORS = function (request) {
201 |
202 | if (request.headers.origin) {
203 | return request.info.cors.isOriginMatch;
204 | }
205 |
206 | return true;
207 | };
208 |
209 | const generate = function (request, h) {
210 |
211 | if (!request.plugins.crumb) {
212 | const crumb = Cryptiles.randomString(settings.size);
213 | h.state(settings.key, crumb, settings.cookieOptions);
214 | request.plugins.crumb = crumb;
215 | }
216 |
217 | return request.plugins.crumb;
218 | };
219 |
220 | server.expose({ generate });
221 | }
222 | };
223 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Stream = require('stream');
4 |
5 | const Code = require('@hapi/code');
6 | const Crumb = require('..');
7 | const Hapi = require('@hapi/hapi');
8 | const Lab = require('@hapi/lab');
9 | const Vision = require('@hapi/vision');
10 |
11 | const TLSCert = require('./fixtures/cert');
12 | const Views = require('./fixtures/views');
13 |
14 |
15 | const internals = {};
16 |
17 |
18 | const { describe, it } = exports.lab = Lab.script();
19 | const { expect } = Code;
20 |
21 | internals.viewOptions = {
22 | path: __dirname + '/templates',
23 | engines: {
24 | html: require('handlebars')
25 | }
26 | };
27 |
28 |
29 | describe('Crumb', () => {
30 |
31 | it('returns view with crumb', async () => {
32 |
33 | const server = Hapi.server();
34 |
35 | server.route([
36 | {
37 | method: 'GET',
38 | path: '/1',
39 | handler: (request, h) => {
40 |
41 | expect(request.plugins.crumb).to.exist();
42 | expect(request.server.plugins.crumb.generate).to.exist();
43 |
44 | return h.view('index', {
45 | title: 'test',
46 | message: 'hi'
47 | });
48 | }
49 | },
50 | {
51 | method: 'POST',
52 | path: '/2',
53 | handler: (request, h) => {
54 |
55 | expect(request.payload).to.equal({ key: 'value' });
56 | return 'valid';
57 | }
58 | },
59 | {
60 | method: 'POST',
61 | path: '/3',
62 | options: {
63 | payload: {
64 | output: 'stream'
65 | }
66 | },
67 | handler: (request, h) => 'never'
68 | },
69 | {
70 | method: 'GET',
71 | path: '/4',
72 | options: {
73 | plugins: {
74 | crumb: false
75 | }
76 | },
77 | handler: (request, h) => {
78 |
79 | return h.view('index', {
80 | title: 'test',
81 | message: 'hi'
82 | });
83 | }
84 | },
85 | {
86 | method: 'POST',
87 | path: '/5',
88 | options: {
89 | payload: {
90 | output: 'stream'
91 | }
92 | },
93 | handler: (request, h) => 'yo'
94 | },
95 | {
96 | method: 'GET',
97 | path: '/6',
98 | handler: (request, h) => h.view('index')
99 | },
100 | {
101 | method: 'GET',
102 | path: '/7',
103 | handler: (request, h) => h.redirect('/1')
104 | }
105 | ]);
106 |
107 | await server.register([
108 | Vision,
109 | {
110 | plugin: Crumb,
111 | options: {
112 | cookieOptions: {
113 | isSecure: true
114 | }
115 | }
116 | }
117 | ]);
118 |
119 | server.views(internals.viewOptions);
120 |
121 | // Works with get requests
122 | const res = await server.inject({
123 | method: 'GET',
124 | url: '/1'
125 | });
126 |
127 | expect(res.statusCode).to.equal(200);
128 |
129 | const header = res.headers['set-cookie'];
130 |
131 | expect(header.length).to.equal(1);
132 | expect(header[0]).to.contain('Secure');
133 |
134 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
135 |
136 | expect(res.result).to.equal(Views.viewWithCrumb(cookie[1]));
137 |
138 | // Works with crumb on POST body request
139 | const res2 = await server.inject({
140 | method: 'POST',
141 | url: '/2',
142 | payload: '{ "key": "value", "crumb": "' + cookie[1] + '" }',
143 | headers: {
144 | cookie: 'crumb=' + cookie[1]
145 | }
146 | });
147 |
148 | expect(res2.result).to.equal('valid');
149 |
150 | // Rejects on invalid crumb on POST body request
151 | const res3 = await server.inject({
152 | method: 'POST',
153 | url: '/2',
154 | payload: '{ "key": "value", "crumb": "x' + cookie[1] + '" }',
155 | headers: {
156 | cookie: 'crumb=' + cookie[1]
157 | }
158 | });
159 |
160 | expect(res3.statusCode).to.equal(403);
161 |
162 | // Rejects on missing crumb on POST stream body requests
163 | const res4 = await server.inject({
164 | method: 'POST',
165 | url: '/3',
166 | headers: {
167 | cookie: 'crumb=' + cookie[1]
168 | }
169 | });
170 |
171 | expect(res4.statusCode).to.equal(403);
172 |
173 | // Works with crumb generation disabled
174 | const res5 = await server.inject({
175 | method: 'GET',
176 | url: '/4'
177 | });
178 |
179 | expect(res5.result).to.equal(Views.viewWithoutCrumb());
180 |
181 | // Works on POST stream requests
182 |
183 |
184 |
185 | const TestStream = class extends Stream.Readable {
186 |
187 | constructor(options) {
188 |
189 | super(options);
190 | this._max = 2;
191 | this._index = 1;
192 | }
193 |
194 | _read() {
195 |
196 | const i = this._index++;
197 | if (i > this._max) {
198 | this.push(null);
199 | }
200 | else {
201 | const str = '' + i;
202 | const buf = Buffer.from(str, 'ascii');
203 | this.push(buf);
204 | }
205 | }
206 | };
207 |
208 | const res6 = await server.inject({
209 | method: 'POST',
210 | url: '/5',
211 | payload: new TestStream(),
212 | headers: {
213 | 'content-type': 'application/octet-stream',
214 | 'content-disposition': 'attachment; filename="test.txt"'
215 | },
216 | simulate: {
217 | end: true
218 | }
219 | });
220 |
221 | expect(res6.statusCode).to.equal(403);
222 |
223 | // Works with get requests with no context
224 | const res7 = await server.inject({
225 | method: 'GET',
226 | url: '/6'
227 | });
228 |
229 | const header2 = res7.headers['set-cookie'];
230 | expect(header2.length).to.equal(1);
231 | expect(header2[0]).to.contain('Secure');
232 |
233 | const cookie2 = header2[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
234 | expect(res7.result).to.equal(Views.viewWithCrumbAndNoContext(cookie2[1]));
235 |
236 | // Works with redirections
237 | const res8 = await server.inject({
238 | method: 'GET',
239 | url: '/7'
240 | });
241 |
242 | const cookie3 = res8.headers['set-cookie'].toString();
243 | expect(cookie3).to.contain('crumb');
244 |
245 | const headers = {};
246 | headers.origin = 'http://127.0.0.1';
247 |
248 | // Works with cross-origin enabled requests
249 | const res9 = await server.inject({
250 | method: 'GET',
251 | url: '/1',
252 | headers
253 | });
254 |
255 | const cookie4 = res9.headers['set-cookie'].toString();
256 | expect(cookie4).to.contain('crumb');
257 | });
258 |
259 | it('Does not add crumb to view context when "addToViewContext" option set to false', async () => {
260 |
261 | const server = Hapi.server();
262 |
263 | server.route({
264 | method: 'GET',
265 | path: '/1',
266 | handler: (request, h) => {
267 |
268 | expect(request.plugins.crumb).to.exist();
269 | expect(request.server.plugins.crumb.generate).to.exist();
270 |
271 | return h.view('index', {
272 | title: 'test',
273 | message: 'hi'
274 | });
275 | }
276 | });
277 |
278 | const plugins = [
279 | Vision,
280 | {
281 | plugin: Crumb,
282 | options: {
283 | cookieOptions: {
284 | isSecure: true
285 | },
286 | addToViewContext: false
287 | }
288 | }
289 | ];
290 |
291 | await server.register(plugins);
292 | server.views(internals.viewOptions);
293 |
294 | const res = await server.inject({
295 | method: 'GET',
296 | url: '/1'
297 | });
298 |
299 | expect(res.result).to.equal(Views.viewWithoutCrumb());
300 | });
301 |
302 | it('Works without specifying plugin options', async () => {
303 |
304 | const server = Hapi.server();
305 |
306 | server.route({
307 | method: 'GET',
308 | path: '/1',
309 | handler: (request, h) => {
310 |
311 | expect(request.plugins.crumb).to.exist();
312 | expect(request.server.plugins.crumb.generate).to.exist();
313 |
314 | return h.view('index', {
315 | title: 'test',
316 | message: 'hi'
317 | });
318 | }
319 | });
320 |
321 | await server.register([Vision, Crumb]);
322 |
323 | server.views(internals.viewOptions);
324 |
325 | const res = await server.inject({
326 | method: 'GET',
327 | url: '/1'
328 | });
329 |
330 | const header = res.headers['set-cookie'];
331 | expect(header.length).to.equal(1);
332 |
333 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
334 | expect(res.result).to.equal(Views.viewWithCrumb(cookie[1]));
335 | });
336 |
337 | it('Adds to the request log if plugin option logUnauthorized is set to true', async () => {
338 |
339 | const server = Hapi.server();
340 |
341 | let logFound;
342 | const preResponse = function (request, h) {
343 |
344 | const logs = request.logs;
345 | logFound = logs.find((log) => {
346 |
347 | return log.tags[0] === 'crumb' && log.data === 'validation failed';
348 | });
349 |
350 | return h.continue;
351 | };
352 |
353 | server.ext('onPreResponse', preResponse);
354 |
355 | server.route({
356 | method: 'POST',
357 | path: '/1',
358 | config: {
359 | log: {
360 | collect: true
361 | }
362 | },
363 | handler: (request, h) => 'test'
364 | });
365 |
366 | await server.register([
367 | {
368 | plugin: Crumb,
369 | options: {
370 | logUnauthorized: true
371 | }
372 | }
373 | ]);
374 |
375 | const headers = {};
376 | headers['X-API-Token'] = 'test';
377 |
378 | await server.inject({
379 | method: 'POST',
380 | url: '/1',
381 | payload: '{ "key": true }',
382 | headers
383 | });
384 | expect(logFound).to.exist();
385 | });
386 |
387 | it('Does not add to the request log if plugin option logUnauthorized is set to false', async () => {
388 |
389 | const server = Hapi.server();
390 |
391 | let logFound;
392 | const preResponse = function (request, h) {
393 |
394 | const logs = request.logs;
395 | logFound = logs.find((log) => {
396 |
397 | return log.tags[0] === 'crumb' && log.data === 'validation failed';
398 | });
399 |
400 | return h.continue;
401 | };
402 |
403 | server.ext('onPreResponse', preResponse);
404 |
405 | server.route({
406 | method: 'POST',
407 | path: '/1',
408 | config: {
409 | log: {
410 | collect: true
411 | }
412 | },
413 | handler: (request, h) => 'test'
414 | });
415 |
416 | await server.register([
417 | {
418 | plugin: Crumb,
419 | options: {
420 | logUnauthorized: false
421 | }
422 | }
423 | ]);
424 |
425 | const headers = {};
426 | headers['X-API-Token'] = 'test';
427 |
428 | await server.inject({
429 | method: 'POST',
430 | url: '/1',
431 | headers
432 | });
433 |
434 | expect(logFound).to.not.exist();
435 | });
436 |
437 | it('should fail to register with bad options', async () => {
438 |
439 | const server = Hapi.server();
440 |
441 | try {
442 | await server.register({
443 | plugin: Crumb,
444 | options: {
445 | foo: 'bar'
446 | }
447 | });
448 | }
449 | catch (err) {
450 | expect(err).to.exist();
451 | expect(err.name).to.equal('ValidationError');
452 | // TODO: Message validation fails because of formatting produced by assert :(
453 | // expect(err.message).to.equal('"foo" is not allowed');
454 | }
455 | });
456 |
457 | it('route uses crumb when route.options.plugins.crumb set to true and autoGenerate set to false', async () => {
458 |
459 | const server = Hapi.server();
460 |
461 | server.route([
462 | {
463 | method: 'GET',
464 | path: '/1',
465 | handler: (request, h) => {
466 |
467 | const crumb = request.plugins.crumb;
468 |
469 | expect(crumb).to.not.exist();
470 |
471 | return 'bonjour';
472 | }
473 | },
474 | {
475 | method: 'GET',
476 | path: '/2',
477 | options: {
478 | plugins: {
479 | crumb: true
480 | }
481 | },
482 | handler: (request, h) => 'hola'
483 | }
484 | ]);
485 |
486 | await server.register([
487 | Vision,
488 | {
489 | plugin: Crumb,
490 | options: {
491 | autoGenerate: false
492 | }
493 | }
494 | ]);
495 |
496 | server.views(internals.viewOptions);
497 |
498 | await server.inject({
499 | method: 'GET',
500 | url: '/1'
501 | });
502 |
503 | const res = await server.inject({ method: 'GET', url: '/2' });
504 |
505 | const header = res.headers['set-cookie'];
506 | expect(header.length).to.equal(1);
507 | });
508 |
509 | it('route should still validate crumb when autoGenerate is false and route.options.plugins.crumb is not defined', async () => {
510 |
511 | const server = Hapi.server();
512 |
513 | server.route([
514 | {
515 | method: 'POST',
516 | path: '/1',
517 | handler: (request, h) => {
518 |
519 | return 'bonjour';
520 | }
521 | }
522 | ]);
523 |
524 | await server.register([
525 | Vision,
526 | {
527 | plugin: Crumb,
528 | options: {
529 | autoGenerate: false,
530 | restful: true
531 | }
532 | }
533 | ]);
534 |
535 | server.views(internals.viewOptions);
536 |
537 | let res = await server.inject({ method: 'POST', url: '/1' });
538 | expect(res.statusCode).to.equal(403);
539 |
540 | const crumbValue = 'someCrumbValue';
541 | res = await server.inject({
542 | method: 'POST',
543 | url: '/1',
544 | headers: {
545 | cookie: `crumb=${crumbValue}`,
546 | 'X-CSRF-token': crumbValue
547 | }
548 | });
549 | expect(res.statusCode).to.equal(200);
550 | });
551 |
552 | it('fails validation when no payload provided and not using restful mode', async () => {
553 |
554 | const server = Hapi.server();
555 |
556 | server.route({
557 | method: 'POST',
558 | path: '/1',
559 | handler: (request, h) => 'test'
560 | });
561 |
562 | await server.register([Crumb]);
563 |
564 | const headers = {};
565 | headers['X-API-Token'] = 'test';
566 |
567 | const res = await server.inject({
568 | method: 'POST',
569 | url: '/1',
570 | headers
571 | });
572 |
573 | expect(res.statusCode).to.equal(403);
574 | });
575 |
576 | it('does not validate crumb when "skip" option returns true', async () => {
577 |
578 | const server = Hapi.server();
579 |
580 | server.route({
581 | method: 'POST',
582 | path: '/1',
583 | handler: (request, h) => 'test'
584 | });
585 |
586 | const skip = (request) => request.headers['x-api-token'] === 'test';
587 |
588 | const plugins = [
589 | {
590 | plugin: Crumb,
591 | options: {
592 | skip
593 | }
594 | }
595 | ];
596 |
597 | await server.register(plugins);
598 |
599 | const headers = {
600 | 'X-API-Token': 'test'
601 | };
602 |
603 | const res = await server.inject({
604 | method: 'POST',
605 | url: '/1',
606 | headers
607 | });
608 |
609 | const header = res.headers['set-cookie'];
610 |
611 | expect(res.statusCode).to.equal(200);
612 | expect(header).to.not.exist();
613 | });
614 |
615 | it('validates crumb when "skip" option returns false', async () => {
616 |
617 | const server = Hapi.server();
618 |
619 | server.route({
620 | method: 'POST',
621 | path: '/1',
622 | handler: (request, h) => 'test'
623 | });
624 |
625 | const skip = (request) => false;
626 |
627 | const plugins = [
628 | {
629 | plugin: Crumb,
630 | options: {
631 | skip
632 | }
633 | }
634 | ];
635 |
636 | await server.register(plugins);
637 |
638 | const headers = {
639 | 'X-API-Token': 'test'
640 | };
641 |
642 | const res = await server.inject({
643 | method: 'POST',
644 | url: '/1',
645 | headers
646 | });
647 |
648 | expect(res.statusCode).to.equal(403);
649 | });
650 |
651 | it('ensures crumb "skip" option is a function', async () => {
652 |
653 | const server = Hapi.server();
654 |
655 | server.route({
656 | method: 'POST',
657 | path: '/1',
658 | handler: (request, h) => 'test'
659 | });
660 |
661 | const skip = true;
662 |
663 | try {
664 | await server.register([
665 | Vision,
666 | {
667 | plugin: Crumb,
668 | options: { skip }
669 | }
670 | ]);
671 | }
672 | catch (err) {
673 | expect(err).to.exist();
674 | }
675 | });
676 |
677 | it('does not set crumb cookie insecurely', async () => {
678 |
679 | const server = Hapi.server({
680 | host: 'localhost',
681 | port: 80,
682 | routes: {
683 | cors: true
684 | }
685 | });
686 |
687 | server.route({
688 | method: 'GET',
689 | path: '/1',
690 | options: {
691 | cors: false
692 | },
693 | handler: (request, h) => 'test'
694 | });
695 |
696 | server.route({
697 | method: 'GET',
698 | path: '/2',
699 | handler: (request, h) => 'test'
700 | });
701 |
702 | server.route({
703 | method: 'GET',
704 | path: '/3',
705 | options: {
706 | cors: {
707 | origin: ['http://127.0.0.1']
708 | }
709 | },
710 | handler: (request, h) => 'test'
711 | });
712 |
713 | await server.register(Crumb);
714 |
715 | const headers = {};
716 |
717 | const res = await server.inject({
718 | method: 'GET',
719 | url: '/1',
720 | headers
721 | });
722 |
723 | const header = res.headers['set-cookie'];
724 | expect(header[0]).to.contain('crumb');
725 |
726 | headers.origin = 'http://localhost';
727 |
728 | const res2 = await server.inject({
729 | method: 'GET',
730 | url: '/2',
731 | headers
732 | });
733 |
734 | const header2 = res2.headers['set-cookie'];
735 | expect(header2[0]).to.contain('crumb');
736 |
737 | headers.origin = 'http://127.0.0.1';
738 |
739 | const res3 = await server.inject({
740 | method: 'GET',
741 | url: '/3',
742 | headers
743 | });
744 |
745 | const header3 = res3.headers['set-cookie'];
746 |
747 | expect(header3[0]).to.contain('crumb');
748 |
749 | const res4 = await server.inject({
750 | method: 'GET',
751 | url: '/3'
752 | });
753 |
754 | const header4 = res4.headers['set-cookie'];
755 |
756 | expect(header4[0]).to.contain('crumb');
757 |
758 | headers.origin = 'http://badsite.com';
759 |
760 | const res5 = await server.inject({
761 | method: 'GET',
762 | url: '/3',
763 | headers
764 | });
765 |
766 | const header5 = res5.headers['set-cookie'];
767 | expect(header5).to.not.exist();
768 | });
769 |
770 | it('does not set crumb cookie insecurely using https', async () => {
771 |
772 | const options = {
773 | host: 'localhost',
774 | port: 443,
775 | tls: TLSCert
776 | };
777 |
778 | const server = Hapi.server(options);
779 |
780 | server.route([
781 | {
782 | method: 'GET',
783 | path: '/1',
784 | handler: (request, h) => 'test'
785 | }
786 | ]);
787 |
788 | await server.register(Crumb);
789 |
790 | const res = await server.inject({
791 | method: 'GET',
792 | url: '/1',
793 | headers: {
794 | host: 'localhost:443'
795 | }
796 | });
797 |
798 | const header = res.headers['set-cookie'];
799 | expect(header[0]).to.contain('crumb');
800 | });
801 |
802 | it('validates crumb with X-CSRF-Token header', async () => {
803 |
804 | const server = Hapi.server();
805 |
806 | server.route([
807 | {
808 | method: 'GET',
809 | path: '/1',
810 | handler: (request, h) => {
811 |
812 | expect(request.plugins.crumb).to.exist();
813 | expect(request.server.plugins.crumb.generate).to.exist();
814 |
815 | return h.view('index', {
816 | title: 'test',
817 | message: 'hi'
818 | });
819 | }
820 | },
821 | {
822 | method: 'POST',
823 | path: '/2',
824 | handler: (request, h) => {
825 |
826 | expect(request.payload).to.equal({ key: 'value' });
827 | return 'valid';
828 | }
829 | },
830 | {
831 | method: 'POST',
832 | path: '/3',
833 | options: { payload: { output: 'stream' } },
834 | handler: (request, h) => 'never'
835 | },
836 | {
837 | method: 'PUT',
838 | path: '/4',
839 | handler: (request, h) => {
840 |
841 | expect(request.payload).to.equal({ key: 'value' });
842 | return 'valid';
843 | }
844 | },
845 | {
846 | method: 'PATCH',
847 | path: '/5',
848 | handler: (request, h) => {
849 |
850 | expect(request.payload).to.equal({ key: 'value' });
851 | return 'valid';
852 | }
853 | },
854 | {
855 | method: 'DELETE',
856 | path: '/6',
857 | handler: (request, h) => 'valid'
858 | },
859 | {
860 | method: 'POST',
861 | path: '/7',
862 | options: {
863 | plugins: {
864 | crumb: false
865 | }
866 | },
867 | handler: (request, h) => {
868 |
869 | expect(request.payload).to.equal({ key: 'value' });
870 | return 'valid';
871 | }
872 | },
873 | {
874 | method: 'POST',
875 | path: '/8',
876 | options: {
877 | plugins: {
878 | crumb: {
879 | restful: false,
880 | source: 'payload'
881 | }
882 | }
883 | },
884 | handler: (request, h) => {
885 |
886 | expect(request.payload).to.equal({ key: 'value' });
887 | return 'valid';
888 | }
889 | }
890 |
891 | ]);
892 |
893 | await server.register([
894 | Vision,
895 | {
896 | plugin: Crumb,
897 | options: {
898 | restful: true,
899 | cookieOptions: {
900 | isSecure: true
901 | }
902 | }
903 | }
904 | ]);
905 |
906 | server.views(internals.viewOptions);
907 |
908 | const res = await server.inject({
909 | method: 'GET',
910 | url: '/1'
911 | });
912 |
913 | const header = res.headers['set-cookie'];
914 | expect(header.length).to.equal(1);
915 | expect(header[0]).to.contain('Secure');
916 |
917 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
918 |
919 | const validHeader = {
920 | cookie: 'crumb=' + cookie[1],
921 | 'x-csrf-token': cookie[1]
922 | };
923 |
924 | const invalidHeader = {
925 | cookie: 'crumb=' + cookie[1],
926 | 'x-csrf-token': 'x' + cookie[1]
927 | };
928 |
929 | expect(res.result).to.equal(Views.viewWithCrumb(cookie[1]));
930 |
931 | const res2 = await server.inject({
932 | method: 'POST',
933 | url: '/2',
934 | payload: '{ "key": "value" }',
935 | headers: validHeader
936 | });
937 |
938 | expect(res2.result).to.equal('valid');
939 |
940 | const res3 = await server.inject({
941 | method: 'POST',
942 | url: '/2',
943 | payload: '{ "key": "value" }',
944 | headers: invalidHeader
945 | });
946 |
947 | expect(res3.statusCode).to.equal(403);
948 |
949 | const res4 = await server.inject({
950 | method: 'POST',
951 | url: '/3',
952 | headers: {
953 | cookie: 'crumb=' + cookie[1]
954 | }
955 | });
956 |
957 | expect(res4.statusCode).to.equal(403);
958 |
959 | const res5 = await server.inject({
960 | method: 'PUT',
961 | url: '/4',
962 | payload: '{ "key": "value" }',
963 | headers: validHeader
964 | });
965 |
966 | expect(res5.result).to.equal('valid');
967 |
968 | const res6 = await server.inject({
969 | method: 'PUT',
970 | url: '/4',
971 | payload: '{ "key": "value" }',
972 | headers: invalidHeader
973 | });
974 |
975 | expect(res6.statusCode).to.equal(403);
976 |
977 | const res7 = await server.inject({
978 | method: 'PATCH',
979 | url: '/5',
980 | payload: '{ "key": "value" }',
981 | headers: validHeader
982 | });
983 |
984 | expect(res7.result).to.equal('valid');
985 |
986 | const res8 = await server.inject({
987 | method: 'PATCH',
988 | url: '/5',
989 | payload: '{ "key": "value" }',
990 | headers: invalidHeader
991 | });
992 |
993 | expect(res8.statusCode).to.equal(403);
994 |
995 | const res9 = await server.inject({
996 | method: 'DELETE',
997 | url: '/6',
998 | headers: validHeader
999 | });
1000 |
1001 | expect(res9.result).to.equal('valid');
1002 |
1003 | const res10 = await server.inject({
1004 | method: 'DELETE',
1005 | url: '/6',
1006 | headers: invalidHeader
1007 | });
1008 |
1009 | expect(res10.statusCode).to.equal(403);
1010 |
1011 | const res11 = await server.inject({
1012 | method: 'POST',
1013 | url: '/7',
1014 | payload: '{ "key": "value" }'
1015 | });
1016 |
1017 | expect(res11.result).to.equal('valid');
1018 |
1019 | const payload = { key: 'value', crumb: cookie[1] };
1020 |
1021 | delete validHeader['x-csrf-token'];
1022 |
1023 | const res12 = await server.inject({
1024 | method: 'POST',
1025 | url: '/8',
1026 | payload: JSON.stringify(payload),
1027 | headers: validHeader
1028 | });
1029 |
1030 | expect(res12.statusCode).to.equal(200);
1031 | });
1032 |
1033 | it('validates crumb with a custom header name', async () => {
1034 |
1035 | const server = Hapi.server();
1036 |
1037 | server.route([
1038 | {
1039 | method: 'GET',
1040 | path: '/1',
1041 | handler: (request, h) => {
1042 |
1043 | expect(request.plugins.crumb).to.exist();
1044 | expect(request.server.plugins.crumb.generate).to.exist();
1045 |
1046 | return h.view('index', {
1047 | title: 'test',
1048 | message: 'hi'
1049 | });
1050 | }
1051 | },
1052 | {
1053 | method: 'POST',
1054 | path: '/2',
1055 | handler: (request, h) => {
1056 |
1057 | expect(request.payload).to.equal({ key: 'value' });
1058 | return 'valid';
1059 | }
1060 | },
1061 | {
1062 | method: 'POST',
1063 | path: '/3',
1064 | options: { payload: { output: 'stream' } },
1065 | handler: (request, h) => 'never'
1066 | },
1067 | {
1068 | method: 'PUT',
1069 | path: '/4',
1070 | handler: (request, h) => {
1071 |
1072 | expect(request.payload).to.equal({ key: 'value' });
1073 | return 'valid';
1074 | }
1075 | },
1076 | {
1077 | method: 'PATCH',
1078 | path: '/5',
1079 | handler: (request, h) => {
1080 |
1081 | expect(request.payload).to.equal({ key: 'value' });
1082 | return 'valid';
1083 | }
1084 | },
1085 | {
1086 | method: 'DELETE',
1087 | path: '/6',
1088 | handler: (request, h) => 'valid'
1089 | },
1090 | {
1091 | method: 'POST',
1092 | path: '/7',
1093 | options: {
1094 | plugins: {
1095 | crumb: false
1096 | }
1097 | },
1098 | handler: (request, h) => {
1099 |
1100 | expect(request.payload).to.equal({ key: 'value' });
1101 | return 'valid';
1102 | }
1103 | },
1104 | {
1105 | method: 'POST',
1106 | path: '/8',
1107 | options: {
1108 | plugins: {
1109 | crumb: {
1110 | restful: false,
1111 | source: 'payload'
1112 | }
1113 | }
1114 | },
1115 | handler: (request, h) => {
1116 |
1117 | expect(request.payload).to.equal({ key: 'value' });
1118 | return 'valid';
1119 | }
1120 | }
1121 |
1122 | ]);
1123 |
1124 | await server.register([
1125 | Vision,
1126 | {
1127 | plugin: Crumb,
1128 | options: {
1129 | restful: true,
1130 | cookieOptions: {
1131 | isSecure: true
1132 | },
1133 | headerName: 'X-CUSTOM-TOKEN'
1134 | }
1135 | }
1136 | ]);
1137 |
1138 | server.views(internals.viewOptions);
1139 |
1140 | const res = await server.inject({
1141 | method: 'GET',
1142 | url: '/1'
1143 | });
1144 |
1145 | const header = res.headers['set-cookie'];
1146 | expect(header.length).to.equal(1);
1147 | expect(header[0]).to.contain('Secure');
1148 |
1149 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
1150 |
1151 | const validHeader = {
1152 | cookie: 'crumb=' + cookie[1],
1153 | 'x-custom-token': cookie[1]
1154 | };
1155 |
1156 | const invalidHeader = {
1157 | cookie: 'crumb=' + cookie[1],
1158 | 'x-custom-token': 'x' + cookie[1]
1159 | };
1160 |
1161 | expect(res.result).to.equal(Views.viewWithCrumb(cookie[1]));
1162 |
1163 | const res2 = await server.inject({
1164 | method: 'POST',
1165 | url: '/2',
1166 | payload: '{ "key": "value" }',
1167 | headers: validHeader
1168 | });
1169 |
1170 | expect(res2.result).to.equal('valid');
1171 |
1172 | const res3 = await server.inject({
1173 | method: 'POST',
1174 | url: '/2',
1175 | payload: '{ "key": "value" }',
1176 | headers: invalidHeader
1177 | });
1178 |
1179 | expect(res3.statusCode).to.equal(403);
1180 |
1181 | const res4 = await server.inject({
1182 | method: 'POST',
1183 | url: '/3',
1184 | headers: {
1185 | cookie: 'crumb=' + cookie[1]
1186 | }
1187 | });
1188 |
1189 | expect(res4.statusCode).to.equal(403);
1190 |
1191 | const res5 = await server.inject({
1192 | method: 'PUT',
1193 | url: '/4',
1194 | payload: '{ "key": "value" }',
1195 | headers: validHeader
1196 | });
1197 |
1198 | expect(res5.result).to.equal('valid');
1199 |
1200 | const res6 = await server.inject({
1201 | method: 'PUT',
1202 | url: '/4',
1203 | payload: '{ "key": "value" }',
1204 | headers: invalidHeader
1205 | });
1206 |
1207 | expect(res6.statusCode).to.equal(403);
1208 |
1209 | const res7 = await server.inject({
1210 | method: 'PATCH',
1211 | url: '/5',
1212 | payload: '{ "key": "value" }',
1213 | headers: validHeader
1214 | });
1215 |
1216 | expect(res7.result).to.equal('valid');
1217 |
1218 | const res8 = await server.inject({
1219 | method: 'PATCH',
1220 | url: '/5',
1221 | payload: '{ "key": "value" }',
1222 | headers: invalidHeader
1223 | });
1224 |
1225 | expect(res8.statusCode).to.equal(403);
1226 |
1227 | const res9 = await server.inject({
1228 | method: 'DELETE',
1229 | url: '/6',
1230 | headers: validHeader
1231 | });
1232 |
1233 | expect(res9.result).to.equal('valid');
1234 |
1235 | const res10 = await server.inject({
1236 | method: 'DELETE',
1237 | url: '/6',
1238 | headers: invalidHeader
1239 | });
1240 |
1241 | expect(res10.statusCode).to.equal(403);
1242 |
1243 | const res11 = await server.inject({
1244 | method: 'POST',
1245 | url: '/7',
1246 | payload: '{ "key": "value" }'
1247 | });
1248 |
1249 | expect(res11.result).to.equal('valid');
1250 |
1251 | const payload = { key: 'value', crumb: cookie[1] };
1252 |
1253 | delete validHeader['x-custom-token'];
1254 |
1255 | const res12 = await server.inject({
1256 | method: 'POST',
1257 | url: '/8',
1258 | payload: JSON.stringify(payload),
1259 | headers: validHeader
1260 | });
1261 |
1262 | expect(res12.statusCode).to.equal(200);
1263 | });
1264 |
1265 | it('Adds to the request log if there are multiple cookie values', async () => {
1266 |
1267 | const server = Hapi.server();
1268 | let logFound;
1269 |
1270 | const preResponse = function (request, h) {
1271 |
1272 | const logs = request.logs;
1273 | logFound = logs.find((log) => {
1274 |
1275 | return log.tags[0] === 'crumb' && log.data === 'multiple cookies found';
1276 | });
1277 |
1278 | return h.continue;
1279 | };
1280 |
1281 | server.ext('onPreResponse', preResponse);
1282 |
1283 | server.route({
1284 | method: 'GET',
1285 | path: '/1',
1286 | handler: (request, h) => {
1287 |
1288 | return h.view('index', {
1289 | title: 'test',
1290 | message: 'hi'
1291 | });
1292 | }
1293 | });
1294 |
1295 | server.route({
1296 | method: 'POST',
1297 | path: '/2',
1298 | config: {
1299 | log: {
1300 | collect: true
1301 | }
1302 | },
1303 | handler: (request, h) => {
1304 |
1305 | return 'success';
1306 | }
1307 | });
1308 |
1309 | await server.register([{
1310 | plugin: Vision
1311 | }, {
1312 | plugin: Crumb
1313 | }]);
1314 |
1315 | server.views(internals.viewOptions);
1316 |
1317 | const res = await server.inject({
1318 | method: 'GET',
1319 | url: '/1'
1320 | });
1321 |
1322 | const header = res.headers['set-cookie'];
1323 |
1324 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
1325 |
1326 | const headers = {
1327 | cookie: 'crumb=' + cookie[1] + '; crumb=' + cookie[1] // multiple cookies
1328 | };
1329 |
1330 | await server.inject({
1331 | method: 'POST',
1332 | url: '/2',
1333 | payload: '{ "key": "value", "crumb": "' + cookie[1] + '" }',
1334 | headers
1335 | });
1336 | expect(logFound).to.exist();
1337 | });
1338 |
1339 | it('Adds to the request log if there are multiple cookie values in restful mode', async () => {
1340 |
1341 | const server = Hapi.server();
1342 | let logFound;
1343 |
1344 | const preResponse = function (request, h) {
1345 |
1346 | const logs = request.logs;
1347 | logFound = logs.find((log) => {
1348 |
1349 | return log.tags[0] === 'crumb' && log.data === 'multiple cookies found';
1350 | });
1351 |
1352 | return h.continue;
1353 | };
1354 |
1355 | server.ext('onPreResponse', preResponse);
1356 |
1357 | server.route({
1358 | method: 'GET',
1359 | path: '/1',
1360 | handler: (request, h) => {
1361 |
1362 | return h.view('index', {
1363 | title: 'test',
1364 | message: 'hi'
1365 | });
1366 | }
1367 | });
1368 |
1369 | server.route({
1370 | method: 'POST',
1371 | path: '/2',
1372 | config: {
1373 | log: {
1374 | collect: true
1375 | }
1376 | },
1377 | handler: (request, h) => {
1378 |
1379 | return 'success';
1380 | }
1381 | });
1382 |
1383 | await server.register([{
1384 | plugin: Vision
1385 | }, {
1386 | plugin: Crumb,
1387 | options: {
1388 | restful: true
1389 | }
1390 | }]);
1391 |
1392 | server.views(internals.viewOptions);
1393 |
1394 | const res = await server.inject({
1395 | method: 'GET',
1396 | url: '/1'
1397 | });
1398 |
1399 | const header = res.headers['set-cookie'];
1400 |
1401 | const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);
1402 |
1403 | const headers = {
1404 | cookie: 'crumb=' + cookie[1] + '; crumb=' + cookie[1], // multiple cookies
1405 | 'X-CSRF-Token': cookie[1]
1406 | };
1407 |
1408 | await server.inject({
1409 | method: 'POST',
1410 | url: '/2',
1411 | payload: '{ "stuff": true }',
1412 | headers
1413 | });
1414 | expect(logFound).to.exist();
1415 | });
1416 |
1417 | it('should set cookie but ignore check with enforce flag turned off', async () => {
1418 |
1419 | const server = Hapi.server();
1420 |
1421 | server.route({
1422 | method: 'POST',
1423 | path: '/1',
1424 | handler: (request, h) => 'test'
1425 | });
1426 |
1427 | const plugins = [
1428 | {
1429 | plugin: Crumb,
1430 | options: {
1431 | enforce: false
1432 | }
1433 | }
1434 | ];
1435 |
1436 | await server.register(plugins);
1437 |
1438 | const headers = {
1439 | 'X-API-Token': 'test'
1440 | };
1441 |
1442 | const res = await server.inject({
1443 | method: 'POST',
1444 | url: '/1',
1445 | headers
1446 | });
1447 |
1448 | const header = res.headers['set-cookie'];
1449 |
1450 | expect(header).to.exist();
1451 | expect(res.statusCode).to.equal(200);
1452 | });
1453 | });
1454 |
--------------------------------------------------------------------------------