BrowserSync / browser-sync

Keep multiple browsers & devices in sync when building websites. https://browsersync.io
https://discord.gg/2d2xUThp
Apache License 2.0
12.16k stars 755 forks source link

Https proxying blocked by browsers Content Security Policy #1241

Open Hurtak opened 7 years ago

Hurtak commented 7 years ago

Issue details

I have server on which I start BrowserSync proxy from CLI (code bellow), the proxied url has some content-security-policy headers set

Browser error:

browser-sync-client.js?v=2.17.5:2Refused to connect to 'wss://joe.dev:10000/browser-sync/socket.io/?EIO=3&transport=websocket&sid=IJFLKqUjWn-BQrnnAAAA' because it violates the following Content Security Policy directive: "default-src data: https: 'unsafe-inline' 'unsafe-eval'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

CSP headers set on proxied url:

content-security-policy:default-src data: https: 'unsafe-inline' 'unsafe-eval'; report-uri /csp-report

I have control over the proxied url html so I could add <meta http-equiv="Content-Security-Policy"> tag but so far I had no luck with making it work. Ideally it would only allow urls that BrowserSync needs and nothing more.

Please specify which version of Browsersync, node and npm you're running

Affected platforms

Browsersync use-case

If CLI, please paste the entire command below

#!/bin/bash -eu

./node_modules/.bin/browser-sync start \
    --proxy "https://page.dev" \
    --port 10000 \
    --ui-port 10001 \
    --cors \
    --no-open

for all other use-cases, (gulp, grunt etc), please show us exactly how you're using Browsersync

./start.sh
Hurtak commented 7 years ago

Ok, I have figured it out. Probably not possible in CLI, but in nodejs interface you are able to alter the response of the proxied request - so what I did is I added little CSP parser and if CSP header is present, I add BrowserSync's wss url into the header.

For anybody interested here is code snippet that fixes the problem

'use strict'

const bs = require('browser-sync').create()
const CspParse = require('csp-parse')

bs.init({
  proxy: {
    target: 'https://page.dev',
    proxyRes: [
      function (proxyRes, req, res) {
        const cspHeader = proxyRes.headers['content-security-policy']
        if (!cspHeader) return

        const newCsp = new CspParse(cspHeader)

        // wss url which browser sync uses to communicate between devices,
        // CPS headers could block this url so add it to allowed urls
        const url = 'wss://' + req.headers.host
        newCsp.add('default-src', url)
        const newCspString = newCsp.toString()

        proxyRes.headers['content-security-policy'] = newCspString
      }
    ]
  },
  port: 10000,
  ui: {
    port: 10001
  }
})

This whole thing could probably be integrated into the Browser-Sync to make proxying even easier (behind some flag)

johndatserakis commented 5 years ago

@Hurtak so clutch you should work for Toyota. Thank you.

hnipps commented 4 years ago

I came upon the issue while using lite-server and built upon @Hurtak 's answer to solve this issue with middleware:

const CspParse = require('csp-parse');

module.exports = {
  https: true,
  server: {
    baseDir: "./dist",
    middleware: {
      2: function (req, res, next) {
        const origHeaders = req.headers;
        const cspHeader = origHeaders['content-security-policy'];
        const url = 'https://' + req.headers.host;
        const websocket = 'wss://' + req.headers.host;

        if (cspHeader) {
          const newCsp = new CspParse(cspHeader);

          // wss url which browser sync uses to communicate between devices,
          // CPS headers could block this url so add it to allowed urls

          newCsp.add('default-src', `'unsafe-inline' ${url} ${websocket}`);
          const newCspString = newCsp.toString();
          res.setHeader('content-security-policy', newCspString);

        } else {
          const newCsp = new CspParse('script-src', `'unsafe-inline' ${url} ${websocket}`);
          newCsp.add('default-src', `'unsafe-inline' ${url} ${websocket}`);
          const newCspString = newCsp.toString();
          res.setHeader('content-security-policy', newCspString);
        }
        next();
      }
    }
  }
}