Browser Runtime Freeze

Ensuring the integrity of the browser runtime environment.

This is the most important security defense. It’s a prerequisite for the other measures to be effective.

It ensures the integrity of the browser environment. This blocks attackers from altering core JavaScript behavior to exfiltrate tokens.

Enabling the defense

vite.config.ts
import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
            // ...
            browserRuntimeFreeze: {
                enabled: true,
                // exclude: ["Promise", "fetch", "XMLHttpRequest"]
            }
        })
    ]
});

browserRuntimeFreeze.exclude

Your app may fail to start after enabling browserRuntimeFreeze.

You might see an exception like this:

In this example, Zone.js tries to overwrite window.fetch. Other libraries can do the same. Telemetry libraries are common offenders (for example, @microsoft/applicationinsights-react-js).

You have two options:

  1. Remove or replace the library that monkey-patches the runtime. (For example, can you go zoneless?)

  2. Add an exception for a specific API. For example, add "fetch" to exclude to allow patching fetch.

How much is my security posture degraded by adding exclusion?

Excluding fetch and XMLHttpRequest is usually not too bad. Although they are the first APIs attackers try to instrument, DPoP and/or Token Substitution makes those vectors much less useful.

The APIs that are most critical like Function, String, or JSON are very rarely instrumented by legitimate library so you shouldn't have to exclude them.

Understanding What This Protects Against

In JavaScript, most built-in APIs can be altered at runtime.

Consider this attack:

browserRuntimeFreeze exists to prevent this.

It ensures that .split(), fetch(), or Promise.then() calls the real browser built-in. It blocks monkey-patched versions from dependencies or XSS.

With browserRuntimeFreeze enabled, String.prototype.split = () => {} throws at runtime.

Was this helpful?