Skip to main content

Ghost Case Study

Tested with CVE Lite CLI v1.16.0

Ghost logo

Summary

  • Project: Ghost — open source publishing platform powering millions of blogs, newsletters, and membership sites worldwide
  • Revision: 359e702345304c6328041eb8654e9ea838f7df5f
  • Lockfile: pnpm-lock.yaml (4,447 resolved packages)
  • Baseline findings: 26 unique vulnerable packages (2 critical · 16 high · 7 medium · 1 low)
  • Direct vs transitive: 0 direct / 26 transitive
  • Validated copy-and-run fix commands: 0 — all 26 findings are transitive; remediation runs through Ghost's own internal packages and upstream toolchain
  • Packages with no known fix: 3 (sanitize-html, html-minifier, elliptic)
  • Automated dependency management: Ghost uses Renovate — yet 26 vulnerable packages remain at this revision

What this case study demonstrates

Ghost is a professionally maintained publishing platform with a dedicated security team and active release cadence. Yet a single lockfile scan reveals 26 vulnerable packages — every one of them transitive, hidden beneath layers of admin UI frameworks, build toolchain, and legacy infrastructure.

The two most critical findings tell the story of why transitive risk is the hardest class of vulnerability to manage:

[email protected] — critical XSS. Ghost uses sanitize-html to clean HTML submitted by editors and members before rendering it to readers. A critical XSS vulnerability in the library meant to make user content safe is precisely the kind of structural risk that a flat advisory list obscures. CVE Lite flags it immediately as critical with no known fix — telling you before you spend time looking that there is currently no version to upgrade to.

[email protected] — critical arbitrary code execution. Six dependency layers deep in Ghost Admin's build toolchain: @tryghost/ember-promise-modalsember-auto-importbabel-corebabel-traverse. An ancient Babel 6.x package from 2017, carrying a critical code execution CVE, present in every installation. A developer reading Ghost's package.json would never find it. A lockfile scanner does.

CVE Lite's direct/transitive split makes the remediation landscape immediately legible: 0 direct findings means there is nothing to fix with a simple pnpm add. Every issue runs through a parent chain. Knowing this early prevents wasted effort on pnpm audit fix and points the remediation work toward Ghost's internal package releases and upstream toolchain updates.

What Renovate couldn't fix. Ghost uses Renovate — a widely adopted automated dependency update bot that monitors the repository and opens PRs when newer package versions are available. It is one of the most sophisticated automation tools in the JS ecosystem. Yet at this revision, 26 vulnerable packages remain. Renovate cannot resolve what it cannot install:

  • No version to suggest: sanitize-html, html-minifier, elliptic, and express-brute have no published non-vulnerable version. Renovate can only open PRs for versions that exist.
  • Breaking changes that stall: knex needs to move from 0.x to 2.4.0 — a major version bump with API-breaking changes. Renovate can open the PR, but it cannot auto-merge a breaking change. Those PRs frequently sit open for months while the vulnerability remains active.
  • Transitive chains outside Renovate's reach: [email protected] is buried six layers deep inside ember-auto-import's Babel 6 dependency chain — a package Ghost does not directly control. Renovate's scope ends at Ghost's direct dependencies. Lockfile scanning sees the full resolved tree regardless of depth.

A project can be doing everything right with automated update tooling and still carry material vulnerability risk. CVE Lite CLI surfaces that residual surface — and tells you which category each finding falls into.


Comparison Note: CVE Lite CLI vs pnpm audit

Ghost uses pnpm. Both tools were run against the same pnpm-lock.yaml on the same machine.

Metricpnpm auditCVE Lite CLI v1.16.0
Total reported findings4426
Critical22
High2316
Moderate / Medium187
Low11
Direct vs transitive breakdown✓ (0 / 26)
Packages with no known fix flagged✓ (3 packages)
Priority ordering (criticals first)
Parent chain identifiedpartial (paths shown)
Validated fix targets
Specific copy-and-run commands✓ (0 in this case — all transitive)

Why CVE Lite reports fewer findings — and why that is not a coverage gap:

pnpm audit counts vulnerability paths, not packages. A single vulnerable package reached via three different dependency paths contributes three entries. CVE Lite counts each unique vulnerable package once regardless of how many paths reach it. That is why the totals differ: 44 vs 26.

