0

I am using a brand new app generated by create-react-app 3.4.1. It uses the default service worker file:

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
  // [::1] is the IPv6 localhost address.
  window.location.hostname === '[::1]' ||
  // 127.0.0.0/8 are considered localhost for IPv4.
  window.location.hostname.match(
    /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
  )
);

type Config = {
  onSuccess?: (registration: ServiceWorkerRegistration) => void;
  onUpdate?: (registration: ServiceWorkerRegistration) => void;
};

export function register(config?: Config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(
      process.env.PUBLIC_URL,
      window.location.href
    );
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (isLocalhost) {
        // This is running on localhost. Let's check if a service worker still exists or not.
        checkValidServiceWorker(swUrl, config);

        // Add some additional logging to localhost, pointing developers to the
        // service worker/PWA documentation.
        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service ' +
            'worker.'
          );
        });
      } else {
        // Is not localhost. Just register service worker
        registerValidSW(swUrl, config);
      }
    });
  }
}

function registerValidSW(swUrl: string, config?: Config) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        if (installingWorker == null) {
          return;
        }
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the updated precached content has been fetched,
              // but the previous service worker will still serve the older
              // content until all client tabs are closed.
              console.log(
                'New content is available and will be used when all ' +
                'tabs for this page are closed.'
              );

              // Execute callback
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.');

              // Execute callback
              if (config && config.onSuccess) {
                config.onSuccess(registration);
              }
            }
          }
        };
      };
    })
    .catch(error => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl: string, config?: Config) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' }
  })
    .then(response => {
      // Ensure service worker exists, and that we really are getting a JS file.
      const contentType = response.headers.get('content-type');
      if (
        response.status === 404 ||
        (contentType != null && contentType.indexOf('javascript') === -1)
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl, config);
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.'
      );
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready
      .then(registration => {
        registration.unregister();
      })
      .catch(error => {
        console.error(error.message);
      });
  }
}

I turned on service worker by changing the code in index.ts to

serviceWorker.register();

I hosted the static files generated by yarn build through https by an Express.js server with strict Content Security Policy (CSP) turned on by helmet.

helmet({
  contentSecurityPolicy: {
    directives: {
      scriptSrc: [
        /* Content Security Policy Level 3 */
        "'strict-dynamic'",
        `'nonce-${cspNonce}'`,

        /* Content Security Policy Level 2 (backward compatible) */
        "'self'",

        // Workbox
        'https://storage.googleapis.com',

        // ...
      ],
      styleSrc: [
        "'self'",
      ],
      // ...
    },
  },
})

When I first time opening the page, the browser fetch files from server. Both JS and CSS have CSP headers. The page shows well.

When I second time opening the page, the files are loaded from service worker. Many got blocked by CSP, as my console shows:

enter image description here

When I further check, CSS files served by service worker still have CSP headers (and nonce inside also changed to new value, create-react-app did it for us?), which load well.

enter image description here

However, the CSP headers on JS files are missing, which got blocked.

enter image description here

Any guide will be helpful. Thanks!


UPDATE

One thing I notice in Chrome, it shows

CAUTION: provisional headers are shown

and I found more info at

"CAUTION: provisional headers are shown" in Chrome debugger

Another thing I found, the page won't load on second call on Chrome and Safari after service worker (create-react-app uses Workbox internally) registered.

For Firefox, although CSP headers are not shown neither in JS and CSS files when read from cache, Firefox still can show the page.

enter image description here

Hongbo Miao
  • 45,290
  • 60
  • 174
  • 267

2 Answers2

2

It is likely that the first time you load the page the nonce in your CSP and your script tags are in sync. On the second load they are no longer present or in sync in your script tags. Check the difference in nonce values in the CSP header and inline script tags.

CSP applies to pages being rendered in the browser (content-type: "text/html"), it doesn't have any effect when set on the other resources loaded. Missing CSP header on js files doesn't have any effect. Your CSS files are included because you include "style-src 'self'", you should add this to script-src as well. If it is not sufficient you could add localhost:5000 in development.

Halvor Sakshaug
  • 2,583
  • 1
  • 6
  • 9
  • Thanks Halvor! I do have `'self'` in **script-src**. I tested on prod environment by `yarn build` and then served by **https**. For CSS, the nonce inside also changed to new value, so I guess create-react-app did it for us (?). Added these info in my question. – Hongbo Miao Sep 14 '20 at 09:49
0
  1. As noticed Halvor Sakshaug above, you do not need to serve JS/CSS with CSP headers, CSP work only for page/code having document property.

  2. As seen from you Chrome console warnings, there is at least 2 issues:

  • an inline scripts blocked (you do use < script>...< /script> or < tag unClick='...'> somewhere). So you have to add 'unsafe-inline' to script-src (or add nonce='server_generated_value' attribute to < script>...< /script>), BUT:
  • 'strict-dynamic' cancels host-based allowlisting (incliding 'self') in CSP3-browsers, so your https://localhost (and other hosts) will be disabled. Also 'strict-dynamic' cancels 'unsafe-inline' ('nonce-value' and 'hash-value' cancel it too). Probably you do not sign inline scripts with nonce='server_generated_nonce' attribute. Or you do use scripts calls incompatible with 'strict-dynamic' (parser-inserted scripts, inline event handlers etc)

You have to revise Content Security Policy rules, they are inconsistent.

granty
  • 7,234
  • 1
  • 14
  • 21