Anomaly in CSP runtimeConfigHash?

Update This post is background. Please see the next post in this thread for documentation of what appears to be an anomaly.


I’m trying to learn more about runtimeConfigHash. This is used in conjunction with CSP. From the Meteor docs page for setting up CSP with Helmet:

// Prepare runtime config for generating the sha256 hash
// It is important, that the hash meets exactly the hash of the
// script in the client bundle.
// Otherwise the app would not be able to start, since the runtimeConfigScript
// is rejected __meteor_runtime_config__ is not available, causing
// a cascade of follow-up errors.
const runtimeConfig = Object.assign(__meteor_runtime_config__, Autoupdate, {
  // the following lines may depend on, whether you called Accounts.config
  // and whether your Meteor app is a "newer" version
  accountsConfigCalled: true,
  isModern: true
})

// add client versions to __meteor_runtime_config__
Object.keys(WebApp.clientPrograms).forEach(arch => {
  __meteor_runtime_config__.versions[arch] = {
    version: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].version(),
    versionRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionRefreshable(),
    versionNonRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionNonRefreshable(),
    // comment the following line if you use Meteor < 2.0
    versionReplaceable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionReplaceable()
  }
})

const runtimeConfigScript = `__meteor_runtime_config__ = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(runtimeConfig))}"))`
const runtimeConfigHash = crypto.createHash('sha256').update(runtimeConfigScript).digest('base64')

If I log runtimeConfigScript to the console, I get this (after decoding the percent-encoding):

{
  "meteorRelease": "METEOR@3.1.1",
  "gitCommitHash": "5777d34d2d4e047fdb2b0e16c9f0f948a7c20201",
  "meteorEnv": {
    "NODE_ENV": "production",
    "TEST_METADATA": "{}"
  },
  "PUBLIC_SETTINGS": {
    "helpscout_beacon_id": "b3230094-1cd5-41c1-860f-a893f6e599d1",
    "agora_public": {
      "development": {
        "appId": "4255acabc1cd482da45a29722e1f4de4"
      },
      "production": {
        "appId": "0fec9570c92a457a9f4a05dda22292e7"
      }
    },
    "packages": {
      "dynamic-import": {
        "useLocationOrigin": true
      }
    },
    "staging_settings": {
      "cloudfront_distribution_domain_name": "d3dm8n75uk9h6.cloudfront.net",
      "csp_json_filename": "csp.json"
    }
  },
  "debug": false,
  "ROOT_URL": "https://www.talk2anurse.com/",
  "ROOT_URL_PATH_PREFIX": "",
  "reactFastRefreshEnabled": true,
  "autoupdate": {
    "versions": {
      "web.browser": {
        "version": "08fad630653109864f1cf07a4641661dfdcc83b4",
        "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
        "versionNonRefreshable": "3a32e88e622cfefbe6c66f752e83d90aba92ed6a",
        "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
      },
      "web.browser.legacy": {
        "version": "2fea4f37e27a3d46aec4987387d5018c8b01579d",
        "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
        "versionNonRefreshable": "25006e314e064a0e7c55c022b68ec276f6953067",
        "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
      }
    },
    "autoupdateVersion": null,
    "autoupdateVersionRefreshable": null,
    "autoupdateVersionCordova": null,
    "appId": "h5ftnywqyw9u.wgzlv1rg0bib"
  },
  "appId": "h5ftnywqyw9u.wgzlv1rg0bib",
  "accountsConfigCalled": true,
  "versions": {
    "web.browser": {
      "version": "08fad630653109864f1cf07a4641661dfdcc83b4",
      "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
      "versionNonRefreshable": "3a32e88e622cfefbe6c66f752e83d90aba92ed6a",
      "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
    },
    "web.browser.legacy": {
      "version": "2fea4f37e27a3d46aec4987387d5018c8b01579d",
      "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
      "versionNonRefreshable": "25006e314e064a0e7c55c022b68ec276f6953067",
      "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
    }
  },
  "isModern": true
}

Now, the website includes this script element:

