How uBlock Origin's Scriptlet Injector Safely Evaluates Code: Architecture and Implementation

uBlock Origin uses a sentinel-based verification system with automatic Blob URL fallback to ensure scriptlets execute safely even when Content Security Policies block inline scripts.

The gorhill/uBlock repository implements a sophisticated scriptlet injection mechanism that evaluates JavaScript filter code securely within web pages. This system, centered around vAPI.scriptletsInjector, guarantees execution success while preventing duplicate injections and bypassing Content Security Policy (CSP) restrictions.

The Scriptlet Injector Architecture

Injector Initialization and Structure

The core injector is defined as an Immediately Invoked Function Expression (IIFE) in platform/firefox/vapi-background-ext.js (lines 54-104). This pattern initializes a global tracking mechanism and returns a function that accepts a hostname and details object:

// https://github.com/gorhill/uBlock/blob/master/platform/firefox/vapi-background-ext.js#L54-L104
vAPI.scriptletsInjector = (( ) => {
    self.uBO_scriptletsInjected = '';
    // Returns function for hostname/details processing
})();

Sentinel Generation for Execution Verification

Before each injection, the system generates a one-time random token using vAPI.generateSecret(3), implemented in platform/common/vapi-background.js (lines 91-96). This sentinel serves as a cryptographic proof-of-execution:

  • The secret is embedded in the injected code as self['<secret>'] = true
  • Post-injection verification checks for this property on self.wrappedJSObject
  • Absence triggers the fallback mechanism, indicating CSP or execution failure

Script Assembly and Hostname Validation

The injector constructs a self-executing wrapper function (lines 55-84) that performs multiple safety checks before running scriptlet code:

  1. Verifies self.uBO_scriptletsInjected is unset to prevent double-injection
  2. Validates document.location.hostname matches the provided target hostname
  3. Sets the sentinel property to signal execution
  4. Creates a script element containing the payload and appends it to document.documentElement

Safe Evaluation Mechanisms

Preventing Accidental Double-Injection

The first operation in the injected wrapper checks self.uBO_scriptletsInjected. If this property exists, indicating previous successful injection, the function returns immediately. Upon successful execution, the property is set to the list of applied filter strings, providing both auditability and duplicate prevention.

CSP Bypass via Blob URL Fallback

When inline script execution fails (sentinel not detected), the injector automatically falls back to a Blob-based approach (lines 89-104):

const blob = new Blob(
    [ details.scriptlets ],
    { type: 'text/javascript; charset=utf-8' }
);
const url = URL.createObjectURL(blob);
// Script element created with src=url, async=false

This technique converts inline code into an external resource, bypassing script-src restrictions that prohibit inline execution while maintaining synchronous loading through async = false. The Blob URL is revoked immediately after insertion to prevent memory leaks.

Isolated World Execution

For scriptlets requiring protection from page-script interference, the isolatedWorldInjector in src/js/scriptlet-filtering.js (lines 10-43) provides a separate execution context. This mechanism ensures that sensitive filter operations—such as modifying fetch or XMLHttpRequest prototypes—remain inaccessible to the host page's JavaScript environment, preventing detection or tampering by malicious scripts.

Implementation Pipeline and Caching

High-Level Orchestration

The ScriptletFilteringEngineEx.retrieve method in src/js/scriptlet-filtering.js (line 308) orchestrates the complete injection workflow:

  1. Matches filter rules to scriptlet templates using src/js/scriptlet-filtering-core.js
  2. Invokes vAPI.scriptletsInjector(hostname, details) to generate wrapped code
  3. Registers the resulting code with the content script system

Per-Hostname Caching Strategy

To optimize performance, the system maintains an MRU cache (scriptletCache, lines 68-76) storing assembled code templates per hostname. This eliminates redundant secret generation and string assembly for repeat visits. Each cached entry maintains a fresh sentinel, ensuring security verification remains accurate across multiple injection attempts.

Practical Code Examples

Programmatic Scriptlet Injection

// Background script context using vAPI
const hostname = 'example.com';
const scriptletDetails = {
    hostname,
    mainWorld: `console.log('uBO scriptlet executing');`,
    isolatedWorld: '',
    filters: ['@@||example.com^$scriptlet']
};

// Generate injection code with embedded sentinel and fallback logic
const injectionCode = vAPI.scriptletsInjector(hostname, scriptletDetails);

// Register with the content script manager
contentScriptRegisterer.register(hostname, injectionCode);

Verification Logic Structure

The injected wrapper follows this safety pattern:

// Executed in the main world context
if ( self.uBO_scriptletsInjected ) { return; }
if ( document.location.hostname !== targetHostname ) { return; }

// Set sentinel before scriptlet execution
self[secretToken] = true;

// ... actual scriptlet code executes here ...

// Background script later verifies:
// if ( self.wrappedJSObject[secretToken] ) { success }

Summary

  • uBlock Origin implements scriptlet safety through vAPI.scriptletsInjector in platform/firefox/vapi-background-ext.js, using sentinel-based verification to guarantee execution
  • Double-injection prevention relies on the self.uBO_scriptletsInjected global flag checked before any code runs
  • CSP circumvention automatically falls back to Blob URL injection when inline scripts are blocked, using synchronous external script loading
  • Isolated world support protects sensitive operations through separate injection contexts defined in src/js/scriptlet-filtering.js
  • MRU caching in the filtering engine optimizes performance by reusing assembled templates per hostname while maintaining unique security tokens for each injection

Frequently Asked Questions

How does uBlock Origin verify that a scriptlet actually executed?

The injector generates a random sentinel using vAPI.generateSecret(3) and embeds code that sets this token as a property on the global object (self['<secret>'] = true). After the script element is removed, the background script checks self.wrappedJSObject for this property. Presence confirms successful execution; absence triggers the Blob URL fallback mechanism.

What happens when Content Security Policy blocks inline scripts?

When sentinel verification fails (indicating CSP blocked the inline script), the injector automatically creates a new Blob() containing the scriptlet code and generates an object URL. This converts the injection into an external script resource, which most CSP configurations permit via script-src directives allowing blob: or self origins. The script loads synchronously using async = false to maintain execution order.

How does the system prevent scriptlet code from leaking to the page?

The injector stores the sentinel on wrappedJSObject—a cross-origin wrapper in Firefox that provides safe access to page globals—and immediately deletes it after verification. For complete isolation, the isolatedWorldInjector executes code in a separate JavaScript realm where page scripts cannot access or modify the injected functionality, protecting against interference from hostile web pages.

Where are assembled scriptlets cached for performance?

Prepared injection code is cached in an MRU (Most Recently Used) cache implemented in src/js/scriptlet-filtering.js (lines 68-76). The cache maps hostnames to assembled code strings, avoiding repeated string concatenation and secret generation for repeat visits. Each cache entry includes the complete wrapper with a specific sentinel, though the cache is cleared periodically to prevent stale entries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →