Key Takeaway: Merging a security pull request in GitHub does not mean your users are safe. Between package manager quirks, multi-tier CDNs, aggressive browser caching, Service Workers, or someone simply forgetting to hit the deploy button, there is a substantial gap between what your repository declares and what actually executes in the browser. It represents a critical blind spot in client-side software supply chain security. Vioro is building outside-in JavaScript detection to verify what is actually running on the wire.
I have spent more than 20 years building web software and breaking systems as a chaos engineer. If there is one fundamental lesson that production outages teach you, it is that what is written in your repository rarely matches what is currently executing in production.
We have all been through the Tuesday morning routine: Dependabot, Renovate, or Snyk flags an automated alert. A utility library in your frontend stack has a known vulnerability. You bump the version or run npm audit fix, the tests pass in CI, you merge the pull request to main, and the security dashboard turns green.
In fact, while pushing the first commit of this very article, our own CI pipeline failed with a bright red build on GitHub. An automated audit check caught several transitive packages that had flagged security advisories requiring an npm audit fix. The irony was impossible to miss. While writing about the fragile relationship between manifests, lockfiles, and deployments, reality intervened in real time. We ran npm audit fix, updated the lockfile, and restored the green checkmark.
Everyone breathes a sigh of relief. The issue is marked resolved.
Except it probably is not.
While your git repository is clean, the JavaScript running inside your users’ browsers right now might still be the unpatched bundle from three weeks ago. Between your source code and your users lies a maze of bundlers, edge proxies, CDN caches, and local browser storage.
We cannot know what is running without looking. And until now, almost nobody in web security has been looking at client-side software supply chains from the outside.
What package.json and package-lock.json actually tell you
Before digging into why production drifts from source control, it helps to be clear about what dependency files actually do. Many teams treat them as interchangeable boilerplate, but they solve two different problems.
flowchart TD
subgraph Repo["Git Repository"]
A["package.json<br/><b>Developer Intent</b> (Semver Ranges)"]
B["package-lock.json<br/><b>Deterministic Graph</b> (Exact Versions + Hashes)"]
A -->|"Defines constraints"| B
end
subgraph CI["Build Pipeline"]
C["npm ci<br/><b>Enforces Lockfile</b> (Strict reproducible build)"]
D["npm install<br/><b>Mutates Lockfile</b> (Risk of silent drift)"]
end
B -->|"Builds reproducible artifact"| C
A -.->|"Risk of drift if run in CI"| D
How package.json defines version ranges
package.json is a human-readable manifest of your project. Crucially, it rarely specifies exact versions for libraries. Instead, it relies on semantic versioning ranges:
- Caret (
^1.2.3): Allows updates that do not modify the left-most non-zero digit (for example,1.3.0or1.9.1). - Tilde (
~1.2.3): Restricts updates strictly to patch releases (such as1.2.4, but not1.3.0).
package.json documents your intent, what you are willing to accept, rather than what is actually installed. It should only change when you intentionally add a dependency, drop an unused library, or bump a major framework version.
Pinning exact dependency trees with package-lock.json
package-lock.json (or pnpm-lock.yaml, yarn.lock) is generated by the package manager. It pins the entire dependency tree at a specific moment in time:
- Exact versions: It locks every direct library and every transitive dependency (which typically make up 90% to 95% of your total dependencies).
- Cryptographic integrity: It records the exact SHA-512 hash for each package tarball to verify that downloads have not been tampered with.
- Deterministic installs: It guarantees that two developers, or your CI build runner, get the exact same bits on disk.
The lockfile updates whenever dependencies are resolved or upgraded (npm install, npm update, or automated PRs from Dependabot and Renovate).
Why npm install behaves differently than npm ci in build pipelines
One common failure mode in deployment pipelines happens when build scripts run npm install instead of npm ci.
If your lockfile is slightly out of sync with package.json, npm install will happily recalculate dependencies on the fly, downloading newer transitive releases that satisfy your ranges. In contrast, npm ci strictly validates the lockfile and immediately aborts if there is any mismatch.
Yet even when you follow strict npm ci hygiene and merge clean lockfiles into main, production reality often diverges the moment code leaves your machine.
How repository code gets transformed into production bundles
Modern frontend frameworks do not serve the code you wrote in your editor. Source code written in TypeScript, JSX, or modern ECMAScript gets fed into bundlers like Vite, Webpack, esbuild, or Rolldown:
- Tree-shaking: Unreferenced exports and dead code are pruned.
- Minification and mangling: Variable names, function identifiers, and class structures are renamed to single characters (
function a(b, c){...}). - Chunk splitting: Code from dozens of different npm packages is sliced up and merged into shared vendor chunks (
chunk-vendor-[hash].js).
The final bundle that lands on the web server bears almost no resemblance to node_modules. But before we even look at the generated assets, there is a much simpler question that teams frequently forget to ask.
When deployments are quietly forgotten or delayed
In many engineering teams, Continuous Integration (CI) and Continuous Deployment (CD) are separate systems.
A security patch merged into main often sits waiting because:
- The CI pipeline passed tests, but the release is gated behind a manual approval step that someone forgot to click.
- The change was deployed to a staging environment, but the production promotion was postponed until a sprint release next Thursday.
- A deployment webhook or token expired silently, leaving the existing container or S3 bucket serving old assets while CI reported success.
Security scanners that only read repository manifests will happily report that your repository is clean, even when production has not been touched in weeks.
How long old artifacts linger in CDNs, proxies, and browser caches
Even when code is deployed immediately, the web infrastructure is specifically architected to prevent clients from fetching new files unless forced to do so. Multi-tier caching delivers fast page loads, but it also creates a persistent survival habitat for outdated, vulnerable code.
flowchart TD
subgraph Origin["1. Origin Deployment"]
Deploy["New Release Deployed<br/>app.new-hash.js"]
end
subgraph CDN["2. Multi-Tier CDN Edge"]
OldAsset["Old Chunks Survive<br/>app.old-hash.js<br/>(Cache-Control: immutable)"]
StaleHTML["Stale index.html<br/>Edge TTL: 1h - 24h"]
end
subgraph Client["3. Client Browser"]
SW["Service Worker CacheStorage<br/>Survives Days / Weeks"]
BrowserCache["HTTP Browser Cache"]
Runtime["Active Browser Execution"]
end
Deploy --> OldAsset
OldAsset --> StaleHTML
StaleHTML --> SW
SW --> BrowserCache
BrowserCache --> Runtime
Content-hashed assets never expire on their own
To allow aggressive caching, modern bundlers generate content-hashed filenames (for example, vendor.d41d8c.js). Standard production guidelines recommend serving these static assets with long cache lifetimes:
Cache-Control: public, max-age=31536000, immutable
This header tells edge CDNs (Cloudflare, CloudFront, Fastly) and user browsers to cache the asset for an entire year without revalidating. When you deploy an updated build, vendor.d41d8c.js is not overwritten on the CDN. Instead, a new file, vendor.e9b3a1.js, is uploaded alongside it.
The old file remains cached across CDN edge nodes indefinitely until it is evicted by LRU (Least Recently Used) cache turnover, which can take weeks on high-capacity edge servers. As long as any client requests that URL, the CDN will keep serving it.
Caching the HTML entrypoint that points to old bundles
The entire cache-busting system relies on index.html. When the browser loads the latest HTML document, it discovers the new script filenames. But what happens if index.html itself gets cached?
- Edge TTL misconfigurations: If your CDN or reverse proxy is configured to cache HTML responses (even for an hour or two), users hitting that edge location will receive the old HTML file containing references to the old, vulnerable bundles.
- Intermediate corporate proxies: Many corporate network firewalls and proxy caches override cache-control headers to save egress bandwidth, holding onto HTML entrypoints longer than specified.
- CDN purge latency: Edge cache purges are asynchronous. An invalidation call can take anywhere from a few seconds to over an hour to propagate to every Point of Presence across the globe.
During that window, users continue downloading the old HTML file, which in turn commands the browser to download the old, vulnerable JavaScript chunks.
Service workers refusing to fetch new application bundles
For Progressive Web Apps and Single Page Applications, Service Workers represent the hardest caching layer to dislodge.
When a Service Worker uses the CacheStorage API to cache application bundles for offline support, it intercepts network requests before they reach the browser’s HTTP cache or the network:
- Unless your Service Worker includes aggressive lifecycle management (
self.skipWaiting()and client claiming), a newly installed service worker waits in the background until every open tab of your application has been closed. - If users keep tabs open on their laptops for days or weeks (typical for dashboards and productivity tools), they will continue running the old, cached JavaScript bundle without ever requesting the updated release.
Expected exposure windows from git merge to user execution
When you account for deployment lag, CDN cache propagation, entrypoint caching, and Service Worker lifecycles, the time it takes for a security fix to actually reach users is rarely instantaneous:
| Layer | Typical Convergence Lag | Worst-Case Exposure Window |
|---|---|---|
| Pipeline & Deployment | 5 to 30 minutes | Days (unexecuted deployment) |
| CDN Invalidation | 1 to 15 minutes | Hours (partial edge purge failures) |
Stale Entrypoint (index.html) | 10 minutes to 2 hours | 24+ hours (caching proxy misconfiguration) |
| Browser HTTP Cache | Instant (if hashed) | Days (if unhashed script tags are used) |
Service Worker (CacheStorage) | Hours (tab restart) | Weeks (persistent background tabs) |
Why traditional security tools gave up on scanning client-side JavaScript
This brings us to a noticeable blind spot in web application security.
Most organizations rely on two categories of security tools:
- Software Composition Analysis (SCA): Tools like Dependabot, Renovate, Snyk, and Mend inspect
package.jsonandpackage-lock.jsonin your repository. They evaluate declared dependencies at commit time. - DAST and WAFs: Dynamic Application Security Testing and Web Application Firewalls inspect HTTP request headers, response status codes, and server endpoints.
Neither category inspects what JavaScript actually executes inside the user’s browser. Attackers do not look at your package-lock.json. They open DevTools and look at the wire. Outside-in detection is the only posture that directly mirrors an actual attack.
Why has the security industry largely avoided scanning production JavaScript from the outside? Because analyzing production bundles has historically been considered too difficult and noisy:
- Minification and mangling: Bundlers rename meaningful variables and functions to single letters. Identifiers disappear, making simple string searches useless.
- Tree-shaking variations: Two web applications using the exact same version of a library will bundle completely different subsets of functions depending on which features they import. Whole-file checksums and file-size comparisons fail immediately.
- Monolithic vendor chunks: Bundlers concatenate dozens of distinct libraries into shared chunks without clean delimiters.
- Stripped source maps: Production builds strip source maps for performance and intellectual property reasons.
- High false-positive rates in legacy scanners: Early tools like Retire.js attempted to match regular expressions against minified scripts. On modern bundled applications, this approach triggered so many false alarms and missed so many libraries that Google Lighthouse removed the Retire.js audit completely in Lighthouse v10.
- Manifest bias: It was computationally cheap and straightforward for security vendors to parse JSON files in git repositories, so the industry stopped looking deeper.
The result is an uncomfortable gap: teams assume their frontend is secure because git is clean, while their CDN continues serving vulnerable code to users.
We cannot know without looking. It is a genuine blind spot in cybersecurity, and Vioro is here to address it.
Building outside-in detection for minified libraries
At Vioro, our core approach has always been outside-in verification: monitoring web applications exactly as an external observer or attacker sees them, without requiring server-side agents, local plugins, or private repository access.
We are currently developing Outside-In JavaScript Library Detection, built directly into VioroBot , our passive web crawler and scanning engine.
flowchart TD
subgraph Recon["1. Public Bundle Retrieval"]
A["Target Web Application"] -->|"Public HTTP GET"| B["VioroBot Scanner"]
B -->|"Extracts Public Scripts"| C["Minified & Tree-Shaken Bundles"]
end
subgraph Analysis["2. Outside-In Analysis & CVE Mapping"]
D["Vioro Outside-In Detection Engine"]
E["Identified Libraries & Exact Versions"]
F["Live CVE & Advisory Mapping"]
D --> E --> F
end
C --> D
classDef highlight fill:#0284c7,stroke:#38bdf8,stroke-width:2px,color:#fff;
class D,E,F highlight;
How outside-in verification works in practice
- Resilient to minification: Our detection engine recognizes library footprints even after variable renaming, code mangling, and bundler dead-code elimination.
- Zero repository access required: The scan operates purely on the public JavaScript assets delivered to the browser. No repository tokens, CI/CD integrations, or build plugins are needed.
- Continuous CDN and cache verification: We detect when an edge cache, proxy layer, or rollback serves an outdated or vulnerable library in production.
- Precision version mapping: The engine differentiates between patch and minor releases, allowing accurate mapping to published Common Vulnerabilities and Exposures (CVEs).
Moving from internal alpha to customer beta
We are currently running this technology in Alpha, validating detection accuracy across modern frontend frameworks and real-world bundle configurations.
Over the coming weeks, we will open this capability to Customer Beta for Vioro users, providing automated alerts whenever deployed client-side bundles contain outdated or vulnerable libraries.
Measuring real-world JavaScript lag across the web
In August 2026, Vioro published an empirical study analyzing 32,036 live WordPress domains . That research showed that 61.2% of scanned sites were running outdated or vulnerable plugins, with more than 20% stranded on unsupported legacy core releases. It highlighted how often production reality differs from what teams assume.
We are now preparing an equivalent large-scale experiment for the JavaScript ecosystem.
We plan to scan thousands of public web applications from the outside to measure:
- How many live web applications are actively serving client-side libraries with known CVEs.
- The average time lag between an npm security advisory release and real-world CDN convergence.
- How frequently stale entrypoints and caching layers keep vulnerable scripts alive after updates.
This study will test our detection capabilities across complex edge architectures while providing open, anonymized data to the engineering community.
Verifying what your users actually download
Your package.json documents what you intend to build. Your package-lock.json documents what your CI pipeline resolved.
Neither one tells you what is executing in your customer’s browser right now.
Until you verify your deployed assets from the outside, your frontend security posture is based on assumptions. Bridging the gap between repository manifests and runtime browser execution is the missing link in client-side software supply chain security.
Verify Your Website’s Real Security Posture
Vioro monitors your websites from the outside in: checking SSL/TLS certificates, security headers, uptime, and upcoming client-side library vulnerabilities around the clock.
Start 14-day trial