<script type="text/javascript">__meteor_runtime_config__ = JSON.parse(decodeURIComponent("%7B%22meteorRelease%22%3A%22METEOR%403.1.1%22%2C% [.....]</script>

If I decode the percent-encoded content of this __meteor_runtime_config__ element, I get this:

__meteor_runtime_config__ = {
  "meteorRelease": "METEOR@3.1.1",
  "gitCommitHash": "a556d7bc260fe505ebc8d7add7aed7aaf9dfb9f5",
  "meteorEnv": {
    "NODE_ENV": "production",
    "TEST_METADATA": "{}"
  },
  "PUBLIC_SETTINGS": {
    "helpscout_beacon_id": "b3230094-1cd5-41c1-860f-a893f6e599d1",
    "agora_public": {
      "development": {
        "appId": "4255acabc1cd482da45a29722e1f4de4"
      },
      "production": {
        "appId": "0fec9570c92a457a9f4a05dda22292e7"
      }
    },
    "packages": {
      "dynamic-import": {
        "useLocationOrigin": true
      }
    },
    "staging_settings": {
      "cloudfront_distribution_domain_name": "d3dm8n75uk9h6.cloudfront.net",
      "csp_json_filename": "csp.json"
    }
  },
  "debug": false,
  "ROOT_URL": "https://www.talk2anurse.com/",
  "ROOT_URL_PATH_PREFIX": "",
  "reactFastRefreshEnabled": true,
  "autoupdate": {
    "versions": {
      "web.browser": {
        "version": "08fad630653109864f1cf07a4641661dfdcc83b4",
        "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
        "versionNonRefreshable": "3a32e88e622cfefbe6c66f752e83d90aba92ed6a",
        "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
      },
      "web.browser.legacy": {
        "version": "2fea4f37e27a3d46aec4987387d5018c8b01579d",
        "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
        "versionNonRefreshable": "25006e314e064a0e7c55c022b68ec276f6953067",
        "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
      }
    },
    "autoupdateVersion": null,
    "autoupdateVersionRefreshable": null,
    "autoupdateVersionCordova": null,
    "appId": "h5ftnywqyw9u.wgzlv1rg0bib"
  },
  "appId": "h5ftnywqyw9u.wgzlv1rg0bib",
  "accountsConfigCalled": true,
  "versions": {
    "web.browser": {
      "version": "08fad630653109864f1cf07a4641661dfdcc83b4",
      "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
      "versionNonRefreshable": "3a32e88e622cfefbe6c66f752e83d90aba92ed6a",
      "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
    },
    "web.browser.legacy": {
      "version": "2fea4f37e27a3d46aec4987387d5018c8b01579d",
      "versionRefreshable": "8f999f9664b487ef9a79b8f8812b0873220581d7",
      "versionNonRefreshable": "25006e314e064a0e7c55c022b68ec276f6953067",
      "versionReplaceable": "aecfa0226aa6a00e9d159fe3872ea7dd661e60f5"
    }
  },
  "autoupdateVersion": null,
  "autoupdateVersionRefreshable": null,
  "autoupdateVersionCordova": null,
  "isModern": true,
  "kadira": {
    "appId": "kuY8Wjg9m2XWDLQrf",
    "endpoint": "https://engine-us.montiapm.com",
    "clientEngineSyncDelay": 10000,
    "recordIPAddress": "full",
    "disableClientErrorTracking": false,
    "enableErrorTracking": true
  }
}

Notice that they are not the same. The gitCommitHash is different, and there’s a section for kadira at the bottom of the first one that isn’t present at the bottom of the second one.

So is this expected and okay – or is there something wrong that will break the runtimeConfigHash and keep my CSP from working?

Documentation of apparent anomaly. Meteor 3.1.1.

It appears to be possible to re-create the runtimeConfigHash in the browser like this:

const scriptElement = Array.from(document.getElementsByTagName('script'))
  .find(script => script.textContent.includes('__meteor_runtime_config__'));
if (scriptElement) {
    const hash = await crypto.subtle.digest('SHA-256', 
        new TextEncoder().encode(scriptElement.textContent));
    console.log('Actual browser script hash:', 
        btoa(String.fromCharCode(...new Uint8Array(hash))));
}

I tested this on the server like this:

const runtimeConfigHash = crypto.createHash('sha256').update(runtimeConfigScript).digest('base64')
console.log('runtimeConfig: ', runtimeConfig)
console.log('runtimeConfigScript: ', runtimeConfigScript)
console.log('runtimeConfigHash: ', runtimeConfigHash)

const runtimeConfigHash_recreated =   await crypto.subtle.digest('SHA-256',
    new TextEncoder().encode(runtimeConfigScript));
console.log('runtimeConfigHash_recreated:',
    btoa(String.fromCharCode(...new Uint8Array(runtimeConfigHashTest))));

runtimeConfigHash_recreated matched runtimeConfigHash on the server.

runtimeConfigHash is what is fed to CSP. As noted in the Meteor docs:

// Prepare runtime config for generating the sha256 hash
// It is important, that the hash meets exactly the hash of the
// script in the client bundle.
// Otherwise the app would not be able to start, since the runtimeConfigScript
// is rejected __meteor_runtime_config__ is not available, causing
// a cascade of follow-up errors.

It appears that they don’t match exactly. On my site as served from production, runtimeConfigHash == fdtPo1B6IMMVVZ0G12TWnISFI/oXIGRiuW7cw5pNJ6U=. But running this on in the browser console for the website:

const scriptElement = Array.from(document.getElementsByTagName('script'))
  .find(script => script.textContent.includes('__meteor_runtime_config__'));
if (scriptElement) {
    const hash = await crypto.subtle.digest('SHA-256', 
        new TextEncoder().encode(scriptElement.textContent));
    console.log('Actual browser script hash:', 
        btoa(String.fromCharCode(...new Uint8Array(hash))));
}

…logs: y/wvebrnBw1Z3ThN+Ov/Mhkt65Aiv4w7B+JKApcmXWM=

This makes sense since, as shown in the first post in this thread, the runtimeConfigScript used to generate the runtimeConfigHash includes info about Kadira on the final served webpage, but not on the server when using the code provided by the Meteor docs to create it.

The fix is probably easy. I need to update this code from the Meteor docs:

const runtimeConfig = Object.assign(__meteor_runtime_config__, Autoupdate, {
  // the following lines may depend on, whether you called Accounts.config
  // and whether your Meteor app is a "newer" version
  accountsConfigCalled: true,
  isModern: true
})

…to include the matching info about Kadira. What’s the correct way to do that?

Hmmm… Kadira isn’t even in Galaxy anymore. It’s been updated to MontiAPM. So the question is – why is info about Kadira, included in the meteor_runtime_script specs that show up on inside of <script> tags on the server?