How to detect vulnerable JavaScript libraries on a live website
CentralCSP Team ·
Last update:
Your build pipeline runs npm audit, your repository has a dependency scanner, and the last pentest still came back with "vulnerable version of the library jQuery found". The scanner was right. The library was not in your source tree. It arrived through a vendor widget, a tag manager container, or a CDN URL somebody pinned in a template years ago, and the only place it ever shows up is in the browser.
This post covers how to detect the vulnerable JavaScript libraries a live site loads, why build-time tools miss a share of them, and how to turn a one-off check into something that tells you when the next CVE lands.
Why vulnerable JavaScript libraries are still everywhere
Outdated client-side libraries are one of the oldest findings in web security and one of the most persistent. A crawl of over 133,000 websites, published at NDSS in 2017, found that 37% of them included at least one library version with a known vulnerability, with a lag behind the current release measured in years, and that many of those inclusions came from third-party services and dynamically injected scripts rather than from the site's own code. The mechanism has not changed since.
Three things keep the number high:
- Nobody owns the third-party copy. A chat widget, an A/B testing tag, or an ad script ships its own copy of a library. Your team never chose the version and never sees it in a lockfile.
- Old URLs keep working. A CDN copy pinned in a template in 2018 loads exactly as well today. Nothing breaks, so nothing prompts an upgrade.
- Scanners rate it low. Pentest tools typically classify an outdated library as an informational finding, so it sits at the bottom of the report until an exploitable CVE turns it into an incident.
How attackers exploit a vulnerable library
A vulnerable library is not a vulnerability by itself. It is a shortcut for one. The common pattern is a library function that trusts its input, combined with any place on your page where attacker-controlled data reaches that function.
The jQuery XSS advisories from 2020 are the textbook case. Passing HTML from an untrusted source to a DOM manipulation method such as .html() or .append() could execute script on versions before 3.5.0, even after the input had been sanitized. A search box that reflects its query into the page through one of those methods is enough. The jQuery version ranges and the fix are covered in Which jQuery versions are vulnerable.
Prototype pollution is the other frequent class. A deep-merge helper that copies keys from user-controlled JSON without checking for __proto__ lets an attacker add properties to every object on the page, and from there reach code paths that were never meant to see that data. Lodash and older jQuery both had advisories of this kind.
In both cases the exploit needs the vulnerable code to be loaded on a page where untrusted input flows into it. That is why knowing which page carries which library matters as much as the version number.
How to check a website for vulnerable JavaScript libraries today
For a single page, three manual checks answer most questions:
- Open the browser console and ask the library for its version. For jQuery that is
jQuery.fn.jquery. Other libraries expose aVERSIONorversionproperty on their global. - Open the DevTools Network panel, filter on JS, and read the script URLs. A versioned filename or CDN path names the version outright.
- Run the Retire.js command-line scanner over the script files you downloaded or over your build output. It matches file contents and filenames against its database of known vulnerable versions.
npx retire --path ./distTwo tools people still reach for no longer work the way they remember. The Lighthouse audit that flagged front-end libraries with known vulnerabilities was removed in Lighthouse 10, so a green Lighthouse report says nothing about library CVEs. The Retire.js browser extensions are no longer officially distributed; the command-line tool is the maintained part, and it scans local files, not a URL.
Manual checks are fine for one page on one day. The problem is that a site is many pages, the set of scripts changes with every deployment and every tag manager publish, and the library that was safe on Monday can have a CVE on Friday.
What npm audit, Snyk, and Retire.js cannot see
Build-time scanners read what is in your repository or your build output. They are good at that. They do not see:
- Scripts a tag manager injects after the build. A container publish adds a library to every page without a commit.
- Third-party tags and their bundled copies. The vendor's script, and whatever it loads, never passes through your pipeline.
- CDN copies referenced from templates or a CMS. A
<script src="https://cdn...">line is not a dependency in any manifest. - Which page carries what. A repository scan says a library exists somewhere. It cannot tell you whether it loads on the checkout page.
- A file changing at a stable URL. A vendor updates, or an attacker replaces, the content behind a URL you pinned. The manifest still says the same thing.
Script monitoring products cover some of this by recording which script URLs load on which pages, but recording a URL is not the same as knowing the library and version behind it, or whether an advisory covers it. What closes the gap is software composition analysis (SCA) run against what the browser actually executed, at runtime, rather than against the source tree.
Detect what browsers actually load with CSP hash reporting
The browser is the one place that sees the complete list of scripts a page runs, and Content Security Policy (CSP) can make it report that list. The 'report-sha256' source expression asks the browser to compute a hash of every script it fetches for the page and send a csp-hash report to your endpoint. It reports, it never blocks, so it is safe in an enforced policy.
Two headers are enough:
Reporting-Endpoints: default="https://MyEndpoint.report.centralcsp.com"Content-Security-Policy: script-src 'self' 'report-sha256'; report-to defaultEach report carries the document URL, the script URL, and the script's hash, which is what the CSP hashes page shows raw. The keywords themselves are covered in CSP report-sha keywords explained. That hash identifies the exact file content, so the same bytes served from three different CDNs resolve to the same library version, and a modified file at a familiar URL stands out as a different hash.
From there the analysis is mechanical. For each script browsers report, the Technologies engine in CentralCSP identifies the library, its version, its lifecycle status, and the published advisories that affect it. You get one row per library version, its lifecycle status (up to date, outdated, dormant, deprecated), the advisories that affect it with their CVE IDs, severity, and affected range, and the list of script URLs that carry it, so you know whether the fix is a bundle upgrade, a template edit, or an email to a vendor.
There is no agent to install and no crawler to schedule, because real visitors' browsers do the discovery. The inventory fills at the pace pages are actually loaded, and it includes every third-party script those pages pulled in. One honest limit: inline scripts are not part of the inventory. The feature is included in the Scale and Enterprise plans; the pricing page has the comparison, and the supply-chain feature page has the tour.
From detection to alerting
A vulnerability scan is a snapshot. The advisory that matters is the one published next month for a library you already run, and nobody re-runs a scan on the day a CVE drops.
Two alert rules turn the inventory into a standing check:
- New vulnerability fires when an advisory is published for a version already on the site, or when a vulnerable version is loaded for the first time. A minimum severity keeps Low findings out of the pager.
- Outdated or deprecated version fires when a library falls behind its current release or stops being maintained, which is the earlier, quieter signal that a CVE is coming with no fix behind it.
Both rules look forward from the moment you create them. Review the inventory once, then let the rules watch it. The Technologies get started guide walks through that order, and alerting delivers to Slack, Teams, Google Chat, Telegram, email, or a signed webhook.
Runtime detection or build-time scanning? Use both
The two approaches answer different questions, and neither replaces the other.
Build-time scanning (npm audit, Snyk, Retire.js) | Runtime detection (CSP hash reporting) | |
|---|---|---|
| What it sees | Dependencies declared in your manifests and files in your build output | Every script file real browsers executed, first-party and third-party |
| When it runs | On commit, on CI, on demand | Continuously, as pages are loaded |
| What it misses | Tag manager injections, vendor tags, CDN copies, per-page reality | Inline scripts, and pages no visitor has loaded yet |
| Best at | Blocking a bad version before it ships | Finding what shipped anyway, and catching the next advisory |
| Cost to set up | A CI step | One CSP source expression and a reporting endpoint |
A finding in either place still needs a reachability check before it becomes a priority. A vulnerable .html() on a page with no untrusted input is a backlog item, not an incident. The version and the page it loads on are what let you make that call quickly.
FAQ
How do I know if my website uses vulnerable JavaScript libraries?
Open the DevTools Network panel, list the JavaScript files each page loads, and check each library's version against its advisories, or run Retire.js over the downloaded files. For a whole site, continuously, use CSP hash reporting so browsers report every script they run and the versions are matched against CVEs for you.
Does Lighthouse still check for vulnerable JavaScript libraries?
No. The audit that reported "Includes front-end JavaScript libraries with known security vulnerabilities" was removed in Lighthouse 10, so a clean Lighthouse run does not mean your libraries are current. Use a dedicated check such as Retire.js, or runtime detection built on CSP hash reports, instead.
Is Retire.js still maintained?
The Retire.js command-line scanner is maintained and remains a good way to check files in a repository or a build. Its browser extensions are no longer officially distributed. Retire.js scans files you give it, so it does not see scripts a tag manager or a vendor adds to your pages at runtime.
What is a client-side SBOM?
A client-side software bill of materials is the list of libraries and versions that actually run in your visitors' browsers, including third-party and CDN scripts, rather than the list declared in your package manifest. It is built from what browsers report loading, and it is what you check when a new CVE is published. Technologies is one.
Start with what the browser sees
Add 'report-sha256' to your script directive, point report-to at an endpoint, and let real page loads build the list. Then review the vulnerable rows once and set the two alert rules so the next advisory reaches a person. Start free with CentralCSP and the inventory starts filling with the first reports.
Related
- Which jQuery versions are vulnerable, and how to find the copy your site loads
- Build a script inventory with CSP hash reporting
- How attackers abuse Google Tag Manager
- What Magecart and formjacking are