feat: support csp (#9111)

Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
NFish
2024-10-11 16:14:56 +08:00
committed by GitHub
parent 7c6ae96a09
commit f4ce08211d
10 changed files with 148 additions and 62 deletions

View File

@@ -22,3 +22,6 @@ NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false
# The timeout for the text generation in millisecond
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
NEXT_PUBLIC_CSP_WHITELIST=

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react'
import React from 'react'
import Script from 'next/script'
import { headers } from 'next/headers'
import { IS_CE_EDITION } from '@/config'
export enum GaType {
@@ -23,9 +24,16 @@ const GA: FC<IGAProps> = ({
if (IS_CE_EDITION)
return null
const nonce = process.env.NODE_ENV === 'production' ? headers().get('x-nonce') : ''
return (
<>
<Script strategy="beforeInteractive" async src={`https://www.googletagmanager.com/gtag/js?id=${gaIdMaps[gaType]}`}></Script>
<Script
strategy="beforeInteractive"
async
src={`https://www.googletagmanager.com/gtag/js?id=${gaIdMaps[gaType]}`}
nonce={nonce!}
></Script>
<Script
id="ga-init"
dangerouslySetInnerHTML={{
@@ -36,6 +44,7 @@ gtag('js', new Date());
gtag('config', '${gaIdMaps[gaType]}');
`,
}}
nonce={nonce!}
>
</Script>
</>

View File

@@ -1,16 +0,0 @@
'use client'
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar'
const Topbar = () => {
return (
<>
<ProgressBar
height='2px'
color="#1C64F2FF"
options={{ showSpinner: false }}
shallowRouting />
</>)
}
export default Topbar

View File

@@ -2,7 +2,6 @@ import type { Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor'
import Topbar from './components/base/topbar'
import { getLocaleOnServer } from '@/i18n/server'
import './styles/globals.css'
import './styles/markdown.scss'
@@ -45,7 +44,6 @@ const LocaleLayout = ({
data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT}
data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS}
>
<Topbar />
<BrowserInitor>
<SentryInitor>
<I18nServer>{children}</I18nServer>

View File

@@ -22,5 +22,6 @@ export NEXT_PUBLIC_SITE_ABOUT=${SITE_ABOUT}
export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
pm2 start ./pm2.json --no-daemon

76
web/middleware.ts Normal file
View File

@@ -0,0 +1,76 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com https://googletagmanager.com https://api.github.com'
export function middleware(request: NextRequest) {
const isWhiteListEnabled = !!process.env.NEXT_PUBLIC_CSP_WHITELIST && process.env.NODE_ENV === 'production'
if (!isWhiteListEnabled)
return NextResponse.next()
const whiteList = `${process.env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}`
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `'nonce-${nonce}'`
const scheme_source = 'data: mediastream: blob: filesystem:'
const cspHeader = `
default-src 'self' ${scheme_source} ${csp} ${whiteList};
connect-src 'self' ${scheme_source} ${csp} ${whiteList};
script-src 'self' ${scheme_source} ${csp} ${whiteList};
style-src 'self' 'unsafe-inline' ${scheme_source} ${whiteList};
worker-src 'self' ${scheme_source} ${csp} ${whiteList};
media-src 'self' ${scheme_source} ${csp} ${whiteList};
img-src 'self' ${scheme_source} ${csp} ${whiteList};
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
`
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue,
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue,
)
return response
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
// source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
source: '/((?!_next/static|_next/image|favicon.ico).*)',
// source: '/(.*)',
// missing: [
// { type: 'header', key: 'next-router-prefetch' },
// { type: 'header', key: 'purpose', value: 'prefetch' },
// ],
},
],
}

View File

@@ -62,7 +62,6 @@
"mermaid": "10.4.0",
"negotiator": "^0.6.3",
"next": "^14.1.1",
"next-nprogress-bar": "^2.3.8",
"pinyin-pro": "^3.23.0",
"qrcode.react": "^3.1.0",
"qs": "^6.11.1",

View File

@@ -7278,13 +7278,6 @@ negotiator@^0.6.3:
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
next-nprogress-bar@^2.3.8:
version "2.3.11"
resolved "https://registry.npmjs.org/next-nprogress-bar/-/next-nprogress-bar-2.3.11.tgz"
integrity sha512-OjSvsQwgSWa2qBMYO478QreGG9Jt82tr4wTQptmiyzNqqjzHCyKZNkhANnzPrjuFAoelIvmruJuakODofSnvTQ==
dependencies:
nprogress "^0.2.0"
next@^14.1.1:
version "14.2.4"
resolved "https://registry.npmjs.org/next/-/next-14.2.4.tgz"
@@ -7367,11 +7360,6 @@ npm-run-path@^5.1.0:
dependencies:
path-key "^4.0.0"
nprogress@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz"
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz"
@@ -8816,7 +8804,14 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9653,8 +9648,7 @@ word-wrap@^1.2.3:
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9672,6 +9666,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"