From 3e67f0a455546f3ce50ac2dc52f5a341ddf17cac Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Fri, 7 May 2021 15:51:43 +0100 Subject: [PATCH] fix: reject requests when cors origin list is empty (#3674) (#3676) * fix: reject requests when cors origin list is empty (#3674) If CORS origin list is empty, Hapi throws an error as it considers that to be invalid configuration. We want to reject requests that send and origin or a referer when no allowed origins have been configured, so when these headers are sent, reject the request if no allowed origins are present in the config. Co-authored-by: Vasco Santos Co-authored-by: Marcin Rataj * chore: ts-ignore until #3655 lands Co-authored-by: Vasco Santos Co-authored-by: Marcin Rataj --- .../src/utils/load-services.js | 1 + .../src/utils/load-services.js | 1 + packages/ipfs-http-server/src/index.js | 42 ++++++- packages/ipfs-http-server/test/cors.js | 111 ++++++++++++++++++ 4 files changed, 152 insertions(+), 3 deletions(-) diff --git a/packages/ipfs-grpc-client/src/utils/load-services.js b/packages/ipfs-grpc-client/src/utils/load-services.js index d929136ad6..55eb9e1ff2 100644 --- a/packages/ipfs-grpc-client/src/utils/load-services.js +++ b/packages/ipfs-grpc-client/src/utils/load-services.js @@ -21,6 +21,7 @@ const CONVERSION_OPTS = { * @returns {object} */ module.exports = function loadServices () { + // @ts-ignore protobuf's generated types are incompatible with it's own types const root = protobuf.Root.fromJSON(protocol) const output = {} diff --git a/packages/ipfs-grpc-server/src/utils/load-services.js b/packages/ipfs-grpc-server/src/utils/load-services.js index c901d4e1d0..c236f1b991 100644 --- a/packages/ipfs-grpc-server/src/utils/load-services.js +++ b/packages/ipfs-grpc-server/src/utils/load-services.js @@ -14,6 +14,7 @@ const CONVERSION_OPTS = { } module.exports = function loadServices () { + // @ts-ignore protobuf's generated types are incompatible with it's own types const root = protobuf.Root.fromJSON(protocol) const output = {} diff --git a/packages/ipfs-http-server/src/index.js b/packages/ipfs-http-server/src/index.js index 409452b0c5..b0f9ff223f 100644 --- a/packages/ipfs-http-server/src/index.js +++ b/packages/ipfs-http-server/src/index.js @@ -40,6 +40,36 @@ async function serverCreator (serverAddrs, createServer, ipfs, cors) { return servers } +/** + * @param {string} [str] + * @param {string[]} [allowedOrigins] + */ +function isAllowedOrigin (str, allowedOrigins = []) { + if (!str) { + return false + } + + let origin + + try { + origin = (new URL(str)).origin + } catch { + return false + } + + for (const allowedOrigin of allowedOrigins) { + if (allowedOrigin === '*') { + return true + } + + if (allowedOrigin === origin) { + return true + } + } + + return false +} + class HttpApi { constructor (ipfs, options = {}) { this._ipfs = ipfs @@ -150,11 +180,17 @@ class HttpApi { const headers = request.headers || {} const origin = headers.origin || '' - const referrer = headers.referrer || '' + const referer = headers.referer || '' const userAgent = headers['user-agent'] || '' - // If these are set, we leave up to CORS and CSRF checks. - if (origin || referrer) { + // If these are set, check them against the configured list + if (origin || referer) { + if (!isAllowedOrigin(origin || referer, cors.origin)) { + // Hapi will not allow an empty CORS origin list so we have to manually + // reject the request if CORS origins have not been configured + throw Boom.forbidden() + } + return h.continue } diff --git a/packages/ipfs-http-server/test/cors.js b/packages/ipfs-http-server/test/cors.js index e96a8c87b8..3c5962f146 100644 --- a/packages/ipfs-http-server/test/cors.js +++ b/packages/ipfs-http-server/test/cors.js @@ -32,9 +32,23 @@ describe('cors', () => { } }, { ipfs, cors: { origin: [origin] } }) + expect(res).to.have.property('statusCode', 200) expect(res).to.have.nested.property('headers.access-control-allow-origin', origin) }) + it('allows request when referer is supplied in request', async () => { + const origin = 'http://localhost:8080' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + referer: origin + '/index.html' + } + }, { ipfs, cors: { origin: [origin] } }) + + expect(res).to.have.property('statusCode', 200) + }) + it('does not allow credentials when omitted in config', async () => { const origin = 'http://localhost:8080' const res = await http({ @@ -149,5 +163,102 @@ describe('cors', () => { expect(res).to.have.property('statusCode', 404) }) + + it('rejects requests when cors origin list is empty and origin is sent', async () => { + const origin = 'http://localhost:8080' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + origin + } + }, { + ipfs, + cors: { origin: [] } + }) + + expect(res).to.have.property('statusCode', 403) + }) + + it('rejects requests when cors origin list is empty and referer is sent', async () => { + const referer = 'http://localhost:8080/index.html' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + referer + } + }, { + ipfs, + cors: { origin: [] } + }) + + expect(res).to.have.property('statusCode', 403) + }) + + it('rejects requests when cors origin list is empty and referer and origin are sent', async () => { + const referer = 'http://localhost:8080/index.html' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + referer, + origin: 'http://localhost:8080' + } + }, { + ipfs, + cors: { origin: [] } + }) + + expect(res).to.have.property('statusCode', 403) + }) + + it('rejects requests when cors origin list is empty and origin is sent as "null" (e.g. data urls and sandboxed iframes)', async () => { + const origin = 'null' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + origin + } + }, { + ipfs, + cors: { origin: [] } + }) + + expect(res).to.have.property('statusCode', 403) + }) + + it('rejects requests when cors origin list does not contain the correct origin and origin is sent', async () => { + const origin = 'http://localhost:8080' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + origin + } + }, { + ipfs, + cors: { origin: ['http://example.com:8080'] } + }) + + expect(res).to.have.property('statusCode', 403) + }) + + it('rejects requests when cors origin list does not contain the correct origin and referer is sent', async () => { + const referer = 'http://localhost:8080/index.html' + const res = await http({ + method: 'POST', + url: '/api/v0/id', + headers: { + referer + } + }, { + ipfs, + cors: { origin: ['http://example.com:8080'] } + }) + + expect(res).to.have.property('statusCode', 403) + }) }) })