From 35395291140dc48b596638a1b10eaa8fb0039cde Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 19 May 2024 18:20:52 -0400 Subject: [PATCH 1/6] [sitecore-jss-nextjs][sitecore-jss] Introduce multi-origin CORS validation and apply it to next editing middlewares --- .../src/editing/constants.ts | 6 +++ .../editing/editing-config-middleware.test.ts | 27 +++++++++- .../src/editing/editing-config-middleware.ts | 9 +++- .../editing/editing-data-middleware.test.ts | 34 +++++++++++- .../src/editing/editing-data-middleware.ts | 10 +++- .../editing/editing-render-middleware.test.ts | 42 +++++++++++++-- .../src/editing/editing-render-middleware.ts | 12 ++++- .../editing/feaas-render-middleware.test.ts | 28 +++++++++- .../src/editing/feaas-render-middleware.ts | 11 +++- .../sitecore-jss-nextjs/src/utils/index.ts | 1 + packages/sitecore-jss/src/utils/index.ts | 2 +- packages/sitecore-jss/src/utils/utils.test.ts | 54 ++++++++++++++++++- packages/sitecore-jss/src/utils/utils.ts | 19 +++++++ 13 files changed, 240 insertions(+), 15 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/constants.ts b/packages/sitecore-jss-nextjs/src/editing/constants.ts index 908e4d1fa8..52dd937259 100644 --- a/packages/sitecore-jss-nextjs/src/editing/constants.ts +++ b/packages/sitecore-jss-nextjs/src/editing/constants.ts @@ -1,3 +1,9 @@ export const QUERY_PARAM_EDITING_SECRET = 'secret'; export const QUERY_PARAM_PROTECTION_BYPASS_SITECORE = 'x-sitecore-protection-bypass'; export const QUERY_PARAM_PROTECTION_BYPASS_VERCEL = 'x-vercel-protection-bypass'; +export const EDITING_ALLOWED_ORIGINS = [ + 'https://pages-dev.sitecore-staging.cloud/', + 'https://pages-staging.sitecore-staging.cloud/', + 'https://pages-preprod.sitecorecloud.io/', + 'https://pages.sitecorecloud.io/', +]; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts index 7f41514412..2b04d3aa81 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts @@ -9,10 +9,16 @@ type Query = { [key: string]: string; }; -const mockRequest = (method: string, query?: Query) => { +const allowedOrigin = 'https://allowed.com'; + +const mockRequest = (method: string, query?: Query, headers?: { [key: string]: string }) => { return { method, query: query ?? {}, + headers: { + origin: allowedOrigin, + ...headers, + }, } as NextApiRequest; }; @@ -24,6 +30,9 @@ const mockResponse = () => { res.json = spy(() => { return res; }); + res.setHeader = spy(() => { + return res; + }); return res; }; @@ -44,10 +53,12 @@ describe('EditingConfigMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; + process.env.API_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; + delete process.env.API_ALLOWED_ORIGINS; }); it('should respond with 401 for missing secret', async () => { @@ -66,6 +77,20 @@ describe('EditingConfigMiddleware', () => { expect(res.json).to.have.been.calledWith(expectedResultForbidden); }); + it('should stop request and return 401 when CORS match is not met', async () => { + const req = mockRequest('GET', {}, { origin: 'https://notallowed.com' }); + const res = mockResponse(); + const middleware = new EditingConfigMiddleware({ components: componentsArray, metadata }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.json).to.have.been.calledOnce; + expect(res.json).to.have.been.calledWith({ message: 'Invalid origin' }); + }); + it('should respond with 401 for invalid secret', async () => { const key = 'wrongkey'; const query = { key } as Query; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts index 0de40da321..cece9230ce 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts @@ -1,8 +1,9 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { QUERY_PARAM_EDITING_SECRET } from './constants'; +import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; import { debug } from '@sitecore-jss/sitecore-jss'; import { Metadata } from '@sitecore-jss/sitecore-jss-dev-tools'; +import { enforceCors } from '../utils/index'; export type EditingConfigMiddlewareConfig = { /** @@ -35,6 +36,12 @@ export class EditingConfigMiddleware { private handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { const secret = _req.query[QUERY_PARAM_EDITING_SECRET]; + if (!enforceCors(_req, res, EDITING_ALLOWED_ORIGINS)) { + debug.editing( + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + ); + return res.status(401).json({ message: 'Invalid origin' }); + } if (secret !== getJssEditingSecret()) { debug.editing( 'invalid editing secret - sent "%s" expected "%s"', diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts index bdb7bb2946..184e038697 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts @@ -15,11 +15,22 @@ type Query = { [key: string]: string; }; -const mockRequest = (method: string, query?: Query, body?: unknown) => { +const allowedOrigin = 'https://allowed.com'; + +const mockRequest = ( + method: string, + query?: Query, + body?: unknown, + headers?: { [key: string]: string } +) => { return { method, query: query ?? {}, body: body ?? {}, + headers: { + origin: allowedOrigin, + ...headers, + }, } as NextApiRequest; }; @@ -34,7 +45,9 @@ const mockResponse = () => { res.end = spy(() => { return res; }); - res.setHeader = spy(); + res.setHeader = spy(() => { + return res; + }); return res; }; @@ -59,10 +72,12 @@ describe('EditingDataMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; + process.env.API_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; + delete process.env.API_ALLOWED_ORIGINS; }); it('should handle PUT request', async () => { @@ -132,6 +147,21 @@ describe('EditingDataMiddleware', () => { expect(res.json).to.have.been.calledWith(mockEditingData); }); + it('should stop request and return 401 when CORS match is not met', async () => { + const req = mockRequest('GET', {}, {}, { origin: 'https://notallowed.com' }); + const res = mockResponse(); + const cache = mockCache(); + const middleware = new EditingDataMiddleware({ editingDataCache: cache }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.json).to.have.been.calledOnce; + expect(res.json).to.have.been.calledWith({ message: 'Invalid origin' }); + }); + it('should respond with 400 for invalid editing data', async () => { const key = 'key1234'; const query = { key } as Query; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts index 0af412c141..01fe9afa42 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts @@ -1,8 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { EditingDataCache, editingDataDiskCache } from './editing-data-cache'; import { EditingData, isEditingData } from './editing-data'; -import { QUERY_PARAM_EDITING_SECRET } from './constants'; +import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; +import { enforceCors } from '../utils'; +import { debug } from '@sitecore-jss/sitecore-jss'; export interface EditingDataMiddlewareConfig { /** @@ -51,6 +53,12 @@ export class EditingDataMiddleware { const secret = query[QUERY_PARAM_EDITING_SECRET]; const key = query[this.queryParamKey]; + if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { + debug.editing( + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + ); + return res.status(401).json({ message: 'Invalid origin' }); + } // Validate secret if (secret !== getJssEditingSecret()) { res.status(401).end('Missing or invalid secret'); diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts index 7bade6fc70..0835690e22 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts @@ -32,12 +32,23 @@ type Query = { [key: string]: string; }; -const mockRequest = (body?: any, query?: Query, method?: string, host?: string) => { +const allowedOrigin = 'https://allowed.com'; + +const mockRequest = ( + body?: any, + query?: Query, + method?: string, + headers?: { [key: string]: string } +) => { return { body: body ?? {}, method: method ?? 'POST', query: query ?? {}, - headers: { host: host ?? 'localhost:3000' }, + headers: { + host: 'localhost:3000', + origin: allowedOrigin, + ...headers, + }, } as NextApiRequest; }; @@ -83,12 +94,14 @@ describe('EditingRenderMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; + process.env.API_ALLOWED_ORIGINS = allowedOrigin; delete process.env.VERCEL; }); after(() => { delete process.env.JSS_EDITING_SECRET; delete process.env.VERCEL; + delete process.env.API_ALLOWED_ORIGINS; }); it('should handle request', async () => { @@ -312,6 +325,27 @@ describe('EditingRenderMiddleware', () => { expect(res.json).to.have.been.calledOnce; }); + it('should stop request and return 401 when CORS match is not met', async () => { + const req = mockRequest({}, {}, 'POST', { origin: 'https://notallowed.com' }); + const res = mockResponse(); + const fetcher = mockFetcher(); + const dataService = mockDataService(); + const middleware = new EditingRenderMiddleware({ + dataFetcher: fetcher, + editingDataService: dataService, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.json).to.have.been.calledOnce; + expect(res.json).to.have.been.calledWith({ + html: 'Requests from origin https://notallowed.com not allowed', + }); + }); + it('should respond with 401 for missing secret', async () => { const fetcher = mockFetcher(); const dataService = mockDataService(); @@ -381,7 +415,7 @@ describe('EditingRenderMiddleware', () => { const dataService = mockDataService(); const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(EE_BODY, query, undefined, 'testhostheader.com'); + const req = mockRequest(EE_BODY, query, undefined, { host: 'testhostheader.com' }); const res = mockResponse(); const middleware = new EditingRenderMiddleware({ @@ -401,7 +435,7 @@ describe('EditingRenderMiddleware', () => { const dataService = mockDataService(); const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(EE_BODY, query, undefined, 'vercel.com'); + const req = mockRequest(EE_BODY, query, undefined, { host: 'vercel.com' }); const res = mockResponse(); process.env.VERCEL = '1'; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index 789121278f..62134e6fcb 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -5,9 +5,10 @@ import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/ import { parse } from 'node-html-parser'; import { EditingData } from './editing-data'; import { EditingDataService, editingDataService } from './editing-data-service'; -import { QUERY_PARAM_EDITING_SECRET } from './constants'; +import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; import { RenderMiddlewareBase } from './render-middleware'; +import { enforceCors } from '../utils'; export interface EditingRenderMiddlewareConfig { /** @@ -87,6 +88,15 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase { body, }); + if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { + debug.editing( + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + ); + return res.status(401).json({ + html: `Requests from origin ${req.headers?.origin} not allowed`, + }); + } + if (method !== 'POST') { debug.editing('invalid method - sent %s expected POST', method); res.setHeader('Allow', 'POST'); diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts index 3b98b23ab3..cb47629038 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts @@ -18,12 +18,18 @@ type Query = { [key: string]: string; }; -const mockRequest = (query?: Query, method?: string, host?: string) => { +const allowedOrigin = 'https://allowed.com'; + +const mockRequest = (query?: Query, method?: string, headers?: { [key: string]: string }) => { return { body: {}, method: method ?? 'GET', query: query ?? {}, - headers: { host: host ?? 'localhost:3000' }, + headers: { + host: 'localhost:3000', + origin: allowedOrigin, + ...headers, + }, } as NextApiRequest; }; @@ -53,10 +59,12 @@ describe('FEAASRenderMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; + process.env.API_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; + delete process.env.API_ALLOWED_ORIGINS; }); it('should handle request', async () => { @@ -138,6 +146,22 @@ describe('FEAASRenderMiddleware', () => { ); }); + it('should stop request and return 401 when CORS match is not met', async () => { + const req = mockRequest({}, 'POST', { origin: 'https://notallowed.com' }); + const res = mockResponse(); + const middleware = new FEAASRenderMiddleware(); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith( + 'Requests from origin https://notallowed.com are not allowed' + ); + }); + it('should respond with 401 for missing secret', async () => { const query = {} as Query; const req = mockRequest(query); diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts index 98c2e53a28..136e203e97 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -1,8 +1,9 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { debug } from '@sitecore-jss/sitecore-jss'; -import { QUERY_PARAM_EDITING_SECRET } from './constants'; +import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; import { RenderMiddlewareBase } from './render-middleware'; +import { enforceCors } from '../utils'; /** * Configuration for `FEAASRenderMiddleware`. @@ -52,6 +53,14 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { headers, }); + if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { + return res + .status(401) + .send( + `Requests from origin ${req.headers?.origin} are not allowed` + ); + } + if (method !== 'GET') { debug.editing('invalid method - sent %s expected GET', method); res.setHeader('Allow', 'GET'); diff --git a/packages/sitecore-jss-nextjs/src/utils/index.ts b/packages/sitecore-jss-nextjs/src/utils/index.ts index bb06217af3..36ca991df8 100644 --- a/packages/sitecore-jss-nextjs/src/utils/index.ts +++ b/packages/sitecore-jss-nextjs/src/utils/index.ts @@ -4,4 +4,5 @@ export { isEditorActive, resetEditorChromes, resolveUrl, + enforceCors, } from '@sitecore-jss/sitecore-jss/utils'; diff --git a/packages/sitecore-jss/src/utils/index.ts b/packages/sitecore-jss/src/utils/index.ts index cde295e130..1706233ed1 100644 --- a/packages/sitecore-jss/src/utils/index.ts +++ b/packages/sitecore-jss/src/utils/index.ts @@ -1,5 +1,5 @@ export { default as isServer } from './is-server'; -export { resolveUrl, isAbsoluteUrl, isTimeoutError } from './utils'; +export { resolveUrl, isAbsoluteUrl, isTimeoutError, enforceCors } from './utils'; export { tryParseEnvValue } from './env'; export { ExperienceEditor, diff --git a/packages/sitecore-jss/src/utils/utils.test.ts b/packages/sitecore-jss/src/utils/utils.test.ts index c6a5baed1e..5a1adee81f 100644 --- a/packages/sitecore-jss/src/utils/utils.test.ts +++ b/packages/sitecore-jss/src/utils/utils.test.ts @@ -2,7 +2,8 @@ import { expect, spy } from 'chai'; import { isEditorActive, resetEditorChromes, isServer, resolveUrl } from '.'; import { ChromeRediscoveryGlobalFunctionName } from './editing'; -import { isAbsoluteUrl, isTimeoutError } from './utils'; +import { enforceCors, isAbsoluteUrl, isTimeoutError } from './utils'; +import { IncomingMessage, OutgoingMessage } from 'http'; // must make TypeScript happy with `global` variable modification interface CustomWindow { @@ -181,4 +182,55 @@ describe('utils', () => { expect(isTimeoutError({ name: 'AbortError' })).to.be.true; }); }); + + describe('enforceCors', () => { + const mockOrigin = 'https://maybeallowed.com'; + const mockRequest = (origin?: string) => { + return { + headers: { + origin: origin || mockOrigin, + }, + } as IncomingMessage; + }; + + const mockResponse = () => { + const res = {} as OutgoingMessage; + res.setHeader = spy(() => { + return res; + }); + + return res; + }; + + it('should return true if origin matches allowedOrigins from API_ALLOWED_ORIGINS env variable', () => { + const req = mockRequest(); + const res = mockResponse(); + process.env.API_ALLOWED_ORIGINS = mockOrigin; + expect(enforceCors(req, res)).to.be.equal(true); + delete process.env.API_ALLOWED_ORIGINS; + }); + + it('should return true if origin matches allowedOrigins passed as argument', () => { + const req = mockRequest('http://allowed.com'); + const res = mockResponse(); + + expect(enforceCors(req, res, ['http://allowed.com'])).to.be.equal(true); + }); + + it('should return false if origin matches neither allowedOrigins from API_ALLOWED_ORIGINS env variable nor argument', () => { + const req = mockRequest('https://notallowed.com'); + const res = mockResponse(); + process.env.API_ALLOWED_ORIGINS = 'https://strictallowed.com, https://alsoallowed.com'; + expect(enforceCors(req, res, ['https://paramallowed.com'])).to.be.equal(false); + delete process.env.API_ALLOWED_ORIGINS; + }); + + it('should set Access-Control-Allow-Origin header for matching origin', () => { + const req = mockRequest(); + const res = mockResponse(); + + enforceCors(req, res, [mockOrigin]); + expect(res.setHeader).to.have.been.called.with('Access-Control-Allow-Origin', mockOrigin); + }); + }); }); diff --git a/packages/sitecore-jss/src/utils/utils.ts b/packages/sitecore-jss/src/utils/utils.ts index 137a0718de..c13a393335 100644 --- a/packages/sitecore-jss/src/utils/utils.ts +++ b/packages/sitecore-jss/src/utils/utils.ts @@ -2,6 +2,7 @@ import isServer from './is-server'; import { ParsedUrlQueryInput } from 'querystring'; import { AxiosError } from 'axios'; import { ResponseError } from '../data-fetcher'; +import { IncomingMessage, OutgoingMessage } from 'http'; /** * note: encodeURIComponent is available via browser (window) or natively in node.js @@ -75,3 +76,21 @@ export const isTimeoutError = (error: unknown) => { (error as Error).name === 'AbortError' ); }; + +export const enforceCors = ( + req: IncomingMessage, + res: OutgoingMessage, + allowedOrigins?: string[] +): boolean => { + const defaultAllowedOrigins = process.env.API_ALLOWED_ORIGINS + ? process.env.API_ALLOWED_ORIGINS.replace(' ', '').split(',') + : []; + allowedOrigins = defaultAllowedOrigins.concat(allowedOrigins || []); + const origin = req.headers.origin; + if (origin && allowedOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH'); + return true; + } + return false; +}; From 0e79cb806e59dc1a2ea6143eb387841c0324c396 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 19 May 2024 18:41:42 -0400 Subject: [PATCH 2/6] add wildcard matching --- .../sitecore-jss-nextjs/src/editing/constants.ts | 7 +------ packages/sitecore-jss/src/utils/utils.test.ts | 10 ++++++++-- packages/sitecore-jss/src/utils/utils.ts | 13 ++++++++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/constants.ts b/packages/sitecore-jss-nextjs/src/editing/constants.ts index 52dd937259..6bde41577f 100644 --- a/packages/sitecore-jss-nextjs/src/editing/constants.ts +++ b/packages/sitecore-jss-nextjs/src/editing/constants.ts @@ -1,9 +1,4 @@ export const QUERY_PARAM_EDITING_SECRET = 'secret'; export const QUERY_PARAM_PROTECTION_BYPASS_SITECORE = 'x-sitecore-protection-bypass'; export const QUERY_PARAM_PROTECTION_BYPASS_VERCEL = 'x-vercel-protection-bypass'; -export const EDITING_ALLOWED_ORIGINS = [ - 'https://pages-dev.sitecore-staging.cloud/', - 'https://pages-staging.sitecore-staging.cloud/', - 'https://pages-preprod.sitecorecloud.io/', - 'https://pages.sitecorecloud.io/', -]; +export const EDITING_ALLOWED_ORIGINS = ['https://pages*.cloud/', 'https://pages.sitecorecloud.io/']; diff --git a/packages/sitecore-jss/src/utils/utils.test.ts b/packages/sitecore-jss/src/utils/utils.test.ts index 5a1adee81f..3e106da96b 100644 --- a/packages/sitecore-jss/src/utils/utils.test.ts +++ b/packages/sitecore-jss/src/utils/utils.test.ts @@ -202,7 +202,7 @@ describe('utils', () => { return res; }; - it('should return true if origin matches allowedOrigins from API_ALLOWED_ORIGINS env variable', () => { + it('should return true if origin is found in allowedOrigins from API_ALLOWED_ORIGINS env variable', () => { const req = mockRequest(); const res = mockResponse(); process.env.API_ALLOWED_ORIGINS = mockOrigin; @@ -210,7 +210,7 @@ describe('utils', () => { delete process.env.API_ALLOWED_ORIGINS; }); - it('should return true if origin matches allowedOrigins passed as argument', () => { + it('should return true if origin is found in allowedOrigins passed as argument', () => { const req = mockRequest('http://allowed.com'); const res = mockResponse(); @@ -225,6 +225,12 @@ describe('utils', () => { delete process.env.API_ALLOWED_ORIGINS; }); + it('should return true when origin matches a wildcard value from allowedOrigins', () => { + const req = mockRequest('https://veryallowed.com'); + const res = mockResponse(); + expect(enforceCors(req, res, ['https://*owed.com'])).to.be.equal(true); + }); + it('should set Access-Control-Allow-Origin header for matching origin', () => { const req = mockRequest(); const res = mockResponse(); diff --git a/packages/sitecore-jss/src/utils/utils.ts b/packages/sitecore-jss/src/utils/utils.ts index c13a393335..3e4bf4ec64 100644 --- a/packages/sitecore-jss/src/utils/utils.ts +++ b/packages/sitecore-jss/src/utils/utils.ts @@ -77,6 +77,10 @@ export const isTimeoutError = (error: unknown) => { ); }; +const convertToRegex = (pattern: string) => { + return pattern.replace('.', '\\.').replace(/\*/g, '.*'); +}; + export const enforceCors = ( req: IncomingMessage, res: OutgoingMessage, @@ -87,7 +91,14 @@ export const enforceCors = ( : []; allowedOrigins = defaultAllowedOrigins.concat(allowedOrigins || []); const origin = req.headers.origin; - if (origin && allowedOrigins.includes(origin)) { + if ( + origin && + allowedOrigins.some( + (allowedOrigin) => + origin === allowedOrigin || + new RegExp('^' + convertToRegex(allowedOrigin) + '$').test(origin) + ) + ) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH'); return true; From 64a8ff1a2175644f99f863f86d4652ff4d2e6a8a Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 20 May 2024 09:33:01 -0400 Subject: [PATCH 3/6] tsdoc, extra debug and utils unit test adjustment --- .../src/editing/editing-config-middleware.ts | 2 +- .../src/editing/editing-data-middleware.ts | 2 +- .../src/editing/editing-render-middleware.ts | 2 +- .../src/editing/feaas-render-middleware.ts | 3 +++ packages/sitecore-jss/src/utils/utils.test.ts | 8 +++++-- packages/sitecore-jss/src/utils/utils.ts | 23 +++++++++++++++---- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts index cece9230ce..1b78add31e 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts @@ -38,7 +38,7 @@ export class EditingConfigMiddleware { const secret = _req.query[QUERY_PARAM_EDITING_SECRET]; if (!enforceCors(_req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ message: 'Invalid origin' }); } diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts index 01fe9afa42..73511bc30a 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts @@ -55,7 +55,7 @@ export class EditingDataMiddleware { if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ message: 'Invalid origin' }); } diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index 62134e6fcb..aa003fd549 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -90,7 +90,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase { if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS env property' + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ html: `Requests from origin ${req.headers?.origin} not allowed`, diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts index 136e203e97..4bb4b1a17d 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -54,6 +54,9 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { }); if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { + debug.editing( + 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' + ); return res .status(401) .send( diff --git a/packages/sitecore-jss/src/utils/utils.test.ts b/packages/sitecore-jss/src/utils/utils.test.ts index 3e106da96b..604da9f9e2 100644 --- a/packages/sitecore-jss/src/utils/utils.test.ts +++ b/packages/sitecore-jss/src/utils/utils.test.ts @@ -231,12 +231,16 @@ describe('utils', () => { expect(enforceCors(req, res, ['https://*owed.com'])).to.be.equal(true); }); - it('should set Access-Control-Allow-Origin header for matching origin', () => { + it('should set Access-Control-Allow-Origin and Access-Control-Allow-Methods headers for matching origin', () => { const req = mockRequest(); const res = mockResponse(); - + const allowedMethods = 'GET, POST, OPTIONS, DELETE, PUT, PATCH'; enforceCors(req, res, [mockOrigin]); expect(res.setHeader).to.have.been.called.with('Access-Control-Allow-Origin', mockOrigin); + expect(res.setHeader).to.have.been.called.with( + 'Access-Control-Allow-Methods', + allowedMethods + ); }); }); }); diff --git a/packages/sitecore-jss/src/utils/utils.ts b/packages/sitecore-jss/src/utils/utils.ts index 3e4bf4ec64..d28d00c7c5 100644 --- a/packages/sitecore-jss/src/utils/utils.ts +++ b/packages/sitecore-jss/src/utils/utils.ts @@ -77,10 +77,24 @@ export const isTimeoutError = (error: unknown) => { ); }; -const convertToRegex = (pattern: string) => { - return pattern.replace('.', '\\.').replace(/\*/g, '.*'); +/** + * Converts a string value in a regex pattern allowing wildcard matching + * @param {string} pattern input with wildcards i.e. site.*.com + * @returns {string} modified string that can be used as regexp input + */ +const convertToWildcardRegex = (pattern: string) => { + return '^' + pattern.replace('.', '\\.').replace(/\*/g, '.*') + '$'; }; +/** + * Tests origin from incoming request against allowed origins list that can be + * set in API_ALLOWED_ORIGINS and/or passed via allowedOrigins param. + * Applies Access-Control-Allow-Origin and Access-Control-Allow-Methods on match + * @param {IncomingMessage} req incoming request + * @param {OutgoingMessage} res response to set CORS headers for + * @param {string[]} [allowedOrigins] additional list of origins to test against + * @returns true if incoming origin matches the allowed lists, false when it does not + */ export const enforceCors = ( req: IncomingMessage, res: OutgoingMessage, @@ -95,12 +109,11 @@ export const enforceCors = ( origin && allowedOrigins.some( (allowedOrigin) => - origin === allowedOrigin || - new RegExp('^' + convertToRegex(allowedOrigin) + '$').test(origin) + origin === allowedOrigin || new RegExp(convertToWildcardRegex(allowedOrigin)).test(origin) ) ) { res.setHeader('Access-Control-Allow-Origin', origin); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE, PUT, PATCH'); return true; } return false; From 5653c3e81f9f10e10f536f90df1de5758c9e56c9 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 20 May 2024 09:53:41 -0400 Subject: [PATCH 4/6] rename API_ALLOWED_ORIGINS to JSS_ALLOWED_ORIGINS, remove enforceCors re-export --- .../src/editing/editing-config-middleware.test.ts | 4 ++-- .../src/editing/editing-config-middleware.ts | 4 ++-- .../src/editing/editing-data-middleware.test.ts | 4 ++-- .../src/editing/editing-data-middleware.ts | 4 ++-- .../src/editing/editing-render-middleware.test.ts | 4 ++-- .../src/editing/editing-render-middleware.ts | 4 ++-- .../src/editing/feaas-render-middleware.test.ts | 4 ++-- .../src/editing/feaas-render-middleware.ts | 4 ++-- packages/sitecore-jss-nextjs/src/utils/index.ts | 1 - packages/sitecore-jss/src/utils/utils.test.ts | 12 ++++++------ packages/sitecore-jss/src/utils/utils.ts | 6 +++--- 11 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts index 2b04d3aa81..c0576fa00b 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.test.ts @@ -53,12 +53,12 @@ describe('EditingConfigMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; - process.env.API_ALLOWED_ORIGINS = allowedOrigin; + process.env.JSS_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should respond with 401 for missing secret', async () => { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts index 1b78add31e..2cefa06506 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-config-middleware.ts @@ -3,7 +3,7 @@ import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants import { getJssEditingSecret } from '../utils/utils'; import { debug } from '@sitecore-jss/sitecore-jss'; import { Metadata } from '@sitecore-jss/sitecore-jss-dev-tools'; -import { enforceCors } from '../utils/index'; +import { enforceCors } from '@sitecore-jss/sitecore-jss/utils'; export type EditingConfigMiddlewareConfig = { /** @@ -38,7 +38,7 @@ export class EditingConfigMiddleware { const secret = _req.query[QUERY_PARAM_EDITING_SECRET]; if (!enforceCors(_req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' + 'invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ message: 'Invalid origin' }); } diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts index 184e038697..b6a7393d4a 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts @@ -72,12 +72,12 @@ describe('EditingDataMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; - process.env.API_ALLOWED_ORIGINS = allowedOrigin; + process.env.JSS_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should handle PUT request', async () => { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts index 73511bc30a..9cce3a2fd4 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts @@ -3,7 +3,7 @@ import { EditingDataCache, editingDataDiskCache } from './editing-data-cache'; import { EditingData, isEditingData } from './editing-data'; import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; -import { enforceCors } from '../utils'; +import { enforceCors } from '@sitecore-jss/sitecore-jss/utils'; import { debug } from '@sitecore-jss/sitecore-jss'; export interface EditingDataMiddlewareConfig { @@ -55,7 +55,7 @@ export class EditingDataMiddleware { if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' + 'invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ message: 'Invalid origin' }); } diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts index 0835690e22..a6db9fb907 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts @@ -94,14 +94,14 @@ describe('EditingRenderMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; - process.env.API_ALLOWED_ORIGINS = allowedOrigin; + process.env.JSS_ALLOWED_ORIGINS = allowedOrigin; delete process.env.VERCEL; }); after(() => { delete process.env.JSS_EDITING_SECRET; delete process.env.VERCEL; - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should handle request', async () => { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index aa003fd549..634bd14f16 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -8,7 +8,7 @@ import { EditingDataService, editingDataService } from './editing-data-service'; import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; import { RenderMiddlewareBase } from './render-middleware'; -import { enforceCors } from '../utils'; +import { enforceCors } from '@sitecore-jss/sitecore-jss/utils'; export interface EditingRenderMiddlewareConfig { /** @@ -90,7 +90,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase { if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' + 'invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable' ); return res.status(401).json({ html: `Requests from origin ${req.headers?.origin} not allowed`, diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts index cb47629038..0140b956e1 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts @@ -59,12 +59,12 @@ describe('FEAASRenderMiddleware', () => { beforeEach(() => { process.env.JSS_EDITING_SECRET = secret; - process.env.API_ALLOWED_ORIGINS = allowedOrigin; + process.env.JSS_ALLOWED_ORIGINS = allowedOrigin; }); after(() => { delete process.env.JSS_EDITING_SECRET; - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should handle request', async () => { diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts index 4bb4b1a17d..3e7772ba50 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -3,7 +3,7 @@ import { debug } from '@sitecore-jss/sitecore-jss'; import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; import { RenderMiddlewareBase } from './render-middleware'; -import { enforceCors } from '../utils'; +import { enforceCors } from '@sitecore-jss/sitecore-jss/utils'; /** * Configuration for `FEAASRenderMiddleware`. @@ -55,7 +55,7 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { if (!enforceCors(req, res, EDITING_ALLOWED_ORIGINS)) { debug.editing( - 'invalid origin host - set allowed origins in API_ALLOWED_ORIGINS environment variable' + 'invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable' ); return res .status(401) diff --git a/packages/sitecore-jss-nextjs/src/utils/index.ts b/packages/sitecore-jss-nextjs/src/utils/index.ts index 36ca991df8..bb06217af3 100644 --- a/packages/sitecore-jss-nextjs/src/utils/index.ts +++ b/packages/sitecore-jss-nextjs/src/utils/index.ts @@ -4,5 +4,4 @@ export { isEditorActive, resetEditorChromes, resolveUrl, - enforceCors, } from '@sitecore-jss/sitecore-jss/utils'; diff --git a/packages/sitecore-jss/src/utils/utils.test.ts b/packages/sitecore-jss/src/utils/utils.test.ts index 604da9f9e2..b4810b256a 100644 --- a/packages/sitecore-jss/src/utils/utils.test.ts +++ b/packages/sitecore-jss/src/utils/utils.test.ts @@ -202,12 +202,12 @@ describe('utils', () => { return res; }; - it('should return true if origin is found in allowedOrigins from API_ALLOWED_ORIGINS env variable', () => { + it('should return true if origin is found in allowedOrigins from JSS_ALLOWED_ORIGINS env variable', () => { const req = mockRequest(); const res = mockResponse(); - process.env.API_ALLOWED_ORIGINS = mockOrigin; + process.env.JSS_ALLOWED_ORIGINS = mockOrigin; expect(enforceCors(req, res)).to.be.equal(true); - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should return true if origin is found in allowedOrigins passed as argument', () => { @@ -217,12 +217,12 @@ describe('utils', () => { expect(enforceCors(req, res, ['http://allowed.com'])).to.be.equal(true); }); - it('should return false if origin matches neither allowedOrigins from API_ALLOWED_ORIGINS env variable nor argument', () => { + it('should return false if origin matches neither allowedOrigins from JSS_ALLOWED_ORIGINS env variable nor argument', () => { const req = mockRequest('https://notallowed.com'); const res = mockResponse(); - process.env.API_ALLOWED_ORIGINS = 'https://strictallowed.com, https://alsoallowed.com'; + process.env.JSS_ALLOWED_ORIGINS = 'https://strictallowed.com, https://alsoallowed.com'; expect(enforceCors(req, res, ['https://paramallowed.com'])).to.be.equal(false); - delete process.env.API_ALLOWED_ORIGINS; + delete process.env.JSS_ALLOWED_ORIGINS; }); it('should return true when origin matches a wildcard value from allowedOrigins', () => { diff --git a/packages/sitecore-jss/src/utils/utils.ts b/packages/sitecore-jss/src/utils/utils.ts index d28d00c7c5..2944f4dda5 100644 --- a/packages/sitecore-jss/src/utils/utils.ts +++ b/packages/sitecore-jss/src/utils/utils.ts @@ -88,7 +88,7 @@ const convertToWildcardRegex = (pattern: string) => { /** * Tests origin from incoming request against allowed origins list that can be - * set in API_ALLOWED_ORIGINS and/or passed via allowedOrigins param. + * set in JSS's JSS_ALLOWED_ORIGINS env variable and/or passed via allowedOrigins param. * Applies Access-Control-Allow-Origin and Access-Control-Allow-Methods on match * @param {IncomingMessage} req incoming request * @param {OutgoingMessage} res response to set CORS headers for @@ -100,8 +100,8 @@ export const enforceCors = ( res: OutgoingMessage, allowedOrigins?: string[] ): boolean => { - const defaultAllowedOrigins = process.env.API_ALLOWED_ORIGINS - ? process.env.API_ALLOWED_ORIGINS.replace(' ', '').split(',') + const defaultAllowedOrigins = process.env.JSS_ALLOWED_ORIGINS + ? process.env.JSS_ALLOWED_ORIGINS.replace(' ', '').split(',') : []; allowedOrigins = defaultAllowedOrigins.concat(allowedOrigins || []); const origin = req.headers.origin; From 2a857e9024901ab85d2a4051d99824c20174a21e Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 20 May 2024 09:59:01 -0400 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cea1443ea..20f811a3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Our versioning strategy is as follows: * `[sitecore-jss-react]`Introduce ErrorBoundary component. All rendered components are wrapped with it and it will catch client or server side errors from any of its children, display appropriate message and prevent the rest of the application from failing. It accepts and can display custom error component and loading message if it is passed as a prop to parent Placeholder. ([#1786](https://github.com/Sitecore/jss/pull/1786) [#1790](https://github.com/Sitecore/jss/pull/1790) [#1793](https://github.com/Sitecore/jss/pull/1793) [#1794](https://github.com/Sitecore/jss/pull/1794)) +* `[sitecore-jss-nextjs]` Enforce CORS policy that matches Sitecore Pages domains for editing middleware API endpoints ([#1798](https://github.com/Sitecore/jss/pull/1798)) + ## 22.0.0 ### 🛠 Breaking Changes From b9afd9743dd2493c2848016fec126683376e1bde Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 20 May 2024 11:02:35 -0400 Subject: [PATCH 6/6] escape all dots and slashes in when converting pattern to regex --- packages/sitecore-jss/src/utils/utils.test.ts | 4 ++-- packages/sitecore-jss/src/utils/utils.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss/src/utils/utils.test.ts b/packages/sitecore-jss/src/utils/utils.test.ts index b4810b256a..00669e380a 100644 --- a/packages/sitecore-jss/src/utils/utils.test.ts +++ b/packages/sitecore-jss/src/utils/utils.test.ts @@ -226,9 +226,9 @@ describe('utils', () => { }); it('should return true when origin matches a wildcard value from allowedOrigins', () => { - const req = mockRequest('https://veryallowed.com'); + const req = mockRequest('https://allowed.dev.com'); const res = mockResponse(); - expect(enforceCors(req, res, ['https://*owed.com'])).to.be.equal(true); + expect(enforceCors(req, res, ['https://allowed.*.com'])).to.be.equal(true); }); it('should set Access-Control-Allow-Origin and Access-Control-Allow-Methods headers for matching origin', () => { diff --git a/packages/sitecore-jss/src/utils/utils.ts b/packages/sitecore-jss/src/utils/utils.ts index 2944f4dda5..e82989f2c8 100644 --- a/packages/sitecore-jss/src/utils/utils.ts +++ b/packages/sitecore-jss/src/utils/utils.ts @@ -83,7 +83,14 @@ export const isTimeoutError = (error: unknown) => { * @returns {string} modified string that can be used as regexp input */ const convertToWildcardRegex = (pattern: string) => { - return '^' + pattern.replace('.', '\\.').replace(/\*/g, '.*') + '$'; + return ( + '^' + + pattern + .replace(/\//g, '\\/') + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + + '$' + ); }; /**