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

Setup with MUI #79

Open MariaSolOs opened 1 year ago

MariaSolOs commented 1 year ago

First of all thanks for the great library! ❤️

I'm currently trying to make this work in a Next project using MUI and I've been using the discussion in #50 as a guide (although it seems like the OP did use style-src: unsafe-inline and I'm trying to avoid that if possible).

This is what I have so far (the source code is here):

// middleware.ts

import { nextSafe, csp, strictDynamic, strictInlineStyles, chainMatch, isPageRequest } from '@next-safe/middleware';
import type { ChainableMiddleware } from '@next-safe/middleware';

const isDev = process.env['VERCEL_ENV'] === 'development';

const securityMiddleware: ChainableMiddleware[] = [
    nextSafe({ disableCsp: true, isDev }),
    csp({
        directives: {
            'font-src': ['self', 'https://fonts.gstatic.com', 'https://cdn.jsdelivr.net'],
            'style-src': ['self', 'https://fonts.googleapis.com', 'https://cdn.jsdelivr.net'],
            'connect-src': ['self', isDev ? 'localhost:4000' : 'https://paradeigma-apollo-app.vercel.app'],
            'worker-src': ['self', 'blob:']
        },
        isDev,
        // TODO: Remove this once I figure out the inline styles thing
        reportOnly: true
    }),
    strictDynamic(),
    strictInlineStyles()
];

export default chainMatch(isPageRequest)(...securityMiddleware);
// _document.tsx

import Document, { Html, Main, DocumentContext, DocumentInitialProps } from 'next/document';
import { getCspInitialProps, provideComponents } from '@next-safe/middleware/dist/document';
import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache, { INSERTION_POINT_NAME } from 'styles/emotion-cache';

type DocumentProps = DocumentInitialProps & { emotionStyleTags: JSX.Element[] };

const cache = createEmotionCache();
// eslint-disable-next-line @typescript-eslint/unbound-method
const { extractCriticalToChunks } = createEmotionServer(cache);

export default class CustomDocument extends Document<DocumentProps> {
    static override async getInitialProps(ctx: DocumentContext): Promise<DocumentProps> {
        const originalRenderPage = ctx.renderPage;

        ctx.renderPage = () =>
            originalRenderPage({
                // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Easily add the emotion cache
                enhanceApp: (App: any) =>
                    function EnhanceApp(props) {
                        return <App emotionCache={cache} {...props} />;
                    }
            });

        const initialProps = await getCspInitialProps({
            ctx,
            trustifyStyles: true,
            trustifyScripts: true
        });

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

        return {
            ...initialProps,
            emotionStyleTags
        };
    }

    override render(): JSX.Element {
        const { Head, NextScript } = provideComponents(this.props);

        return (
            <Html lang="en">
                <Head>
                    <meta charSet="utf-8" />
                    <meta name="author" content="Maria Solano" />

                    {/* PWA stuff */}
                    <link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
                    <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
                    <link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
                    <link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#1C2541" />
                    <meta name="msapplication-TileColor" content="#FFFFFF" />
                    <meta name="theme-color" content="#FFFFFF" />
                    <link rel="manifest" href="/manifest.json" />

                    {/* Google fonts */}
                    <link
                        href="https://fonts.googleapis.com/css2?family=Bungee&family=Bungee+Shade&family=PT+Mono&display=swap"
                        rel="stylesheet"
                    />

                    {/* Programming language icons */}
                    <link
                        rel="stylesheet"
                        href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.15.1/devicon.min.css"
                    />

                    <meta name={`"${INSERTION_POINT_NAME as string}"`} content="" />
                    {this.props.emotionStyleTags}
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

Unfortunately, I'm still having issues (you can visit the site and open the dev tools):

image

That error comes from the Google fonts, but I also have errors like these, which pop up when navigating to the "New mikro" page:

image

I'm stuck now and don't really know what I'm missing :( I also tried following the setup from the e2e example but it resulted in the same errors.

Thanks in advance for your help!

MariaSolOs commented 1 year ago

I should also mention that those errors only happen in production (but I guess that's because of the isDev settings).

kiily commented 1 year ago

I am hitting a similar roadblock here, in my case it's related to the styles that I import in my _app.tsx file as below:

import "react-toastify/dist/ReactToastify.css";
import "video.js/dist/video-js.css";
import "../styles/globals-2-2.css";
jake-ruth-qm commented 1 year ago

Any updates here on being able to use MUI without unsafe-inline? Did you guys find any solution?

Jokinen commented 1 year ago

We are facing similar issues.

Unfortunately I do not have a complete solution. I can however point out that the issue that has to do with Google Fonts that's mentioned in the first message can be rectified with the example in the repo for using Google Fonts. Likely the styles imported from cdn.jsdelivr.net are also an issue if not adjusted.

I attempted to take some patterns from the e2e example and as I've progressed in an explorative manner, I can't list in detail all the steps I took.

In general it seems that a way to debug these issues is to try and find the inline styles that cause these issues. It may be that some cases don't directly relate to the interop of MUI and this library, but instead are their own niche cases that circumvent the MUI's CSS-in-JS implementation.

For instance in our case one problem scenario came about due to a 3rd party library that we were using. It included its own inline styles which were not hashed at any point because they are only loaded in the client once they are needed.

I could find out that these styles were the issue by navigating to the "source file" through the error log and adding a breakpoint that allowed me to inspect what CSS created an error when it was added into the DOM.

As far as I know, some of these files you just have to manually pull into the hashRawCss function so that hashes can be created for them. Not sure if there are approaches that are easier to maintain.

For now I still haven't been able to resolve all cases. But as far as I can see, they tend to relate to how 3rd party libraries include css in the DOM. If you import a css file with import "style.css", next may inline it into the DOM causing this error. The use of such an import can't necessarily be programmatically predicted as far as I can see.

However, I couldn't find a good way to extract the CSS that next injects to the head for 3rd party apps (as talked about here https://nextjs.org/docs/pages/building-your-application/styling/css-modules#import-styles-from-node_modules). Next may optimise the css files, add source maps and whatnot. These changes result in changes to the text content which in turn result in changes to the created hash. I would need to get a representation of the contents of the style tag that matches with what Next will inject in the client once the code is required in order to get the correct hash.

One workaround I could come up with was to not load this CSS as needed, but instead always load it by including it in the _app file. This way it'll always be available to be processed during the server side render. The obvious downside is that unnecessary data will be sent to the user.

Take this next bit with a grain of salt as I couldn't find a source that addressed this issue specifically.

One thing that may cause confusion is that CSP has different "strictnesses". Some contexts refer to hashes and nonces as "strict" CSP whereas CSPs that contain safelists are "safelist" CSPs (strict-dynamic may also need to be set). In general it seems that you can't mix and match a strict CSP with a safelisted CSP. This seems to be the reason for why for instance Google Fonts fails to load even though its URI is correctly included in the CSP. As a strict CSP is used for style-src, the browser ignores the provided safelist and instead expects hashes or nonces that allow the particular inline styles to be applied.

It's not incorrect per se to have these mix matched directives. In essence it can be looked as a backwards compatibility feature. One could in theory provide a safelist for older browsers and nonces and hashes for newer ones.