nibtime / next-safe-middleware

Strict CSP (Content-Security-Policy) for Next.js hybrid apps https://web.dev/strict-csp/
https://next-safe-middleware.vercel.app
MIT License
78 stars 20 forks source link

TS + MUI + Emotion + Next-safe-middleware #50

Closed oauramos closed 2 years ago

oauramos commented 2 years ago

Hi @nibtime , how are you doing?

Im trying to apply next-safe-middleware in Nextjs + Emotion + MUI application, why? Because using emotionCache and use default CSP provided by Vercel not work.

Here my _document.tsx

import createEmotionServer from '@emotion/server/create-instance';
import {
  getCspInitialProps,
  provideComponents,
} from '@next-safe/middleware/dist/document';
import Document, { Html, Main } from 'next/document';
import * as React from 'react';

import createEmotionCache from '../shared-ui-components/src/utility/createEmotionCache';

interface IProps {
  nonce: string;
  trustifyStyles: boolean;
  trustifyScripts: boolean;
  html: string;
  head?: JSX.Element[];
  styles?: JSX.Element | React.ReactElement<any, string | React.JSXElementConstructor<any>>[] | React.ReactFragment;
}

export default class MyDocument extends Document<IProps> {

  render() {
    const { Head, NextScript } = provideComponents(this.props) as any;
    return (
      <Html>
        <Head>
          <link
            rel="stylesheet"
            href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@300;600&family=Sora:wght@700;800&display=swap"
          />
          <meta property="og:image" content="Image URL" />
          <script type="application/ld+json"></script>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

MyDocument.getInitialProps = async (ctx) => {
  const originalRenderPage = ctx.renderPage;
  const cache = createEmotionCache();
  const { extractCriticalToChunks } = createEmotionServer(cache);

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App: any) =>
        function EnhanceApp(props) {
          return <App emotionCache={cache} {...props} />;
        },
    });

  const initialProps = await getCspInitialProps({ ctx });
  const emotionStyles = extractCriticalToChunks(initialProps.html);
  const emotionStyleTags = emotionStyles.styles.map((style, index) => (
    <style
      data-emotion={`${style.key} ${style.ids.join(' ')}`}
      key={index}
      dangerouslySetInnerHTML={{ __html: style.css }}
    />
  ));

  return {
    ...initialProps,
    styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
  };
};

Also, follow my _middleware.tsx using chain() (of course)

import {  chain, chainMatch,csp, isPageRequest,nextSafe } from '@next-safe/middleware';
import { NextRequest, NextResponse } from 'next/server';

const PUBLIC_FILE = /\.([a-zA-Z0-9]+$)/;

function routesMiddleware(request: NextRequest) {
  const cleanPathName = request.nextUrl.pathname.startsWith('/default')
    ? request.nextUrl.pathname.replace('/default', '/').replace('//', '/')
    : request.nextUrl.pathname;

  const shouldHandleLocale =
    !PUBLIC_FILE.test(cleanPathName) &&
    !cleanPathName.includes('/api/') &&
    request.nextUrl.locale === 'default';

  return shouldHandleLocale
    ? NextResponse.redirect(new URL(`/en${cleanPathName}`, request.url))
    : undefined;
}

