Skip to content

Commit

Permalink
fix: added nonce type CSP to remove the need for whitelisting
Browse files Browse the repository at this point in the history
  • Loading branch information
meza committed Feb 28, 2023
1 parent a70f2cd commit 01cb7e8
Show file tree
Hide file tree
Showing 22 changed files with 170 additions and 182 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ storybook-static
server/index.js
server/index.js.map
public/build
preferences.arc
sam.json
sam.yaml
.env
Expand Down
5 changes: 5 additions & 0 deletions preferences.arc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@create
autocreate true

@sandbox
livereload false
7 changes: 6 additions & 1 deletion src/components/Cookieyes/Cookieyes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ We use Cookieyes to manage cookie consent. This is a third party service that we
You can read more about how they use your data in their [privacy policy](https://cookieyes.com/privacy-policy/).

<h2>Usage</h2>
<h3>Parameters</h3>

- `isProduction`: Is the app in production mode?
- `token`: The cookieyes token
- `nonce`: The nonce for the script tag

~~~jsx
import Cookieyes from '~/components/Cookieyes';

<Cookieyes isProduction={true} token={abc123}/>
<Cookieyes isProduction={true} token={'abc123'} nonce={'pinoihnrgoire'}/>
~~~
3 changes: 2 additions & 1 deletion src/components/Cookieyes/Cookieyes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ describe('Cookieyes', () => {
/>
`);
// eslint-disable-next-line new-cap
expect(Cookieyes({ isProduction: true, token: 'abc' })).toMatchInlineSnapshot(`
expect(Cookieyes({ isProduction: true, token: 'abc', nonce: 'a-nonce' })).toMatchInlineSnapshot(`
<script
async={true}
id="cookieyes"
nonce="a-nonce"
src="https://cdn-cookieyes.com/client_data/abc/script.js"
type="text/javascript"
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Cookieyes/Cookieyes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const Cookieyes = (props: { isProduction: boolean, token: string }) => {
export const Cookieyes = (props: { isProduction: boolean, token: string; nonce?: string; }) => {
if (props.isProduction) {
return <script async id="cookieyes" type="text/javascript" src={`https://cdn-cookieyes.com/client_data/${props.token}/script.js`}></script>;
return <script nonce={props.nonce} async id={'cookieyes'} type={'text/javascript'} src={`https://cdn-cookieyes.com/client_data/${props.token}/script.js`}/>;
}
return null;
};
3 changes: 2 additions & 1 deletion src/components/GoogleAnalytics/GoogleAnalytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ We use [GA4](https://support.google.com/analytics/answer/10089681?hl=en) to reco

- `googleAnalyticsId`: The ID of the GA4 measurement
- `visitorId`: The arbitrary ID of the visitor (userId, email, etc.)
- `nonce`: The nonce for the script tag

<h3>Example</h3>

~~~jsx
import { GoogleAnalytics } from '~/components/GoogleAnalytics';

<GoogleAnalytics googleAnalyticsId={'12345'}/>
<GoogleAnalytics googleAnalyticsId={'12345'} visitorId={'fvev34'} nonce={'rewpu8y9'}/>
~~~
12 changes: 9 additions & 3 deletions src/components/GoogleAnalytics/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GoogleAnalytics } from './index';

describe('Hotjar', () => {
describe('Google Analytics 4', () => {
beforeEach(() => {
vi.resetAllMocks();
});
Expand All @@ -10,11 +10,13 @@ describe('Hotjar', () => {
// eslint-disable-next-line new-cap
expect(GoogleAnalytics({
googleAnalyticsId: '123',
visitorId: 'abc'
visitorId: 'abc',
nonce: 'a-nonce'
})).toMatchInlineSnapshot(`
<React.Fragment>
<script
async={true}
nonce="a-nonce"
src="https://www.googletagmanager.com/gtag/js?id=123"
/>
<script
Expand All @@ -29,18 +31,21 @@ describe('Hotjar', () => {
}
}
id="google-analytics"
nonce="a-nonce"
/>
</React.Fragment>
`);

// eslint-disable-next-line new-cap
expect(GoogleAnalytics({
googleAnalyticsId: 'triangulation',
visitorId: 'abc123'
visitorId: 'abc123',
nonce: 'a-nonce2'
})).toMatchInlineSnapshot(`
<React.Fragment>
<script
async={true}
nonce="a-nonce2"
src="https://www.googletagmanager.com/gtag/js?id=triangulation"
/>
<script
Expand All @@ -55,6 +60,7 @@ describe('Hotjar', () => {
}
}
id="google-analytics"
nonce="a-nonce2"
/>
</React.Fragment>
`);
Expand Down
10 changes: 8 additions & 2 deletions src/components/GoogleAnalytics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
interface GoogleAnalyticsProps {
googleAnalyticsId: string;
visitorId: string;
nonce?: string;
}

export const GoogleAnalytics = (props: GoogleAnalyticsProps) => {
const inputProps: {nonce?: string} = {};
if (props.nonce) {
inputProps.nonce = props.nonce;
}
return (
<>
<script async src={`https://www.googletagmanager.com/gtag/js?id=${props.googleAnalyticsId}`}/>
<script async nonce={props.nonce} src={`https://www.googletagmanager.com/gtag/js?id=${props.googleAnalyticsId}`}></script>
<script
{...inputProps}
id={'google-analytics'}
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];
Expand All @@ -17,7 +23,7 @@ export const GoogleAnalytics = (props: GoogleAnalyticsProps) => {
'user_id': '${props.visitorId}'
});`
}}
/>
></script>
</>
);
};
3 changes: 2 additions & 1 deletion src/components/Hotjar/Hotjar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ Hotjar is a tool that we use to better understand our users’ needs and experie

- `hotjarId`: The ID of the Hotjar project
- `visitorId`: The arbitrary ID of the visitor (userId, email, etc.)
- `nonce`: The nonce generated by the server


<h3>Example</h3>

~~~jsx
import { Hotjar } from '~/components/Hotjar';

<Hotjar hotjarId={'12345'} visitorId={'abcdef'}/>
<Hotjar hotjarId={'12345'} visitorId={'abcdef'} nonce={'oihfoihrefg'}/>
~~~
76 changes: 44 additions & 32 deletions src/components/Hotjar/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,63 @@ describe('Hotjar', () => {
// eslint-disable-next-line new-cap
expect(Hotjar({
hotjarId: '123',
visitorId: 'abc'
visitorId: 'abc',
nonce: 'a-nonce'
})).toMatchInlineSnapshot(`
<script
async={true}
dangerouslySetInnerHTML={
{
"__html": "(function(h,o,t,j,a,r){
<React.Fragment>
<script
async={true}
dangerouslySetInnerHTML={
{
"__html": "(function(h){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:'123',hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
h._hjSettings={hjid:'123',hjsv:6,hjdebug:false};
})(window);
hj('identify', 'abc');
",
}
}
}
id="hotjar-tracker"
/>
id="hotjar-init"
nonce="a-nonce"
/>
<script
async={true}
id="hotjar-script"
nonce="a-nonce"
src="https://static.hotjar.com/c/hotjar-123.js?sv=6"
/>
</React.Fragment>
`);

// eslint-disable-next-line new-cap
expect(Hotjar({
hotjarId: '567',
visitorId: 'xyz'
hotjarId: '324123',
visitorId: 'ewfwfec',
nonce: 'a-nonce2'
})).toMatchInlineSnapshot(`
<script
async={true}
dangerouslySetInnerHTML={
{
"__html": "(function(h,o,t,j,a,r){
<React.Fragment>
<script
async={true}
dangerouslySetInnerHTML={
{
"__html": "(function(h){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:'567',hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
hj('identify', 'xyz');
h._hjSettings={hjid:'324123',hjsv:6,hjdebug:false};
})(window);
hj('identify', 'ewfwfec');
",
}
}
}
id="hotjar-tracker"
/>
id="hotjar-init"
nonce="a-nonce2"
/>
<script
async={true}
id="hotjar-script"
nonce="a-nonce2"
src="https://static.hotjar.com/c/hotjar-324123.js?sv=6"
/>
</React.Fragment>
`);
});
});
40 changes: 27 additions & 13 deletions src/components/Hotjar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
interface HotjarProps {
hotjarId: string;
visitorId: string;
nonce?: string;
}

export const Hotjar = (props: HotjarProps) => {
const { hotjarId, visitorId } = props;

const inputProps: {nonce?: string} = {};
if (props.nonce) {
inputProps.nonce = props.nonce;
}
const hotjarVersion = 6;
const debug = process.env.NODE_ENV === 'development';

return (
<script
async
id={'hotjar-tracker'}
dangerouslySetInnerHTML={{
__html: `(function(h,o,t,j,a,r){
<>
<script
{...inputProps}
async
id={'hotjar-init'}
dangerouslySetInnerHTML={{
__html: `(function(h){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:'${hotjarId}',hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
h._hjSettings={hjid:'${hotjarId}',hjsv:${hotjarVersion},hjdebug:${debug}};
})(window);
hj('identify', '${visitorId}');
`
}}
/>
}}
/>
<script
{...inputProps}
async
id={'hotjar-script'}
src={`https://static.hotjar.com/c/hotjar-${hotjarId}.js?sv=${hotjarVersion}`}
/>
</>
);
};
4 changes: 4 additions & 0 deletions src/components/NonceContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';

// The default value (`undefined`) will be used on the client
export const NonceContext = React.createContext<string | undefined>(undefined);
7 changes: 5 additions & 2 deletions src/entry.server.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import crypto from 'node:crypto';
import { Response } from '@remix-run/node';
import { cleanup } from '@testing-library/react';
import { renderToString } from 'react-dom/server';
Expand All @@ -11,13 +12,15 @@ vi.mock('~/session.server');
vi.mock('~/utils/securityHeaders');
vi.mock('@remix-run/react');
vi.mock('react-dom/server');
vi.mock('node:crypto');

describe('entry.server', () => {
const originalEnv = structuredClone(process.env);
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(initServerI18n).mockResolvedValue('mocked initServerI18n' as never);
vi.mocked(renderToString).mockReturnValue('mocked renderToString markup');
vi.mocked(crypto.randomBytes).mockReturnValue('mocked nonce' as never);
});

afterEach(() => {
Expand Down Expand Up @@ -64,7 +67,7 @@ describe('entry.server', () => {
await entry(request, responseCode, responseHeaders, context);

expect(initServerI18n).toHaveBeenCalledWith('en', context);
expect(addSecurityHeaders).toHaveBeenCalledWith(responseHeaders, true);
expect(addSecurityHeaders).toHaveBeenCalledWith(responseHeaders, 'mocked nonce');
expect(sanitizeHeaders).toHaveBeenCalledWith(responseHeaders);

});
Expand All @@ -85,7 +88,7 @@ describe('entry.server', () => {
} as never;

const actualResponse = await entry(request, responseCode, responseHeaders, context);
expect(addSecurityHeaders).toHaveBeenCalledWith(responseHeaders, false);
expect(addSecurityHeaders).toHaveBeenCalledWith(responseHeaders, 'mocked nonce');
expect(actualResponse.status).toBe(200);
expect(await actualResponse.headers).toMatchInlineSnapshot(`
Headers {
Expand Down
Loading

0 comments on commit 01cb7e8

Please sign in to comment.