Add Content Security Policy headers (#805)
* Basic CSP with unsafe-inline, unsafe-eval * Allow 'self' for img-src and connect-src Apparently, there is a bug for Chrome on iOS if connect-src does not allow 'self'. See known issues at https://caniuse.com/contentsecuritypolicy * Use nonces for strict CSP * More CSP comments * Add frame-ancestors directive * Add more useful headers * Add HSTS header * Allow youtube and twitter embeds For some reason, www.youtube.com is enough. It also works for youtube.com and youtube-nocookie.com. For twitter embeds from twitter.com or x.com, platform.twitter.com is enough. * Allow CDN and media domain in CSP * Only allow unsafe-eval in dev build * Ignore _next/webpack-hmr in middleware
This commit is contained in:
parent
a4e84e7a2e
commit
fc18a917e3
|
@ -1,8 +1,8 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export function middleware (request) {
|
||||
const regex = /(\/.*)?\/r\/([\w_]+)/
|
||||
const m = regex.exec(request.nextUrl.pathname)
|
||||
const referrerRegex = /(\/.*)?\/r\/([\w_]+)/
|
||||
function referrerMiddleware (request) {
|
||||
const m = referrerRegex.exec(request.nextUrl.pathname)
|
||||
|
||||
const url = new URL(m[1] || '/', request.url)
|
||||
url.search = request.nextUrl.search
|
||||
|
@ -13,6 +13,64 @@ export function middleware (request) {
|
|||
return resp
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/(.*/|)r/([\\w_]+)([?#]?.*)']
|
||||
export function middleware (request) {
|
||||
let resp = NextResponse.next()
|
||||
if (referrerRegex.test(request.nextUrl.pathname)) {
|
||||
resp = referrerMiddleware(request)
|
||||
}
|
||||
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
|
||||
const cspHeader = [
|
||||
// if something is not explicitly allowed, we don't allow it.
|
||||
"default-src 'none'",
|
||||
"font-src 'self' a.stacker.news",
|
||||
// we want to load images from everywhere but we can limit to HTTPS at least
|
||||
"img-src 'self' a.stacker.news m.stacker.news https: data:",
|
||||
// Using nonces and strict-dynamic deploys a strict CSP.
|
||||
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
|
||||
// Old browsers will ignore nonce and strict-dynamic
|
||||
// and fallback to host matching, unsafe-inline and unsafe-eval (no protection against XSS)
|
||||
process.env.NODE_ENV === 'production'
|
||||
? `script-src 'self' 'unsafe-inline' 'nonce-${nonce}' 'strict-dynamic' https:`
|
||||
// unsafe-eval is required during development due to react-refresh.js
|
||||
// see https://github.com/vercel/next.js/issues/14221
|
||||
: `script-src 'self' 'unsafe-inline' 'unsafe-eval' 'nonce-${nonce}' 'strict-dynamic' https:`,
|
||||
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
||||
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
||||
"manifest-src 'self'",
|
||||
'frame-src www.youtube.com platform.twitter.com',
|
||||
"connect-src 'self' https: wss:",
|
||||
// disable dangerous plugins like Flash
|
||||
"object-src 'none'",
|
||||
// blocks injection of <base> tags
|
||||
"base-uri 'none'",
|
||||
// tell user agents to replace HTTP with HTTPS
|
||||
'upgrade-insecure-requests',
|
||||
// prevents any domain from framing the content (defense against clickjacking attacks)
|
||||
"frame-ancestors 'none'"
|
||||
].join('; ')
|
||||
|
||||
resp.headers.set('Content-Security-Policy', cspHeader)
|
||||
// for browsers that don't support CSP
|
||||
resp.headers.set('X-Frame-Options', 'DENY')
|
||||
// more useful headers
|
||||
resp.headers.set('X-Content-Type-Options', 'nosniff')
|
||||
resp.headers.set('Referrer-Policy', 'origin-when-cross-origin')
|
||||
resp.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// NextJS recommends to not add the CSP header to prefetches and static assets
|
||||
// See https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
|
||||
{
|
||||
source: '/((?!api|_next/static|_next/image|_next/webpack-hmr|favicon.ico).*)',
|
||||
missing: [
|
||||
{ type: 'header', key: 'next-router-prefetch' },
|
||||
{ type: 'header', key: 'purpose', value: 'prefetch' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,15 +1,40 @@
|
|||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import Script from 'next/script'
|
||||
|
||||
export default function Document () {
|
||||
class MyDocument extends Document {
|
||||
// https://nextjs.org/docs/pages/building-your-application/routing/custom-document#customizing-renderpage
|
||||
static async getInitialProps (ctx) {
|
||||
const originalRenderPage = ctx.renderPage
|
||||
|
||||
// Run the React rendering logic synchronously
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
// Useful for wrapping the whole react tree
|
||||
enhanceApp: (App) => App,
|
||||
// Useful for wrapping in a per-page basis
|
||||
enhanceComponent: (Component) => Component
|
||||
})
|
||||
|
||||
// response is not available on offline page for example
|
||||
const csp = ctx.res?.getHeaders()['content-security-policy']
|
||||
const nonce = csp ? /nonce-([a-zA-Z0-9]{48})/.exec(csp)?.[1] : undefined
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
|
||||
return { ...initialProps, nonce }
|
||||
}
|
||||
|
||||
render () {
|
||||
const { nonce } = this.props
|
||||
return (
|
||||
<Html lang='en'>
|
||||
<Head>
|
||||
<Head nonce={nonce}>
|
||||
<link rel='manifest' href='/api/site.webmanifest' />
|
||||
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff2`} as='font' type='font/woff2' crossOrigin='' />
|
||||
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff`} as='font' type='font/woff' crossOrigin='' />
|
||||
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.ttf`} as='font' type='font/ttf' crossOrigin='' />
|
||||
<style
|
||||
nonce={nonce}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
` @font-face {
|
||||
|
@ -132,8 +157,11 @@ export default function Document () {
|
|||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
<NextScript nonce={nonce} />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
|
|
Loading…
Reference in New Issue