const securityMiddleware = [
  nextSafe({ disableCsp: true }),
  csp({
    // your CSP base configuration with IntelliSense 
    // single quotes for values like 'self' are automatic
    directives: {
      'default-src': ['self'],
      'script-src': ['self'],
      'style-src': ['self', 'https://fonts.googleapis.com', 'unsafe-inline'],
      'font-src': ['self', 'https://fonts.gstatic.com', 'data:', ],
      'img-src': ['self', 'data:', 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com', 'https://*.g.doubleclick.net', 'https://*.google.com'],
      'media-src': ['self', 'data:', 'http://localhost:3000' ],
      'connect-src': ['self', 'https://*.google-analytics.com' , 'https://*.analytics.google.com', 'https://*.googletagmanager.com', 'https://*.g.doubleclick.net', 'https://*.google.com']
    },
  }),
];

export default chain(
  routesMiddleware,
  chainMatch(isPageRequest)(...securityMiddleware)
)

My huge question is, I already add config into dynamic staticProps and staticPaths

export const config = {
  unstable_includeFiles: ['.next/static/chunks/**/*.js', '.next/static/chunks/**/*.ts'],
};

But still suffering from missing hash/nonce alerts in App :/ image

NextJS: 2.1.5 "@emotion/cache":"^11.7.1", "@emotion/react": "^11.8.1", "@emotion/server": "^11.4.0", "@emotion/styled": "^11.8.1",

oauramos commented 2 years ago

Everything is working, except missing hash! :cry:

nibtime commented 2 years ago

Hi @middlebaws

You just forgot to add the strictDynamic and strictInlineStyles middlewares to securityMiddleware :). They inject the strict CSP features on top of your base csp configuration. If you add them, it should work:

import {  chain, chainMatch,csp, isPageRequest,nextSafe, strictDynamic, strictInlineStyles } from '@next-safe/middleware';
import { NextRequest, NextResponse } from 'next/server';

const PUBLIC_FILE = /\.([a-zA-Z0-9]+$)/;

function routesMiddleware(request: NextRequest) {
 ...
}

const securityMiddleware = [
  nextSafe({ disableCsp: true }),
  csp({
    // your CSP base configuration with IntelliSense 
    // single quotes for values like 'self' are automatic
    directives: {
      'default-src': ['self'],
      'script-src': ['self'],
      'style-src': ['self', 'https://fonts.googleapis.com', 'unsafe-inline'],
      'font-src': ['self', 'https://fonts.gstatic.com', 'data:', ],
      'img-src': ['self', 'data:', 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com', 'https://*.g.doubleclick.net', 'https://*.google.com'],
      'media-src': ['self', 'data:', 'http://localhost:3000' ],
      'connect-src': ['self', 'https://*.google-analytics.com' , 'https://*.analytics.google.com', 'https://*.googletagmanager.com', 'https://*.g.doubleclick.net', 'https://*.google.com']
    },
  }),
  strictDynamic(),
  strictInlineStyles(),
];

export default chain(
  routesMiddleware,
  chainMatch(isPageRequest)(...securityMiddleware)
)

I would also recommend initializing the style server outside of the MyDocument.getInitialProps closure. The e2e app uses a setup with Mantine (uses emotion under the hood) that works and in Mantine docs it is done outside, like so:

const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);

MyDocument.getInitialProps = async (ctx) => {
const originalRenderPage = ctx.renderPage;
...
}

Please let me know if that solved your problem. I am also going to release 0.9.0 soon and drop support for Beta middleware in favor of stable middleware (no more pages/_middleware.ts, just root-level middleware.ts), so I also recommend the upgrade to Next 12.2 and 0.9.0, especially if you host your app on Vercel, I did some stuff to massively decrease CPU usage (see #45).

oauramos commented 2 years ago

Okay, reading here some questions related to Mantine. I build my own ServerStyle.tsx and works pretty great at Chrome. (Locally and deployed)

Buuuuuuuuuut, strict-dynamic are not supported by firefox. Have some way to work with that?

nibtime commented 2 years ago

I am currently working on #40 and I am experimenting with loading Next by an inline script proxy (transitive trust). I tried that before, but it messed up Next script loading, but I have a new idea that could work. Then it would work with Firefox.

With the current approach (injecting integrity attributes to scripts with src) unfortunately not, because Firefox messes up SRI validation for scripts with src attribute in conjunction with strict-dynamic: https://bugzilla.mozilla.org/show_bug.cgi?id=1409200

oauramos commented 2 years ago

For me its fine, if you want can close this case.