hotjar / hotjar-js

Bring Hotjar directly to your application
https://hotjar.github.io/hotjar-js/
MIT License
49 stars 8 forks source link

nonce does not get passed to every script and style element #26

Open duhdugg opened 1 year ago

duhdugg commented 1 year ago

Running with the nonce option and appropriate CSP headers set, I am still seeing some blocking happening because the nonce is not used for every <script> and <style> created by Hotjar.

Here is the offending Hotjar code I'm seeing where a script element is created and appended without setting the nonce value:

(function r(e, t = !1) {
  const o = "6.0";
  let i = {};
  function s(e) {
    if (!e.__vdevtools__injected)
      try {
        e.__vdevtools__injected = !0;
        const t = () => {
          try {
            e.contentWindow.__VUE_DEVTOOLS_IFRAME__ = e;
            const t = e.contentDocument.createElement("script");
            (t.textContent = ";(" + r.toString() + ")(window, true)"),
              e.contentDocument.documentElement.appendChild(t),
              t.parentNode.removeChild(t);
          } catch (t) {}
        };
        t(), e.addEventListener("load", () => t());
      } catch (t) {}
  }
  let a = 0;
  // ...
})(window);

Here is the offending Hotjar code I'm seeing where a style element is created without a nonce value. Follow along to see that Ar is passing the nonce to Ln, which is setting the attribute correctly, but the nonce is never passed to Ar when defining Hr.

        Ln = (function () {
          function e(e) {
            var t = this;
            (this._insertTag = function (e) {
              var n;
              (n =
                0 === t.tags.length
                  ? t.prepend
                    ? t.container.firstChild
                    : t.before
                  : t.tags[t.tags.length - 1].nextSibling),
                t.container.insertBefore(e, n),
                t.tags.push(e);
            }),
              (this.isSpeedy = void 0 === e.speedy || e.speedy),
              (this.tags = []),
              (this.ctr = 0),
              (this.nonce = e.nonce),
              (this.key = e.key),
              (this.container = e.container),
              (this.prepend = e.prepend),
              (this.before = null);
          }
          var t = e.prototype;
          return (
            (t.hydrate = function (e) {
              e.forEach(this._insertTag);
            }),
            (t.insert = function (e) {
              this.ctr % (this.isSpeedy ? 65e3 : 1) == 0 &&
                this._insertTag(
                  (function (e) {
                    var t = document.createElement("style");
                    return (
                      t.setAttribute("data-emotion", e.key),
                      void 0 !== e.nonce && t.setAttribute("nonce", e.nonce),
                      t.appendChild(document.createTextNode("")),
                      t.setAttribute("data-s", ""),
                      t
                    );
                  })(this),
                );
              var t = this.tags[this.tags.length - 1];
              if (this.isSpeedy) {
                var n = (function (e) {
                  if (e.sheet) return e.sheet;
                  for (var t = 0; t < document.styleSheets.length; t++)
                    if (document.styleSheets[t].ownerNode === e)
                      return document.styleSheets[t];
                })(t);
                try {
                  n.insertRule(e, n.cssRules.length);
                } catch (e) {}
              } else t.appendChild(document.createTextNode(e));
              this.ctr++;
            }),
            (t.flush = function () {
              this.tags.forEach(function (e) {
                return e.parentNode && e.parentNode.removeChild(e);
              }),
                (this.tags = []),
                (this.ctr = 0);
            }),
            e
          );
        })(),
// ...
        Ar = function (e) {
// ...
          var h = {
            key: t,
            sheet: new Ln({
              key: t,
              container: r,
              nonce: e.nonce,
              speedy: e.speedy,
              prepend: e.prepend,
            }),
            nonce: e.nonce,
            inserted: a,
            registered: {},
            insert: o,
          };
          return h.sheet.hydrate(s), h;
        }
// ...
      var Hr = P(
        "undefined" != typeof HTMLElement
          ? Ar({
              key: "css",
            })
          : null,
      );
FCartier commented 1 year ago

Hey there

Have you tried to pass the nonce param in the init() function?

const siteId = 123;
const hotjarVersion = 6;
const initOptions = {
    debug: false,
    nonce: true
}

Hotjar.init(siteId, hotjarVersion, initOptions);

If it does not help, please give more context around your issue

duhdugg commented 1 year ago

Yes, I am passing that option. What more context would be helpful?

duhdugg commented 1 year ago

btw, the nonce option being passed matches the value sent in our CSP header. In your example you have nonce as a boolean, which is incorrect.

With the following CSP header:

default-src 'self';
img-src 'self' https://static.hotjar.com https://script.hotjar.com https://www.hotjar.com;
connect-src 'self' https://*.hotjar.com https://*.hotjar.io wss://*.hotjar.com;
font-src 'self' https://script.hotjar.com;
script-src 'self' https://static.hotjar.com https://script.hotjar.com nonce-rAnd0m;
style-src 'self' https://static.hotjar.com https://script.hotjar.com nonce-rAnd0m

...I am initializing Hotjar with:

Hotjar.init(siteId, hotjarVersion, {
  debug: false,
  nonce: "rAnd0m",
});

The problem is that while the initial javascript loads with nonce values correctly, additional inline scripts (and styles) are created by Hotjar which do not include the nonce value initially passed to Init. In my original post I point out where in the Hotjar javascript code that is happening.

The only solution I can find right now is to abandon nonces entirely and use 'unsafe-inline', which has a lot of security implications.