-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 97f3b46
Showing
10 changed files
with
461 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Auto detect text files and perform LF normalization | ||
* text=auto |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
 | ||
|
||
# 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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 []; | ||
}; | ||
} |
Oops, something went wrong.