Skip to main content

Harden the JavaScript tracker against compromised scripts

The JavaScript tracker runs in your page, so every script it loads, including sp.js, plugins, and libraries that plugins load, runs with the same access as your own code. If a CDN or an upstream package serving one of those scripts is compromised, the change reaches your page the next time the script loads.

This page describes three measures that limit this exposure. You can apply them independently, but they work best together.

Self-host or bundle the tracker and plugins​

When you load sp.js or a plugin from a third-party CDN, the browser runs whatever that CDN serves at that URL. Hosting the files yourself means they only change when you deploy a new version.

You can remove third-party script loading for the tracker and its plugins in any of these ways:

  • Self-host sp.js by following the hosting guides
  • Install the tracker and plugins from npm and bundle them into your application
  • Create a custom build of sp.js that includes only the plugins you use

If you load plugins by URL using addPlugin, self-host the plugin files too, or pin each URL to an exact version, such as @4.10.2. Never use @latest or a major-version range such as @4 in production, since the file behind those URLs changes with each release.

Restrict which origins can serve scripts​

A Content Security Policy (CSP) tells the browser which origins your page can load scripts from and send requests to. The browser blocks a script from any other origin, even if the script is injected into your page.

Add the origins the tracker needs to your existing policy. The tracker needs script-src to allow the origin you load sp.js and plugins from, and connect-src to allow your Collector. For example, if you self-host the tracker and your Collector is at collector.example.com:

Content-Security-Policy: script-src 'self'; connect-src 'self' https://collector.example.com

The tag snippet that loads sp.js is an inline script, so a policy without 'unsafe-inline' also needs to allow the snippet with a nonce or hash.

Some plugins load additional scripts that also need to be allowed:

  • Web Vitals plugin: loads the web-vitals library from https://unpkg.com by default. Allow that origin, or self-host the library and set the webVitalsSource option to its URL
  • YouTube tracking plugin: loads the YouTube IFrame Player API from https://www.youtube.com

Verify scripts with Subresource Integrity​

Subresource Integrity (SRI) lets the browser check that a script matches a hash you provide, and refuse to run it if it doesn't. SRI only works with a URL that points to an exact version, because the hash changes whenever the file changes.

To get the hash for an exact version on jsDelivr, open the package page on jsDelivr and copy the SRI hash for the file. For a file you host yourself, compute the hash:

bash
openssl dgst -sha384 -binary sp.js | openssl base64 -A

The standard tag snippet creates the script element in JavaScript, so set the hash on that element rather than on a <script> tag. This version of the snippet adds n.integrity and n.crossOrigin before the script loads:

html
<script type="text/javascript" async=1>
;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; n.integrity="sha384-{{hash}}"; n.crossOrigin="anonymous"; n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","https://cdn.jsdelivr.net/npm/@snowplow/javascript-tracker@{{version}}/dist/sp.js","snowplow"));
</script>

When you upgrade the tracker, update the version and the hash together. If they don't match, the browser doesn't run the tracker and no events are sent.

Understand what these measures don't cover​

SRI checks only the script that has the integrity attribute. It doesn't check scripts that script loads afterwards.

The YouTube tracking plugin loads the IFrame Player API from https://www.youtube.com/iframe_api. Google doesn't version this file or publish a hash for it, so you can't pin it or verify it with SRI. If you use the YouTube plugin, your page runs whatever version of the API Google serves, which is also true of any page that embeds the YouTube player.

On this page

Try Snowplow Free for 14 Days

Collect behavioral data and act on it in real time. Design a schema, send an event, and fetch live context with the AI Assistant and MCP server.

Want to see a custom demo instead? Book a demo