Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
guydumais committed Sep 10, 2021
0 parents commit 97f3b46
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
*.log
.next
.history
package-lock.json
yarn.lock
notes.md
.DS_Store
src
tsconfig.json
.babelrc
183 changes: 183 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
![Alt](/public/guy-dumais-logo.svg "Guy Dumais")

# Next.js Strict Content Security Policy
Hash-based Strict Content Security Policy generator for Next.js to protect a single-page app (SPA) against XSS and CSP bypass.

## Why
Based on a study from Google:
1. 95% of real-world CSP deployments are bypassed.
2. 99.34% of hosts with CSP use policies that offer no benefit against XSS.

By using this package in your Next.js website you'll protect your single-page app (SPA) built with Next.js against XSS and CSP bypass.

## How to install
With NPM:
```
npm install next-strict-csp
```

With YARN:
```
yarn add next-strict-csp
```

## Basic usage
Integrate the CSP generator in your `_document.tsx` this way:

**_document.tsx**

```
...
// Next.js libraries
import Document, { Html, Head, Main, NextScript } from 'next/document'
// Next Strict Content Security Policy
import { NextStrictCSP } from 'next-strict-csp'
...
// Enable Head Strict CSP in production mode only
const HeadCSP = process.env.NODE_ENV === 'production' ? NextStrictCSP : Head
...
// Document component
class MyDoc extends Document {
render() {
return (
<Html>
<HeadCSP>
{ process.env.NODE_ENV === 'production' &&
<meta httpEquiv="Content-Security-Policy" />
}
...
</HeadCSP>
<body>
...
<Main />
<NextScript />
...
</body>
</Html>
)
}
}
```

## Advanced usage with inline scripts
You can also integrate any additionnal inline scripts and they'll get hashed automatically. Here's an example to add Google Tag Manager and Cloudflare Analytics inline scripts:

**_document.tsx**