This deduplication is intentional. A developer looking at 44 pnpm audit findings cannot tell how many distinct packages need attention. CVE Lite's 26 is the true exposure surface: 26 packages, each needing exactly one decision.

pnpm audit's output for sanitize-html:

critical Apostrophe has default XSS via `xmp` raw-text passthrough in `sanitize-html`
Package sanitize-html
Patched versions <0.0.0

CVE Lite's output:

Transitive dependency
Fix: ⚠ no fix — no non-vulnerable version currently available

Both flag the finding. CVE Lite's ⚠ no fix indicator surfaces immediately in the summary view and is not buried in per-package detail blocks. A developer scanning the output can triage "fixable now" from "wait for upstream" at a glance.


Before vs After

No remediation pass was performed for this study. CVE Lite correctly identified that all 26 findings are transitive with no confident first-pass fix commands — meaning the usual direct-fix workflow does not apply here.

StageFindingsCriticalHighMediumLowDirectTransitiveCommand groups
Baseline26216710260

This result is itself meaningful. When a scanner generates zero copy-and-run commands, it is telling you something important: the remediation work is not in your direct dependency surface. It runs through upstream package releases and parent-chain decisions that require different effort — tracking Ghost's own internal packages, watching for Babel 7 migration in the admin toolchain, and monitoring upstream projects for the three packages that have no fix available at all.

A tool that ignores this distinction and suggests pnpm audit fix regardless would send a developer down a path that either fails silently or introduces breaking changes without reducing the underlying vulnerability count.


Fix Journey

Ghost's vulnerability surface is entirely in the transitive layer. The scanner's job here is not to generate install commands — it is to surface the nature of the risk clearly enough that the developer knows not to reach for pnpm audit fix.

The two critical findings follow different remediation paths:

[email protected]: The dependency chain runs through @tryghost/ember-promise-modalsember-auto-importbabel-core. The fix requires ember-auto-import to migrate from Babel 6 to Babel 7 — a change that is controlled by the ember-auto-import maintainers, not by Ghost directly. CVE Lite surfaces this as: "Upgrade babel-plugin-transform-class-properties — check for a release resolving babel-traverse to 7.23.2+". This tells you where to look, not just what the problem is.

[email protected]: No fix available. The advisory (GHSA-rpr9-rxv7-x643) reflects an XSS vulnerability via <xmp> raw-text passthrough that had no published non-vulnerable version at scan time. CVE Lite flags this explicitly with ⚠ no fix rather than leaving the developer to discover during manual triage that there is nothing to install.

For [email protected] and [email protected] (SQL injection, high severity), the fix version is 2.4.0 — a major version bump with breaking changes. In a production CMS, this upgrade requires careful coordination with everything in Ghost core that depends on knex.

The correct response to this scan is not an install command. It is a ticket to track: three packages with no fix available to watch for upstream releases, one Babel toolchain migration to follow in ember-auto-import, and a knex major-version upgrade to plan for the Ghost core team.


Why this matters

Ghost is not a neglected project. It has a dedicated security team, a published Security Policy, and a track record of responsible disclosure. Yet a single lockfile scan of its pnpm workspace surfaces 26 vulnerable packages, including a critical XSS vulnerability in the library responsible for making user content safe.

This is not a failure of Ghost's security practices. It is an illustration of the nature of transitive risk in large JavaScript applications. The packages involved are not things Ghost's team chose to add — they are downstream of admin UI frameworks, build systems, and legacy tooling that shipped with different security assumptions.

The finding that matters most here is not any individual CVE. It is the shape of the result: zero direct vulnerabilities, 26 transitive ones. That means the risk is invisible to anyone who only reads the project's package.json. It is invisible to tools that scan manifest files rather than lockfiles. It is only visible when you scan the full resolved dependency tree — which is exactly what CVE Lite does.

For Ghost's 4,447 resolved packages, the lockfile is the ground truth. And the ground truth has 26 vulnerable packages that a developer reading the source code would never find.


Scan command

Run from the root of a Ghost checkout or from the examples/ghost directory in this repository:

cve-lite . --verbose --all

The example lockfile in this repository reflects Ghost at revision 359e702345304c6328041eb8654e9ea838f7df5f. Ghost releases frequently — running against a more recent checkout may show a different finding count.


Remaining risk

