"The web hosts most of the world's new crypto functionality. A significant portion of that crypto has been implemented in Javascript, and is thus doomed.”
In the coming weeks, we are preparing for the testing release for WEBCAT, our previously announced framework for distributing signed, integrity-verified web applications in the browser. WEBCAT aims to address the long-standing “chicken-and-egg” problem of verifying web applications that perform, among other things, client-side cryptography. We recently presented its design at Transparency.dev 2025 and gave a demo at Tor’s State of the Onion 2025.
Now that we are participating in broader community efforts to bring some of WEBCAT’s guarantees to new web standards, we frame these efforts in this post by explaining the main challenges in designing an integrity mechanism for the web, focusing on reproducibility and auditability. Some are explained in more detail in this document, though parts of the architecture have since evolved.
What is a web application, anyway?
WEBCAT wants to bring package-manager or app-store-like security to some web applications. But which ones? Not all web applications are suitable for this model. Some mix client and server code by dynamically generating markup such as HTML, or code such as JavaScript, depending on the user session or queries. These applications (imagine your legacy home banking) do not have a clear division between a client and a server: The server itself generates parts of the client upon every execution. Thus, it is impossible to sign and commit strictly to the client’s code. WEBCAT cannot be used in this context.
A well-known historical example is JSONP. At a time when browsers did not allow cross-origin data requests, developers could bypass this restriction by loading data as executable JavaScript. The downside was that if a JSONP endpoint injected any additional code — whether intentionally or via a reflected cross-site scripting vulnerability — that code would be executed directly in the user’s browser. The classic mix of PHP and HTML, or more generally any form of server-side templating, incurs the same limitations.
In contrast, when a clear split between frontend and backend is defined, for instance by using a REST API or a WebSocket for data exchange, then we have what is often referred to as a single-page application. SPAs generally load all their code from an HTML page, and then dynamically load, route, and populate their views using only JavaScript. As a consequence, the whole web application can be pre-generated, and its code is not expected to mutate across requests or during execution. Many modern web applications follow this model, and some of them were compatibility targets while developing WEBCAT, such as Jitsi and Cryptpad.
While WEBCAT aims to be compatible with SPAs, it is not restricted to them. Anything that can be pre-generated (such as websites built using static site generators) can easily be signed and transparency logged. As such, one could have a static website that has signed pages, for instance, for authenticated contact information, warrant canaries, or other security-sensitive content.
Let’s talk bundles and manifests
WebBundles, an earlier proposal from Google, shared a similar high-level requirement: Package a web application and sign it, so that the same bundle could be reloaded either locally or from other origins while preserving integrity guarantees. Although Google’s goal at the time was different, and WebBundles never really gained third-party adoption, their structure remains instructive. They include a special archive of web application assets, with associated metadata, such as all the necessary HTTP headers, including, but not limited to, the Content Security Policy and a content-type for each resource.
For several reasons, we have chosen not to adopt an archive-based approach. First, avoiding archives allows for greater flexibility in progressive loading; only the resources required for a given functionality need to be fetched, rather than downloading a monolithic bundle containing the entire application. Second, this design is more feasible to implement and verify within the constraints of a WebExtension. Finally, we want to preserve zero-effort backward compatibility; by introducing an additional metadata file instead of changing existing resource formats, browsers that do not support WEBCAT can simply ignore it and continue to operate unchanged. Thus, we rely on a single metadata file to describe and verify application resources.
Accordingly, WEBCAT — like related proposals — relies on a metadata file called a manifest (not to be confused with PWA Web Manifests). This manifest describes the web application, its assets, and its execution environment. It is the only object that needs to be signed and transparency-logged, but because it commits to all of the assets that make up the application, they inherit those security properties.
A reproducible runtime is the only auditable runtime
Let’s start by stating the obvious: No mechanism described here can prevent vulnerabilities in web application code, and nothing can stop intentionally malicious code. If a developer signs and distributes a messaging application that sends your private keys to third parties, this cannot be prevented. If someone prepares a browser exploit and signs it, it will still run. If one of the API endpoints contains an SQL injection or a similar vulnerability, it will be exploitable. What can be done, by designing a solid sandbox, is to prevent some classes of bugs and some known bad paradigms.
WEBCAT aims for two very ambitious goals:
- The execution environment must be reproducible.
- Only JavaScript that has been committed to may execute within the same-origin policy of a WEBCAT application.
To restate it differently: In a web application verified using a given manifest, no JavaScript that is not present in a manifest should reach execution in the same context. The practical consequence of it is that an auditor (somebody wanting to verify what is being executed and reproduce the application behavior) must be able to access all the corresponding JavaScript.
To show concrete examples, the following code snippets would violate these constraints:
eval(await (await fetch("/someimage.jpg")).text());
In this case, eval allows the execution of arbitrary JavaScript strings that can either be constructed at runtime or fetched from anywhere, without meaningful restrictions.
document.write(`<script>alert(${Math.random()})<\/script>`);
Here, a script is dynamically constructed and written into the document in a way that makes its contents impossible to determine in advance.
serviceWorker.register(URL.createObjectURL(new Blob([`self.x=${Math.random()}`])));
Similarly, arbitrary blobs can be constructed at runtime and used to instantiate service workers, allowing dynamically generated code to execute outside the original document context.
All these three cases would break the reproducibility goals that WEBCAT is designed to enforce. Luckily, Content Security Policy comes to our rescue. While CSP was not originally designed with this specific usage in mind, its security goal of disallowing certain JavaScript execution sources or methods, while allowing others, can make it a powerful tool for enforcing these constraints.
Designing a CSP sandbox
Content Security Policy provides fine-grained controls over a wide range of web features, supports different types of origins, and in some cases includes integrity-related metadata. From a sandboxing perspective, an apparently simple solution would be to enforce a highly restrictive CSP that is sufficiently constrained to be safe in all cases. However, doing so would partially defeat our commitment to supporting existing open source web applications, as it would require them to weaken or substantially modify their current security posture, rather than making only the minimal changes necessary to be compatible with the sandbox.
CSP directives
The WEBCAT approach here is to provide CSP constraints that are only as strict as necessary to achieve the objective, and no stricter. Let’s consider a few examples:
- The
script-srcdirective restricts the origins from which JavaScript may be executed; as expected,unsafe-inlineis disallowed. In current browsers,wasm-unsafe-evalis the only mechanism available to permit WebAssembly execution at all. Consequently, WebAssembly modules are treated as opaque data until they are explicitly compiled or instantiated via JavaScript APIs. CSP cannot impose fine-grained constraints on individual modules or origins, only on whether runtime compilation is allowed at all. To address this, WEBCAT injects JavaScript hooks to intercept WebAssembly compilation and instantiation functions, and verifies the module bytes against the manifest before allowing execution. While this approach is constrained by browser limitations and somewhat inelegant, it is functional. - Similarly, the
worker-srcdirective is subject to restrictions. - The
style-srcdirective follows a comparable model, although it is enforced slightly less strictly in order to preserve compatibility with existing applications.
CSP policies have a hierarchical structure. The default-src directive can be overridden by script-src, which in turn can be overridden by script-src-elem. As a result, any parser validating whether a policy satisfies the sandbox rules must thoroughly account for these inheritance and override relationships.
Origins and cross-domain isolation
Most CSP keywords allow source expressions, such as wildcards, schemes, or specific hosts. Forbidding these entirely would break many existing web applications, and there are legitimate uses for them.
CryptPad, for example, relies on iframe sandboxing; the main application logic runs inside a sandboxed iframe, while users’ private data remains in the parent frame. Strong isolation is achieved by leveraging the browser’s same-origin policy. When the sandboxed component is loaded from a different origin, it can only communicate with its parent via message passing.
WEBCAT supports remote origins for iframes, scripts, workers, and styles by validating — at manifest parsing time — that any referenced origin is also enrolled in the WEBCAT system. This is a local check against the domain list shipped with the WEBCAT browser extension. Each such origin is therefore expected to have its own transparently logged manifest, allowing an auditor to reconstruct interactions across domains.
For all the non-executable assets, such as images or media, WEBCAT imposes no origin restrictions. This allows loading resources from content delivery networks or storing user data separately from the frontend.
For more information, see the developers’ documentation. These guidelines may evolve as we audit and improve the sandbox. For example, machine checking its constraints is something we’d like to work on in 2026.
Sometimes, data is as important as code
So far, we have mostly focused on verifying code and markup, but other assets are equally critical to a web application’s security. Consider a user interface: It consists not only of layout, styling, and executable logic, but also of text, images, fonts, and other media. It is difficult to imagine a meaningful interface that does not rely on at least some of these elements. If such assets are not verified, they become an attack surface. A maliciously swapped font could alter or obscure text. Images could be replaced to mislead users, for example, by turning an error indicator into a success indicator in a contact-verification workflow of a messaging application. Even static text can be manipulated to convey arbitrary or deceptive messages. In short, integrity failures in non-code assets are sufficient to undermine user trust and application security.
However, many applications necessarily rely on a mix of trusted assets and less-trusted or user-generated content. In a messaging application, for example, this includes users’ avatars or shared attachments. To accommodate this in WEBCAT, we adopt the following split:
- Markup and executable code are blocked unless they are present in, and verified against, the manifest.
- For all other assets, WEBCAT first attempts to find a matching entry in the manifest:
- If a match exists, the asset is integrity-checked.
- If not, it is streamed through without additional verification.
This approach does not prevent developers from using untrusted media in core parts of their applications, but it offers a safer alternative for projects that want stronger guarantees, including even font files or binary objects, such as static downloads.
WEBCAT response validation logic, once a manifest has been successfully verified and loaded.
Our current proposal
Putting these pieces together, a WEBCAT manifest currently has the following fields:
{
"app":"https://github.com/myusername/myapp",
"version":"0.2.1",
"default_csp":"default-src 'self'; script-src 'self'; style-src 'self'; frame-src 'none';",
"files":{
"/index.html":"gVrKmd85flDlMrihlu4Tx6bROCAp-xhELjmQ3qUgx1M",
"/error.html":"JBqm-rCfcDVvHJjzvmVBV0hM-iSQy8GQmMYB-DuUx5A",
"/css/font.css":"dbWS84-I4LYXRfbUzniUoH7GAGbLAy6uo--oHD9AqlQ",
"/fonts/bagnard.regular.woff2":"yaNWBOi_jaRgaIoAytj5OUhAMnlEFoCfrHsbb87DLrg",
"/js/import.js":"JX8MgluhffaPJt2ondbRbbUWlOYn2Lf_oLovB3lrw38",
"/workers/worker.js":"DRQSRx5axe128HXncK_xBNwko4SrbBN2vtXRKbbLLxg"
},
"default_index":"index.html",
"default_fallback":"/error.html",
"wasm":[
"-ClDvgkF7OGRzC9DM4YeKEbULD7GSn6iTfIaMi8bMBM",
"cPVvAA_pOfMwc21s9hnE13kRLd46XE3Ci5_Tb1osoP4"
],
"extra_csp":{
"/workers/worker.js":"default-src: none; script-src 'wasm-unsafe-eval';"
}
}
- app and version should point to a Git repository and the Git tag corresponding to this manifest. Ideally, an auditor should be able to reproduce a manifest by cloning the repository, checking out the same tag, and then running a standardized build command in a standardized environment. This cannot be enforced at runtime, but open-source projects with some visibility should have auditors who reproduce every release automatically. In the future, we might explore adding support for attestations (such as GitHub attestations) directly.
- default-csp is the Content Security Policy string expected by default for all paths, unless there are path-specific overrides in extra-csp. All CSPs must satisfy the criteria we outlined earlier, and they are verified at manifest validation time. An invalid CSP in the manifest, or a mismatch with the CSP served by the server, will trigger an integrity error. Any extra CSP is specified per path and applied using longest-prefix matching; the most specific path override takes precedence. We learned of the requirements of some web applications to use different CSPs for different paths, thanks to community feedback.
- files is a map of path → SHA-256 hash of the file contents. Enforcement follows the criteria outlined here; the manifest always has priority, meaning that all listed files are verified, and any mismatch at any time triggers an integrity error.
- wasm is an array of hashes of all WebAssembly blobs that may be executed. There are practical implementation reasons for using an array rather than a map, most notably the limitations imposed by wasm-unsafe-eval in the CSP. Because WebAssembly is always instantiated from byte strings, integrity verification happens at instantiation time rather than at fetch time, unlike other resources. This distinction is necessary because a resource could be fetched with a benign content type (e.g., an image) and later evaluated as WebAssembly; in such cases, network-level validation would not be sufficient.
- default-index should mirror what is configured on the web server and is used to map directory roots to paths in the manifest.
- default-fallback should point to a standard error page or to a server-side catchall for invalid paths. This helps prevent integrity errors when a user visits a non-existent page or when the server returns an error. For example, if a project relies on the web server’s default error pages, those pages should either be included in the manifest or mapped to application-provided error pages that are specified in the manifest. Projects like Jitsi, which uses URL rewrites for meeting room names (rather than URL fragments), should instead point default-fallback to their index page, allowing it to function as a catchall.
While our specifications and research are still in progress, we believe this is a solid base to enforce proper reproducibility.
Limitations
There are a few shortcomings. First, while we tried to require the minimum possible amount of changes from existing projects, some will work out of the box, while others will still require additional plumbing.
There are also intrinsic limitations. For instance, by committing to a specific CSP, manifest reuse across domains (e.g., for self-hosting) can be difficult. While GlobaLeaks uses a sandboxed iframe generated from a data: URL, CryptPad instead relies on a dedicated subdomain. However, wildcards for iframes are currently forbidden, and the only alternative is to specify the full URL. This, in turn, requires changing the CSP per domain, making manifests less portable.
Furthermore, WebAssembly enforcement remains imperfect. A more expressive CSP — ideally with a directive such as wasm-src, structured similarly to script-src — would greatly simplify our implementation and significantly strengthen the robustness of the sandbox.
There are a few additional nuances and runtime constraints that we enforce. Some stem from our own limitations. For example, browsers support multiple Content Security Policy headers and merge them by applying the most restrictive combination. Replicating this logic would add significant complexity, so we currently support only a single CSP header at a time. Furthermore, there are some lesser-known headers — such as the Link header — that can have unexpected effects. As far as we know, their use is not critical for typical web applications, so we choose to forbid them. Redirects are also tricky. An application may have legitimate reasons to perform redirects, and client-side code is still allowed to do so. Server-controlled redirects, however, implemented via HTTP headers, are forbidden unless they are relative and remain within the same origin.
Lastly, some commonly used services, such as Google’s reCaptcha or Cloudflare Turnstile, provide their code functionality by injecting opaque and obfuscated JavaScript directly into a web application. These blobs are not open source, and not versioned, and could change anytime. While it is technically possible to predownload the blob and include it in the manifest, the chances of it breaking later on are pretty high; the provider might change the code, and the embedded one might go out of sync and no longer work. In general, the idea of third parties injecting code into your application is against the safeguards that WEBCAT tries to build. However, we acknowledge that for many projects, these integrations are currently critical. We are exploring whether we can allow less-trusted iframes for sandboxing these uses, and then rely on message passing.
What’s next
We are working to release a testing version very soon. In the meantime, you can follow our specifications and extension development. If you are an open-source developer and would like to test your project or make it compatible, feel free to open an issue or get in touch.
We are thankful to all the projects that collaborated so far!