```
...
// Next.js libraries
import Document, { Html, Head, Main, NextScript } from 'next/document'
// Next Strict Content Security Policy
import { NextStrictCSP } from 'next-strict-csp'
...
// Cloudflare Insights Script (Optional)
const cloudflareJs = `var s = document.createElement('script')
s.src = 'https://static.cloudflareinsights.com/beacon.min.js'
s.setAttribute('data-cf-beacon', '{"token": "YOUR CLOUDFLARE WEB ANALYTICS TOKEN STRING"}')
document.body.appendChild(s)`
// Google Tag Manager Script (Optional)
const GTMJs = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','YOUR GOOGLE TAG MANAGER ID STRING');`
// Next Strict CSP
// Inline scripts to hash (Optional)
NextStrictCSP.inlineJs = [
cloudflareJs,
GTMJs
]
...
// Enable Head Strict CSP in production mode only
const HeadCSP = process.env.NODE_ENV === 'production' ? NextStrictCSP : Head
...
// Document component
class MyDoc extends Document {
render() {
return (
<Html>
<HeadCSP>
{ process.env.NODE_ENV === 'production' &&
<meta httpEquiv="Content-Security-Policy" />
}
...
{/* Google Tag Manager */}
{ process.env.NODE_ENV === 'production' &&
<script
dangerouslySetInnerHTML={{
__html: GTMJs
}}
/>
}
{/* End Google Tag Manager */}
</HeadCSP>
<body>
{ process.env.NODE_ENV === 'production' &&
<noscript
dangerouslySetInnerHTML={{
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=YOUR GOOGLE TAG MANAGER ID STRING" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
}}
/>
}
...
<Main />
<NextScript />
{/* Cloudflare Web Analytics */}
{/*<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon={`{"token": "YOUR CLOUDFLARE WEB ANALYTICS TOKEN STRING"}`}></script>*/}
{process.env.NODE_ENV === 'production' &&
<script dangerouslySetInnerHTML={{
__html: cloudflareJs
}} />
}
{/* End Cloudflare Web Analytics */}
...
</body>
</Html>
)
}
}
```

## Demo
Live demo with Next.js Strict CSP for testing with website security scanner available here:
[Guy Dumais](https://guydumais.digital/ "Guy Dumais Digital")

## Learn more
Learn more about website security and how to protect a Next.js website:
[Website Security](https://guydumais.digital/blog/tag/website-security/ "Website Security")
10 changes: 10 additions & 0 deletions dist/cjs/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Head } from '../../../next/dist/pages/_document';
export declare class NextStrictCSP extends Head {
static inlineJs: string[];
static inlineJsHashed: string[];
static nextJsFiles: string[];
getDynamicChunks(): never[];
getScripts: ({ allFiles }: {
allFiles: any;
}) => never[];
}
71 changes: 71 additions & 0 deletions dist/cjs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NextStrictCSP = void 0;
const react_1 = __importDefault(require("react"));
const _document_1 = require("../../../next/dist/pages/_document");
const crypto_1 = __importDefault(require("crypto"));
const cspHashOf = (text) => {
const hash = crypto_1.default.createHash('sha256');
hash.update(text);
return `'sha256-${hash.digest('base64')}'`;
};
class NextStrictCSP extends _document_1.Head {
static inlineJs = [];
static inlineJsHashed = [];
static nextJsFiles = [];
getDynamicChunks() {
const { dynamicImports } = this.context;
NextStrictCSP.nextJsFiles = dynamicImports
.filter((file) => /\.js$/.test(file))
.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
return [];
}
getScripts = ({ allFiles }) => {
const jsFiles = allFiles
.filter((file) => /\.js$/.test(file))
.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
const { buildManifest, __NEXT_DATA__ } = this.context;
const { lowPriorityFiles } = buildManifest;
const jsFiles2 = lowPriorityFiles.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
const jsFiles3 = jsFiles.concat(jsFiles2);
const nextJsFiles = NextStrictCSP.nextJsFiles.concat(jsFiles3);
const nextJsSPA = `var scripts = [${nextJsFiles.join()}]
scripts.forEach(function(scriptUrl) {
var s = document.createElement('script')
s.src = scriptUrl
s.async = false // to preserve execution order
s.defer = true
document.head.appendChild(s)
})`;
const nextJsSPAScript = react_1.default.createElement("script", { defer: true, dangerouslySetInnerHTML: {
__html: nextJsSPA
} });
NextStrictCSP.inlineJsHashed = NextStrictCSP.inlineJs.map((inlineJs) => {
return cspHashOf(inlineJs);
});
const newChildren = [];
react_1.default.Children.forEach(this.props.children, (child) => {
if (child.type === 'meta') {
if (child.props?.httpEquiv !== undefined) {
if (child.props.httpEquiv === 'Content-Security-Policy') {
child.props.content = `script-src 'strict-dynamic' ${cspHashOf(nextJsSPA)} ${NextStrictCSP.inlineJsHashed.join(' ')} 'unsafe-inline' http: https:;`;
child.props.slug = __NEXT_DATA__.page;
}
}
}
newChildren.push(child);
});
this.context.headTags.push(nextJsSPAScript);
return [];
};
}
exports.NextStrictCSP = NextStrictCSP;
10 changes: 10 additions & 0 deletions dist/esm/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Head } from '../../../next/dist/pages/_document';
export declare class NextStrictCSP extends Head {
static inlineJs: string[];
static inlineJsHashed: string[];
static nextJsFiles: string[];
getDynamicChunks(): never[];
getScripts: ({ allFiles }: {
allFiles: any;
}) => never[];
}
64 changes: 64 additions & 0 deletions dist/esm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { Head } from '../../../next/dist/pages/_document';
import crypto from 'crypto';
const cspHashOf = (text) => {
const hash = crypto.createHash('sha256');
hash.update(text);
return `'sha256-${hash.digest('base64')}'`;
};
export class NextStrictCSP extends Head {
static inlineJs = [];
static inlineJsHashed = [];
static nextJsFiles = [];
getDynamicChunks() {
const { dynamicImports } = this.context;
NextStrictCSP.nextJsFiles = dynamicImports
.filter((file) => /\.js$/.test(file))
.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
return [];
}
getScripts = ({ allFiles }) => {
const jsFiles = allFiles
.filter((file) => /\.js$/.test(file))
.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
const { buildManifest, __NEXT_DATA__ } = this.context;
const { lowPriorityFiles } = buildManifest;
const jsFiles2 = lowPriorityFiles.map((jsFile) => {
return `'/_next/${encodeURI(jsFile)}'`;
});
const jsFiles3 = jsFiles.concat(jsFiles2);
const nextJsFiles = NextStrictCSP.nextJsFiles.concat(jsFiles3);
const nextJsSPA = `var scripts = [${nextJsFiles.join()}]
scripts.forEach(function(scriptUrl) {
var s = document.createElement('script')
s.src = scriptUrl
s.async = false // to preserve execution order
s.defer = true
document.head.appendChild(s)
})`;
const nextJsSPAScript = React.createElement("script", { defer: true, dangerouslySetInnerHTML: {
__html: nextJsSPA
} });
NextStrictCSP.inlineJsHashed = NextStrictCSP.inlineJs.map((inlineJs) => {
return cspHashOf(inlineJs);
});
const newChildren = [];
React.Children.forEach(this.props.children, (child) => {
if (child.type === 'meta') {
if (child.props?.httpEquiv !== undefined) {
if (child.props.httpEquiv === 'Content-Security-Policy') {
child.props.content = `script-src 'strict-dynamic' ${cspHashOf(nextJsSPA)} ${NextStrictCSP.inlineJsHashed.join(' ')} 'unsafe-inline' http: https:;`;
child.props.slug = __NEXT_DATA__.page;
}
}
}
newChildren.push(child);
});
this.context.headTags.push(nextJsSPAScript);
return [];
};
}
Loading

0 comments on commit 97f3b46

Please sign in to comment.