From 463e5517cb8d1c171f846414eb24fdf58c19ecf4 Mon Sep 17 00:00:00 2001 From: Michelle Chen Date: Thu, 21 Dec 2023 15:40:42 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20add=20applyDefault=20option=20t?= =?UTF-8?q?o=20createContentSecurityPolicy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/heavy-coins-tickle.md | 18 ++++++++++++++++++ packages/hydrogen/src/csp/csp.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .changeset/heavy-coins-tickle.md diff --git a/.changeset/heavy-coins-tickle.md b/.changeset/heavy-coins-tickle.md new file mode 100644 index 0000000000..39815c252b --- /dev/null +++ b/.changeset/heavy-coins-tickle.md @@ -0,0 +1,18 @@ +--- +'@shopify/hydrogen': patch +--- + +✨ add applyDefault option to createContentSecurityPolicy which allow use to add policy in front of the existing rules instead of overriding them. The default value of applyDefault option is false which is the current behaviour. + +Example usage: + +```diff +const {nonce, header, NonceProvider} = createContentSecurityPolicy( + {connectSrc: 'wss://public-domain:*'}, ++ {applyDefault: true}, +); +``` + +Result of connect-src when `applyDefault=false` is "wss://public-domain:\*" + +Result of connect-src when `applyDefault=true` is "wss://public-domain:\* 'self' 'https://cdn.shopify.com' 'https://shopify.com'" diff --git a/packages/hydrogen/src/csp/csp.ts b/packages/hydrogen/src/csp/csp.ts index 61836fa449..c35f5b3c83 100644 --- a/packages/hydrogen/src/csp/csp.ts +++ b/packages/hydrogen/src/csp/csp.ts @@ -26,9 +26,10 @@ type ContentSecurityPolicy = { */ export function createContentSecurityPolicy( directives: Record = {}, + options: {applyDefault?: boolean} = {}, ): ContentSecurityPolicy { const nonce = generateNonce(); - const header = createCSPHeader(nonce, directives); + const header = createCSPHeader(nonce, directives, options?.applyDefault); const Provider = ({children}: {children: ReactNode}) => { return createElement(NonceProvider, {value: nonce}, children); @@ -44,6 +45,7 @@ export function createContentSecurityPolicy( function createCSPHeader( nonce: string, directives: Record = {}, + applyDefault = false, ): string { const nonceString = `'nonce-${nonce}'`; const styleSrc = ["'self'", "'unsafe-inline'", 'https://cdn.shopify.com']; @@ -78,6 +80,14 @@ function createCSPHeader( } const combinedDirectives = Object.assign({}, defaultDirectives, directives); + if (applyDefault) { + for (const key in defaultDirectives) { + combinedDirectives[key] = addCspDirective( + directives[key], + defaultDirectives[key], + ); + } + } // Make sure that at least script-src includes a nonce directive. // If someone doesn't want a nonce in their CSP, they probably @@ -98,3 +108,19 @@ function createCSPHeader( directives: combinedDirectives, }); } + +function addCspDirective( + currentValue: string[] | string | boolean, + value: string[] | string | boolean, +): boolean | string[] { + const normalizedValue = typeof value === 'string' ? [value] : value; + const normalizedCurrentValue = Array.isArray(currentValue) + ? currentValue + : [String(currentValue)]; + + const newValue = Array.isArray(normalizedValue) + ? [...normalizedCurrentValue, ...normalizedValue] + : normalizedValue; + + return newValue; +} From 3ba358529e0c26054b3d9688cc4be82f8f4a524a Mon Sep 17 00:00:00 2001 From: Michelle Chen Date: Wed, 3 Jan 2024 11:10:31 -0500 Subject: [PATCH 2/3] Update .changeset/heavy-coins-tickle.md Co-authored-by: Bret Little --- .changeset/heavy-coins-tickle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-coins-tickle.md b/.changeset/heavy-coins-tickle.md index 39815c252b..508963e44d 100644 --- a/.changeset/heavy-coins-tickle.md +++ b/.changeset/heavy-coins-tickle.md @@ -2,7 +2,7 @@ '@shopify/hydrogen': patch --- -✨ add applyDefault option to createContentSecurityPolicy which allow use to add policy in front of the existing rules instead of overriding them. The default value of applyDefault option is false which is the current behaviour. +✨ add `applyDefault` option to `createContentSecurityPolicy` which automatically adds Shopify domains to the content security policy, extending whatever rules are passed instead of overriding them. The default value of `applyDefault` option is false which is the current behavior. Example usage: From 3f88fbdafc8f0a6daf255ea7627fbe4ce2fd9faa Mon Sep 17 00:00:00 2001 From: Michelle Chen Date: Wed, 3 Jan 2024 11:23:16 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A5=20remove=20applyDefault=20opti?= =?UTF-8?q?on=20and=20make=20extension=20the=20default=20behaviour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/heavy-coins-tickle.md | 17 ++--------------- packages/hydrogen/src/csp/csp.test.ts | 16 ++++------------ packages/hydrogen/src/csp/csp.ts | 10 +++++----- 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/.changeset/heavy-coins-tickle.md b/.changeset/heavy-coins-tickle.md index 508963e44d..03b8eb3c96 100644 --- a/.changeset/heavy-coins-tickle.md +++ b/.changeset/heavy-coins-tickle.md @@ -1,18 +1,5 @@ --- -'@shopify/hydrogen': patch +'@shopify/hydrogen': minor --- -✨ add `applyDefault` option to `createContentSecurityPolicy` which automatically adds Shopify domains to the content security policy, extending whatever rules are passed instead of overriding them. The default value of `applyDefault` option is false which is the current behavior. - -Example usage: - -```diff -const {nonce, header, NonceProvider} = createContentSecurityPolicy( - {connectSrc: 'wss://public-domain:*'}, -+ {applyDefault: true}, -); -``` - -Result of connect-src when `applyDefault=false` is "wss://public-domain:\*" - -Result of connect-src when `applyDefault=true` is "wss://public-domain:\* 'self' 'https://cdn.shopify.com' 'https://shopify.com'" +💥 Change the behaviour of `createContentSecurityPolicy` where the custom rules passed in will extends the default Shopify and development domains instead of overriding them. diff --git a/packages/hydrogen/src/csp/csp.test.ts b/packages/hydrogen/src/csp/csp.test.ts index d36e1e75ba..9f2783e90c 100644 --- a/packages/hydrogen/src/csp/csp.test.ts +++ b/packages/hydrogen/src/csp/csp.test.ts @@ -25,28 +25,20 @@ describe('createContentSecurityPolicy', () => { it('adds custom directives', () => { expect( createContentSecurityPolicy({ - styleSrc: [ - "'self'", - 'https://cdn.shopify.com', - 'https://some-custom-css.cdn', - ], + styleSrc: ['https://some-custom-css.cdn'], }).header, ).toBe( - `base-uri 'self'; default-src 'self' 'nonce-somenonce' https://cdn.shopify.com https://shopify.com; frame-ancestors none; style-src 'self' https://cdn.shopify.com https://some-custom-css.cdn; connect-src 'self' https://monorail-edge.shopifysvc.com`, + `base-uri 'self'; default-src 'self' 'nonce-somenonce' https://cdn.shopify.com https://shopify.com; frame-ancestors none; style-src https://some-custom-css.cdn 'self' 'unsafe-inline' https://cdn.shopify.com; connect-src 'self' https://monorail-edge.shopifysvc.com`, ); }); it('adds nonce to custom directives', () => { expect( createContentSecurityPolicy({ - scriptSrc: [ - "'self'", - 'https://cdn.shopify.com', - 'https://some-custom-css.cdn', - ], + scriptSrc: ['https://some-custom-css.cdn'], }).header, ).toBe( - `base-uri 'self'; default-src 'self' 'nonce-somenonce' https://cdn.shopify.com https://shopify.com; frame-ancestors none; style-src 'self' 'unsafe-inline' https://cdn.shopify.com; connect-src 'self' https://monorail-edge.shopifysvc.com; script-src 'self' https://cdn.shopify.com https://some-custom-css.cdn 'nonce-somenonce'`, + `base-uri 'self'; default-src 'self' 'nonce-somenonce' https://cdn.shopify.com https://shopify.com; frame-ancestors none; style-src 'self' 'unsafe-inline' https://cdn.shopify.com; connect-src 'self' https://monorail-edge.shopifysvc.com; script-src https://some-custom-css.cdn 'nonce-somenonce'`, ); }); }); diff --git a/packages/hydrogen/src/csp/csp.ts b/packages/hydrogen/src/csp/csp.ts index c35f5b3c83..38a0cc6d8e 100644 --- a/packages/hydrogen/src/csp/csp.ts +++ b/packages/hydrogen/src/csp/csp.ts @@ -26,10 +26,9 @@ type ContentSecurityPolicy = { */ export function createContentSecurityPolicy( directives: Record = {}, - options: {applyDefault?: boolean} = {}, ): ContentSecurityPolicy { const nonce = generateNonce(); - const header = createCSPHeader(nonce, directives, options?.applyDefault); + const header = createCSPHeader(nonce, directives); const Provider = ({children}: {children: ReactNode}) => { return createElement(NonceProvider, {value: nonce}, children); @@ -45,7 +44,6 @@ export function createContentSecurityPolicy( function createCSPHeader( nonce: string, directives: Record = {}, - applyDefault = false, ): string { const nonceString = `'nonce-${nonce}'`; const styleSrc = ["'self'", "'unsafe-inline'", 'https://cdn.shopify.com']; @@ -80,8 +78,10 @@ function createCSPHeader( } const combinedDirectives = Object.assign({}, defaultDirectives, directives); - if (applyDefault) { - for (const key in defaultDirectives) { + + //add defaults if it was override + for (const key in defaultDirectives) { + if (directives[key]) { combinedDirectives[key] = addCspDirective( directives[key], defaultDirectives[key],