All 26 baseline findings remain open at the time of this study. No remediation was applied.

  • 2 critical: [email protected] (code execution), [email protected] (XSS, no fix)
  • 16 high: including knex (SQL injection, 2 versions), jsonwebtoken (auth), @tryghost/members-csv (CSV injection), validator (2 versions), protobufjs (2 advisories), rollup (XSS, 2 versions), serialize-javascript (2 versions), lodash.template, lodash.pick, html-minifier (no fix), fast-uri
  • 7 medium: markdown-it, file-type, postcss (2 versions), request, express-brute (no fix), @protobufjs/utf8
  • 1 low: elliptic (no fix)

Baseline findings

Full vulnerable package list at scan time (revision 359e702):

PackageVersionSeverityRelationshipFix hintAdvisory IDs
babel-traverse6.26.0criticaltransitive7.23.2GHSA-67hx-6x53-jw92
sanitize-html2.17.0criticaltransitive⚠ no fixGHSA-rpr9-rxv7-x643
@babel/plugin-transform-modules-systemjs7.29.0hightransitive7.29.4GHSA-fv7c-fp4j-7gwp
@tryghost/members-csv2.0.7hightransitive5.82.0GHSA-xgwh-cgv9-783v
fast-uri3.1.0hightransitive3.1.1GHSA-q3j6-qgpj-74h6, GHSA-v39h-62p7-jpjc
html-minifier4.0.0hightransitive⚠ no fixGHSA-pfq8-rq6v-vf5m
jsonwebtoken8.5.1hightransitive9.0.0GHSA-8cf7-32gw-wr33, GHSA-hjrf-2m68-5957
knex0.20.15hightransitive2.4.0GHSA-4jv9-3563-23j3
knex0.21.21hightransitive2.4.0GHSA-4jv9-3563-23j3
lodash.pick4.4.0hightransitive4.17.19GHSA-p6mc-m468-83gw
lodash.template4.5.0hightransitive4.17.21GHSA-35jh-r3h4-6jhm, GHSA-r5fr-rjxr-66jc
protobufjs7.5.5hightransitive1.1.1GHSA-2pr8-phx7-x9h3, GHSA-66ff-xgx4-vch8
rollup0.57.1hightransitive2.79.2GHSA-gcx4-mw62-g8wm, GHSA-mw96-cpmx-2vgc
rollup1.32.1hightransitive2.79.2GHSA-gcx4-mw62-g8wm, GHSA-mw96-cpmx-2vgc
serialize-javascript4.0.0hightransitive7.0.3GHSA-5c6j-r48x-rmvq, GHSA-qj8w-gfj5-8c6v
serialize-javascript6.0.2hightransitive7.0.3GHSA-5c6j-r48x-rmvq, GHSA-qj8w-gfj5-8c6v
validator13.12.0hightransitive13.15.20GHSA-9965-vmph-33xx, GHSA-vghf-hv5q-vc2g
validator7.2.0hightransitive13.7.0GHSA-9965-vmph-33xx, GHSA-qgmg-gppg-76gx
@protobufjs/utf81.1.0mediumtransitive1.1.1GHSA-q6x5-8v7m-xcrf
express-brute1.0.1mediumtransitive⚠ no fixGHSA-984p-xq9m-4rjw
file-type16.5.4mediumtransitive21.3.1GHSA-5v7r-6r5c-r473
markdown-it8.4.2mediumtransitive12.3.2GHSA-6vfc-qv3f-vr6c
postcss7.0.39mediumtransitive8.4.31GHSA-7fh5-64p2-3v2j, GHSA-qx2v-qp2m-jg93
postcss8.5.6mediumtransitive8.5.10GHSA-qx2v-qp2m-jg93
request2.88.2mediumtransitive3.0.0GHSA-p8p7-x288-28g6
elliptic6.6.1lowtransitive⚠ no fixGHSA-848j-6mx2-7j84

Want your project reviewed?

If you maintain an interesting JavaScript or TypeScript project and want CVE Lite CLI considered for a public case study, open an issue in the CVE Lite CLI repository.

Please include:

  • the repository link
  • why the project would make a useful case study
  • whether the dependency graph is publicly reproducible

Not every project will be selected. Preference will go to projects that are publicly useful, technically interesting, and strong examples of realistic dependency remediation workflows.