The File Name Is the Argument: Eighteen Command-Injection CVEs in Nineteen Seconds, Nine With No Fix
Between 15:16:59 and 15:17:18 UTC on 14 October 2026 — nineteen seconds — NVD ingested eighteen CVE records with VulnCheck as the assigning CNA, every one of them CWE-78, and every one of them the same sentence written eighteen ways: a file name reaches a shell. Twelve are scored CVSS 4.0 9.3 (AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H), one 9.2, one 8.7, four 8.5. All are status Received.
Individually these are small packages, and the temptation is to file the batch under “npm long tail” and move on. Two things make that the wrong call. The first is what the packages do: transcribe audio, convert documents, extract archives, generate thumbnails, stitch video, compress fonts. That is the exact middle layer of a modern ingestion pipeline, the layer an agent reaches for when a task says summarise this recording or pull the text out of these PDFs. The second is the split we found when we read every record and every linked pull request: nine of the eighteen have a fixed version, and nine do not. All nineteen fix PRs were filed by the same person, months ago.
The batch, split by whether anyone merged the patch
NVD version ranges encode the split precisely. A record that reads before X has a fix; a record that reads through X does not. Nine of each:
Fixed. git-diff before 2.0.7 (CVE-2026-110634), webp-converter before 3.0.0 (CVE-2026-110635), buildebooks before 0.2.2 (CVE-2026-110637), @mborne/extract before 0.2.1 (CVE-2026-110639), xhtml2pdf before 0.2.19 (CVE-2026-110642), video-stitch before 2.0.0-beta.1 (CVE-2026-110645), snappdf 1.1.0 before 6.1.0 (CVE-2026-110646), @offactory/compress-fonts before 1.1.1 (CVE-2026-110648), stills before 25.24.2 (CVE-2026-110649).
No fixed version exists. office-converter through 1.0.2 (CVE-2020-37282, a retroactive identifier on a package last published in 2015), pypdftk through 0.5 (CVE-2026-110636), doc-recog through 1.1.6 (CVE-2026-110638), whisper-node through 1.1.1 (CVE-2026-110640), @runnerty/executor-decompress through 3.1.1 (CVE-2026-110641), transcribe2texts through 1.0.6 (CVE-2026-110643), nodejs-whisper through 0.3.1 (CVE-2026-110644), turnpress through 0.4.2 (CVE-2026-110647), the thumbnail Python package through 1.5 (CVE-2026-110650).
For eight of those nine, the version named in the record is the version a fresh install gets today. We checked the npm registry and PyPI: nodejs-whisper 0.3.1 is latest (published 3 August 2026), whisper-node 1.1.1 is latest (published 29 November 2023), thumbnail is at 1.5, pypdftk at 0.5, turnpress at 0.4.2.
One researcher, nineteen pull requests, four months of waiting
Every fix PR referenced across the eighteen records was opened by the same GitHub account, zachariah-mithani, between 21 June and 2 September 2026. We pulled the state of each one from the GitHub API. The distribution of maintainer responses is the most interesting artefact in the batch:
- Merged within hours.
stillsPR #29 was opened 5 August at 17:11:53 UTC and merged at 17:23:44 — eleven minutes and fifty-one seconds.git-diffPR #16: ninety-one minutes.@offactory/compress-fontsPR #1: ninety-two minutes.VideoStitchPR #26: ten hours. A follow-upstillsPR #30, titled “Finish the shell-injection fix: two more paths reach a shell in 25.24.1,” was merged the next day. - Merged on the eve of publication.
@mborne/extractPR #8, open since 2 September, was merged 13 October at 19:29 UTC; v0.2.1 was tagged twelve minutes later and v0.3.0 half an hour after that.buildebooksPR #12, also open since early September, was merged 13 October at 22:00 UTC, with npm 0.3.1 published at 22:15. Both landed under twenty hours before their CVE records went public. Coordinated disclosure deadlines are visible in commit timestamps if you know to look. - Closed without merging, but fixed anyway. Two maintainers took the report and discarded the diff. For
xhtml2pdf, PR #835 was closed; thestartViewerfix instead arrived inside commitf86380d, a 48-file merge titled “Development (#838)” authored by Luis Zárate on 12 September. Forwebp-converter, the maintainer explained the decision in the thread: “Rather than merge this diff directly, I rolled the fix into the larger 3.0.0 modernization, which rewrote the library in TypeScript.” Both are legitimate outcomes; both also mean a reader tracing the CVE to its linked PR sees “closed, not merged” and has to keep digging to learn the bug is actually dead. - Still open. Nine, the oldest since 21 June.
One exchange in the webp-converter thread deserves quoting because it is the step most of this batch is missing. On 1 September the reporter asked the maintainer to file a GitHub Security Advisory on the repository, noting it was needed “to ensure downstream users and automated tools (npm audit / Dependabot) are notified to upgrade from older versions.” A merged patch silently changes the future. An advisory is what reaches everyone already running the old version — which, for a package with 42,000 monthly downloads, is most of them.
The code, read at the pinned commits
We read the vulnerable source for each record we quote. The cleanest specimen is whisper-node, where src/whisper.ts builds the whisper.cpp invocation as a template literal with no quoting at all:
export const createCppCommand = ({ filePath, modelName = null, modelPath = null, options = null }: CppCommandTypes) =>
`./main ${getFlags(options)} -m ${modelPathOrName(modelName, modelPath)} -f ${filePath}`;
The language flag is assembled the same way elsewhere in the file — if(flags.language) s += " -l " + flags.language; — which is why the CVE names both filePath and whisperOptions.language as injection points. The resulting string goes to shelljs’s exec(). A space in a path is a bug report; a $(...) in a path is remote code execution.
nodejs-whisper is the more instructive case, because its author did think about quoting. In src/utils.ts at tag v0.3.1:
const command = `ffmpeg -nostats -loglevel error -y -i "${inputFilePath}" -ar 16000 -ac 1 -c:a pcm_s16le "${outputFilePath}"`
const result = shell.exec(command)
Double quotes defeat word-splitting and glob expansion. They do not defeat command substitution: inside double quotes, sh still expands $(...) and backticks. That is the entire difference between a path-with-spaces bug and a 9.3, and it is why this package scores identically to the one that used no quotes. Quoting is not escaping, and escaping is not the fix — the fix, in every merged patch in this batch, is the same one line of architecture: stop building a string, pass an argv array, drop the shell. execFile instead of exec, subprocess.run([...]) instead of os.system.
The Python entries show the unquoted form. thumbnail/thumbnail.py concatenates and hands the result straight to os.system:
command = 'ffmpeg -y -i '+input+' -vf thumbnail '+vidcommand+' -frames:v 1 '+output
os.system(command)
and xhtml2pdf’s startViewer, in pisa.py at v0.2.18, had the macOS fallback os.system('open "%s"' % filename) — reachable from the CLI’s -s/--start-viewer flag, and quoted in exactly the way that does not help.
Why a batch of small packages belongs on this site
Set aside the CVSS vectors for a moment and look at what the attacker controls. Not a request body, not a config file, not a model’s tool call — a file name. That is a category of input almost nobody treats as hostile, because for thirty years the worst it did was break a build script. It arrives from object storage, from a zip someone uploaded, from an email attachment, from a scraper, from a previous pipeline stage that named the file after a field in a document it had just parsed.
In agent plumbing this gets sharper. An agent that is told to transcribe the recordings in this bucket enumerates names it did not choose and passes them to a wrapper it did not audit. The sandbox discussion around such a system is almost always about the model’s tool calls — which commands it may run, which approval gate covers them. None of that is engaged here. The model never asked to run anything. A helper library turned a data value into a command three stack frames below the policy layer, which is the same place the Emacs TRAMP filename RCE lived, and the same reason the five Roo Code approval bypasses were unfixable by improving the approval list: the gate and the shell were parsing different languages.
VulnCheck itself documented the end state of this pattern two months ago, in a disclosure we had not previously covered. CVE-2026-14863, published 13 August 2026 by Valentin Lobstein, is the same bug in FileRun, a commercial self-hosted file manager: the thumbnail extractors paste an uploaded file path into a double-quoted string and call exec(), and the filename sanitiser lets $() through, so “a file named $(payload).mp4 runs its payload the moment a thumbnail is generated.” Fixed in 2026.2.1 after four years in the codebase. The write-up also names the force behind batches like today’s, in a sentence worth sitting with: “Open-source codebases are getting shredded by AI-assisted auditing, where every researcher and their LLM greps the same GitHub repos and finds the same bugs.”
That is the honest framing for eighteen records that arrived in nineteen seconds. This is not eighteen pieces of deep research. It is one well-understood pattern — untrusted file name reaches a shell-interpolated string — applied at scale across a package ecosystem, with a real patch attached to each instance. The scarce resource is no longer finding these. It is getting someone to merge the fix and publish an advisory, and on that measure the batch is running at exactly fifty percent.
Three details worth catching before you triage
video-stitchis listed as fixed and is not, by default. The record says “before 2.0.0-beta.1,” and that version does exist on npm — under thenextdist-tag. Thelatesttag still points at 1.7.1, published 25 June 2021.npm install video-stitchinstalls the vulnerable version today. A scanner reading only the fixed-version field will mark this one green.- Popularity does not track maintenance. The two unfixed transcription wrappers are the widely used ones:
nodejs-whisperdrew 57,354 downloads last month andwhisper-node30,928, against 1,805 for thestillspackage whose maintainer merged in twelve minutes.whisper-node’s repository has 307 stars, 31 open issues, and no push since 5 August 2024; its newest npm release predates the current whisper.cpp CLI. It is not archived, so nothing on the package page warns you. - Fixed upstream is not fixed downstream.
webp-converter’s fix came as a TypeScript rewrite released as 3.0.0; the vulnerable API lived insrc/webpconverter.js, which no longer exists. That is a major-version migration, not a patch bump, and pinned dependents will not take it automatically. Same story forsnappdf(1.1.0 → 6.1.0).
What to do
- Grep your pipeline for the pattern, not the package list. Eighteen CVEs is a sample, not a census. Search for template literals and string concatenation feeding
child_process.exec,execSync,shelljs.exec,execawithshell: true,os.system, andsubprocesswithshell=True. Any of those carrying a path variable is the same bug under a different CVE number. - Upgrade where a fix exists:
git-diff≥ 2.0.7,webp-converter≥ 3.0.0 (current 3.0.1),buildebooks≥ 0.2.2,@mborne/extract≥ 0.2.1,xhtml2pdf≥ 0.2.19 (current 0.2.23),snappdf≥ 6.1.0,@offactory/compress-fonts≥ 1.1.1,stills≥ 25.24.2. Forvideo-stitchyou must pin2.0.0-beta.1explicitly. - For the nine with no fix, do not wait for one. Four of the nine packages have had no release in over five years. The practical mitigations are to wrap the call yourself with an argv-array spawn, or — cheaper and more robust — to never pass an externally supplied name through. Copy the input to a random, charset-restricted path (
[A-Za-z0-9._-], no leading dash) before invoking any converter, and map the result back afterwards. This also fixes the argument-injection variant that quoting and escaping both miss. - Treat file names as untrusted input in your threat model, in writing. If a document’s metadata, a model’s output, or a remote server’s
Content-Dispositionheader can influence a name on disk, that name is attacker-controlled data with a path toargv[0]. - If you maintain one of these packages, publish a GHSA. A merged patch protects people who upgrade. An advisory is what tells
npm audit, Dependabot, and every SCA scanner that the old version is dangerous — and as of this writing, most of the nine fixed packages in this batch have no repository advisory at all.
Verification note: all eighteen CVE identifiers, publication timestamps, CVSS 4.0 vectors and base scores, CWE assignments, affected and fixed version ranges, vulnStatus values, the VulnCheck CNA attribution, and every reference URL were read first-hand from the NVD 2.0 API. The state, title, author, creation time and merge time of all nineteen referenced pull requests, the maintainer and reporter comments quoted from the webp-converter thread, the xhtml2pdf fix commit f86380dad15b3a6810201d08755a3ee6b19d5063 (12 September 2026, Luis Zárate, 48 files) and the buildebooks merge commit 123776c9 (13 October 2026 22:00:38 UTC) were read first-hand from the GitHub API. Vulnerable source was fetched directly at the commits and tags pinned in the CVE references: whisper-node src/whisper.ts at c72944cf, nodejs-whisper src/utils.ts at v0.3.1, thumbnail/thumbnail.py at 23b2e305, xhtml2pdf/pisa.py at v0.2.18, and stills lib/content/video.js at 66d5def1 — all code blocks above are verbatim from those files. Current versions, publish timestamps, dist-tags and last-month download counts came from the npm registry and PyPI JSON APIs on 14 October 2026; repository metadata (stars, open issues, archived status, last push) from the GitHub API. The FileRun comparison and both quoted sentences come from VulnCheck’s own disclosure blog post for CVE-2026-14863, fetched directly. Brave Search returned only CVE-mirror aggregators for these identifiers and no analytical coverage. We installed none of these packages, crafted no file names, ran no converter, and executed no proof of concept; every mechanism described above is read from source.
Sources:
- NVD — CVE-2026-110644, nodejs-whisper through 0.3.1 OS command injection in convertToWavType (published 14 October 2026, CVSS 4.0 9.3, CWE-78)
- NVD — CVE-2026-110640, whisper-node through 1.1.1 OS command injection via filePath and language
- NVD — CVE-2026-110650, thumbnail through 1.5 OS command injection in generate_thumbnail
- NVD — CVE-2026-110645, video-stitch before 2.0.0-beta.1 (the fixed version npm’s latest tag does not point at)
- whisper-node src/whisper.ts — createCppCommand, the unquoted template literal (read at the pinned commit)
- nodejs-whisper src/utils.ts at v0.3.1 — the double-quoted ffmpeg command passed to shelljs exec
- webp-converter PR #53 — closed unmerged; maintainer thread on rolling the fix into the 3.0.0 TypeScript rewrite and the request to publish a GHSA
- stills PR #30 — “Finish the shell-injection fix: two more paths reach a shell in 25.24.1” (merged 6 August 2026)
- VulnCheck — FileRun: When Your File Manager Runs Your Files (CVE-2026-14863, Valentin Lobstein, 13 August 2026)