Skip to content

Media-pipeline edge surfaces: filename shell-out, metadata XML XXE, author-template RCE, and chunked-upload validation asymmetry

Source: hourly offensive-security scan of GitHub Security Advisories, 2026-09-20 (wave published 2026-09-20T12:30Z).

Advisories: GHSA-7c6f-jc48-6hfq / CVE-2026-94106 and GHSA-gmhw-pxmg-j22w / CVE-2026-94108 (getID3), GHSA-w599-5qgp-f3cf / CVE-2026-94109 (openEQUELLA), GHSA-m76p-p4wh-76x9 / CVE-2026-94104 (NivoCart).

Four records from one wave generalize into the same hunting rule: the media/upload pipeline has more code paths than the upload endpoint — filenames get interpolated into shell commands, metadata sidecars get XML-parsed, "personalization" fields get compiled by a template engine, and the chunked-upload branch skips the checks the single-file branch runs. Each is a separate sink reachable from the same "benign" file-upload or content-edit feature.

Advisory table

Product GHSA CVE CVSS Boundary
getID3 < 1.9.26 GHSA-7c6f-jc48-6hfq CVE-2026-94106 8.8 OS command injection in shell-out handlers: external-tool invocations (ffmpeg/ffprobe-style probe helpers) build command strings with the filename unescaped → a media file named with shell metacharacters executes arbitrary commands with the embedding process's privileges. getID3 is embedded in hundreds of PHP upload/CMS/shelf apps
getID3 ≤ 1.9.26 GHSA-gmhw-pxmg-j22w CVE-2026-94108 6.5 XXE in XML2array — entity loading not disabled (PHP < 8.0 default) → XML metadata embedded in a media file discloses local files, SSRFs, or explodes entities. The XML never touches an "XML endpoint"; it rides inside an audio/video container parsed by the metadata library
openEQUELLA < 2026.1.0 GHSA-w599-5qgp-f3cf CVE-2026-94109 8.8 FreeMarker SSTI → RCE: unsandboxed TemplateClassResolver lets an authenticated user's template expressions in collection summaries, dashboard portlets, or MIME templates instantiate freemarker.template.utility.Execute → Runtime.exec. Content-author rights = server code execution
NivoCart ≤ 2.4.0 GHSA-m76p-p4wh-76x9 CVE-2026-94104 8.8 Chunked-upload validation asymmetry: File Manager multi() endpoint skips extension validation for new filenames and whenever the chunks parameter is ≥ 2 → a view-only back-office user uploads PHP to the web-accessible image/data/ directory → RCE

Why this is worth an operator page

  • The filename is an injection site when any handler shells out. Everyone fuzzes file contents; almost nobody fuzzes the name against media pipelines. Any app that probes uploads with an external binary (getID3's shell-out handlers, ffmpeg/ffprobe wrappers, ImageMagick delegate calls, exiftool glue) may build cmd <name> strings. Upload canary names shaped like '$(id).mp3, x;touch /tmp/;1.mp3, backtick and | variants (URL-encoded and raw, multipart filename and stored path) and diff probe-side effects against a plain name. On open-source PHP stacks the grep is two-step: getID3 in composer.json, then any user-controlled path reaching exec|shell_exec|system|passthru|proc_open|popen.
  • Metadata sidecars are a hidden XML entry point. The XXE battery normally runs against SOAP/API endpoints; CVE-2026-94108 shows the same primitive reachable by embedding XML in a media container that a metadata parser XML-parses. On every upload feature, ask: which parsers see which parts of my file (containers, sidecars, EXIF/XMP, ID3 XML tags, zip comment blocks)? Craft a media file whose metadata block is an XXE canary (inert out-of-band marker against an owned callback) and watch whether the parse error or fetched resource leaks. simplexml_load_* without LIBXML_NOENT-disabling discipline, XML2array-style helpers, and any PHP < 8.0 host are the fingerprint.
  • "Personalization" fields are template-engine entry points. LMS/CMS/repository platforms that let authors write collection summaries, dashboard portlets, or email/MIME templates frequently back those fields with FreeMarker/Velocity/Twig with the default class resolver. Test each free-text authoring field with the canonical inert FreeMarker probe (${"freemarker.template.utility.Execute"?new()("id")} equivalent kept lab-only and marker-only, e.g. ?new() on a class that only prints a constant or writes a marker file) and record which fields get compiled vs stored. Same rule as the Sept 19 hono/jsx and OpenPanel items: the sandbox policy belongs to the render/compile path, not the product — unsandboxed resolver means the field is RCE for the lowest role that can edit it.
  • Run every validation matrix branch, not just the default one. NivoCart's chunked branch skipped the extension check the single-file branch enforced — the chunks/chunkNumber/resumable parameter family (tus, resumable.js, plupload, dropzone servers) is a validation-bypass class of its own. For any upload endpoint that supports chunking: upload the same disallowed-extension file once as a single request and once as a multi-chunk sequence, and also under an alternate filename, then diff verdicts. This is the validate-leg ≠ persist-leg family with a parameter-selected code path twist.

Validation workflow (authorized scope only)

Inert markers, lab targets, no live shells

Prove command execution with marker-file/marker-output techniques on lab installs you own; never deploy web shells or touch production media libraries. XXE proofs use owned callbacks only. Template probes print constants — no downloaders, no reverse shells.

  1. Filename canary matrix (media upload, lab). Same tiny valid media bytes under N names: plain, $(marker).ext, backtick, ;touch-marker, |, percent-encoded variants. Record: accepted/stored name, probe-side effects (latency, marker file), and whether the stored path re-enters a probe/shell-out call later (rename-then-trigger chains).
  2. Metadata XXE canary. Build a valid container (audio/video) with an XML metadata block containing an external-entity reference to your owned no-content callback. Upload; poll callback and error text. Negative controls: entity-disabled parse on PHP ≥ 8.0 and plain metadata file.
  3. Template-field sweep (openEQUELLA-class or any platform with author templates). Enumerate every free-text field that renders styled output (summaries, portlets, notification email templates). Submit per-engine inert probes (FreeMarker ?new() on a constant-print class, Velocity #set math, Twig {{7*7}}) and record which field compiles which engine. RCE claim requires the Execute-class proof on a lab instance only, captured as marker output.
  4. Chunk-vs-single upload parity. Same disallowed-extension file: single POST, two-chunk sequence, three-chunk sequence, alternate filename per leg, plus chunks=1 vs chunks=2 parameter-only flip. Verdict table proves the branch asymmetry; place the file in the app's stated upload dir and confirm executability claim by response of the handler mapping, not by running commands.
  5. Fingerprint before claiming. getID3 version from error strings/composer.lock echoes; openEQUELLA version from product footer/about API; fixed lines: getID3 1.9.26, openEQUELLA 2026.1.0, NivoCart > 2.4.0 (no fix — "through 2.4.0").

Reporting checklist

  • For filename shell-out: pin the embedding app + getID3 version, the exact multipart filename bytes, and the marker side-effect evidence; state the privileges of the embedding process, not "root".
  • For metadata XXE: show the container structure (which box/atom carried the XML), the entity payload, and callback hit with timestamps; note PHP runtime version when inferable.
  • For template RCE: minimum author role, exact field, compiled-vs-stored verdict table, and lab-only Execute proof; report as "content-author rights = server code execution through unsandboxed template resolver".
  • For chunked upload: the parameter-only-flip decision table is the core exhibit; note the target's handler mapping if claiming PHP execution.
  • Generalize in the report body: filename sinks, sidecar parsers, template-backed fields, and chunk branches are four extra paths through one upload feature — the upload endpoint's own validation passing is not a negative control.

Related pages: the Sept 18 media client-side render sinks page (auxiliary asset formats as input channels), the Sept 20 WordPress deserialization-transport page (un-fuzzed transports), the Sept 19 JS sandbox/async-render escape page (render-path-bound policy), and the May 9 template/route authority batch.

September 21 follow-up: format-agnostic image "validation" + attacker suffix + static mapping = stored XSS

  • newbee-mall ≤ 1.0.0 goods-name stored XSS via image-guard bypass (CVE-2026-94045, GHSA-92hr-qfch-w4h7): the upload "image-only" guard is ImageIO.read() — a format-agnostic read that returns non-null for a polyglot file (valid PNG header + <img onerror> tail), and it is the attacker-controlled suffix plus the /upload/** static mapping that turns the accepted upload into persisted XSS rather than a one-shot blob. Operator rule for every upload feature claiming image validation: a decode-success check validates the prefix, not the file — build polyglot canaries (magic bytes + inert HTML/JS tail), request the stored URL under both the real and claimed extension, and confirm the server serves your tail bytes with an executable-context Content-Type. The finding class is the three-leg chain — validation leg (decode-only), storage leg (suffix preserved), delivery leg (static route) — pass all three as separate evidence fields. Lab-bounded: synthetic polyglot canaries on your own upload target; never leave live event-handler payloads.

September 23 follow-up: the same product's second RCE leg — a deserialization denylist enforced on the outer stream only

  • openEQUELLA < 2026.1.0 authenticated RCE via SignedObject-nested deserialization (CVE-2026-67615, GHSA-h2p3-9m67-89gr, 8.8): the HTTP invoker endpoint at /invoker/* pipes serialized input through PluginAwareObjectInputStream, which enforces a class-name denylist — but nesting a payload inside a java.security.SignedObject makes the inner content get deserialized by a separate ObjectInputStream that applies no denylist, reaching a JNDI sink → code execution for any authenticated non-guest user. Same product as this page's FreeMarker SSTI leg: two independent content-to-code channels, one author-rights, one any-user.
  • Durable axis: deny/allow filters on a deserialization entry point enforce only what the filtered stream itself reads. JDK-standard container classes that perform secondary deserialization on their payload — java.security.SignedObject, javax.management.remote.rmi.JRMP_Stub (equals/hashCode-triggered), MXClean, Rome SyndicationImpl — each open an inner stream outside the outer filter's coverage. Sweep rule for every Java deserialization sink with a hardening class: (1) fingerprint the filtering (ObjectInputFilter, custom resolveClass, lookahead-stream) and read which stream applies it; (2) test the payload wrapped in each known secondary-deserialization container; (3) prove reachability of the inner stream with a benign resolved-class marker before claiming the JNDI/exec leg. Lab-bounded: owned LDAP/RMI callbacks and inert marker classes only; never chain to live JNDI against production hosts.
  • Generalizes the standing "fix coverage drift" family (mcp-atlassian per-tool validators, Skipper auth-branch hooks): here the drift is per-stream — the guard is bound to one ObjectInputStream instance, and any code path that constructs a fresh one is unguarded.

September 23 second follow-up: a personal-dashboard portlet field evaluated as full TALES = RCE for any registered user (Plone, folded)

  • plone.app.portlets Classic portlet RCE via TALES injection (CVE-2026-57149, GHSA-rr49-f9g6-c9r5, 9.9 scope-changed, patched Plone 6.2→7.0.2 / 6.1→6.0.4 / 6.0→5.0.8): the Classic portlet takes user-supplied template/macro fields, builds a TALES path expression by string interpolation, and hands it to the TAL path() helper — the value is evaluated as a full TALES expression, so a crafted string escapes the intended "simple path" grammar and executes arbitrary expressions in the Plone/Zope process. With the default role map, ManageOwnPortlets lets any authenticated regular user add a portlet to their personal dashboard — registered-user → server-process code execution, no admin or editor rights needed.
  • Same rule as this page's FreeMarker leg (openEQUELLA author-template RCE): fields whose nominal function is "path", "template", or "macro" are parsed by an expression engine that treats your input as code, not data. The field's UI label describes the intended grammar; the evaluator applies the engine's full grammar.
  • New axis this exemplar adds: enumerate self-service features' default role maps as footholds. Personal dashboards, portlets, preferences, saved searches, and workspace widgets are designed for every registered user, so their permissions are open by default — but when such a feature crosses into a server-side template/expression engine, the "low-privilege" UI reaches an admin-grade sink. Audit sweep for any Zope/TALES/TAL/Twig/FreeMarker/Velocity-adjacent CMS: in every user-reachable content field, test expression-engine syntax markers (python:, string:, structure:, nocall: prefix forms; engine-specific openers) as inert probes — a rendered/echoed evaluation result or differential error message proves the field is expression-parsed before you prove anything executable.
  • Bounded validation: own lab Plone with a disposable registered account; prove with a benign expression that changes rendered portlet output (string concatenation marker), never a python: RCE chain on a shared host; capture the field→evaluator trace for the report. Generalize alongside the Sept 21 Configlet-SSTI rule (config-generation templates are SSTI sinks) — here the "config feature" is a per-user portlet and the default role map is the precondition.

September 23 third follow-up: an "isolated" template sandbox is still an oracle — unauthenticated form field values evaluated as Twig (Solspace Freeform, folded)

  • Solspace Freeform for Craft CMS 5.0.0–5.10.13 unauthenticated limited SSTI (CVE-2026-73858, GHSA-gxrg-x694-283w, 5.3, patched 5.10.14): public form submissions are passed through Freeform's isolated Twig renderer, so {{ constant('PHP_VERSION') }}@example.com in an email field evaluates server-side and reflects in the response (value="8.2.27@example.com"); a text field accepts {{ constant('CRAFT_VENDOR_PATH') }} for absolute-path disclosure. The vendor-tested sandbox blocks craft, craft.app, alias(), getenv(), credentials, and security keys — but the evaluated-yes/no differential itself is unauthenticated, no-interaction, and yields runtime fingerprinting (PHP version/OS, absolute filesystem layout) straight from the public marketing site's contact form.
  • Same page rule as the Plone TALES fold two sections up: fields whose data is re-rendered through a template engine are SSTI probes regardless of the field's UI purpose. New axis this exemplar adds: a hardened/sandboxed SSTI is still worth reporting and chaining — treat a confirmed-but-sandboxed evaluation as (1) a fingerprinting oracle (constant(), engine version introspection, error-message differentials leak PHP/OS/paths that sharpen every later probe), (2) proof the render path parses your input, which justifies deeper sandbox-escape research against that specific engine+config combination, and (3) a candidate pivot when combined with a separate weakness (file write + {{ source() }}/include primitives, or a future sandbox regression). Do not discard a "limited" SSTI because the advisory CVSS is 5.3 — the entry point being a nopriv public form with zero prerequisites is the operator-relevant property the score hides.
  • Sweep: any Craft CMS (or Twig/Blade/Tpl engine) site with a public form (Freeform, Formie, Neo, custom controllers) — submit engine markers into every field type (text, email, hidden, options) and diff echo vs evaluation vs engine-error; confirm sandbox ceiling with inert constant()-class probes only. Fix confirms the contract: vendor patched by not rendering submitted values as Twig at all — "values are data" is the correct boundary, and any plugin that re-renders user data through its template layer is the same bug until proven otherwise. Bounded validation: own lab Craft install with a disposable Freeform form; reflect PHP version markers only; never probe env/credential classes beyond documenting the sandbox's claimed denial.

October 3 follow-up: the converter backend's own executable-format family + security-policy parse fail-open (ConvertX, ImageMagick)

  • ConvertX < 0.19.0 unauthenticated → RCE via Calibre recipe files (CVE-2026-105080 / GHSA-6w7x-66x4-9w6v, 9.9 critical, CVSS S:C, CWE-829): the self-hosted conversion API's converters/calibre.ts routes ebook uploads to ebook-convert, and its format gate does not block .recipe / .downloaded_recipe — Calibre formats whose content is executable JavaScript run by the Calibre runtime (recipe files define fetch-and-transform scripts; .downloaded_recipe additionally instructs a download leg). Upload a recipe → code executes in the converter process, which in typical deployments is the container hosting every other converter binary. Durable axis: an upload-type filter enumerates formats; the backend's supported-input list is the real attack surface — and some "document" formats are executable code. Every conversion service wraps a general-purpose engine whose native-format list includes script-bearing formats: Calibre .recipe, LibreOffice macro documents (.docm/.odm), ImageMagick MSL/MVG/SVG delegate forms, FFmpeg concat/demuxer features, archive formats with symlink/preserve-permissions entries. Sweep for any conversion/export/upload API: (1) identify the backend binary (error strings, timing, version banners — ebook-convert, soffice, convert, ffmpeg); (2) enumerate the backend's own documented input formats, not the UI's advertised ones; (3) submit one inert marker per executable-semantics format (a recipe whose JS is a fetch to your owned callback) and diff "rejected by filter" vs "handed to backend". Fix shape is instructive: the vendor patched at 0.19.0 by blocking the recipe extensions — filter-list patches against engine-format lists recur, so re-test after any upgrade with the engine's current docs. Bounded validation: own lab ConvertX container, callback-only recipe JS, inspect container state; never leave live recipes on shared converters.
  • ImageMagick ≤ 7.1.2-32 / ≤ 6.9.13-57 silent security-policy disable via alternate DOCTYPE (CVE-2026-105083 / GHSA-9r9r-c7x8-qp3r, low/3.9 as scored — the operator value outruns the CVSS): LoadPolicyCache mis-parses a policy.xml whose DOCTYPE is an alternate/valid-but-nonstandard form not ending in ]> — the parser consumes the rest of the file as the internal subset, applies zero policy rules, and fails silently (no error, no log). Policies are how ImageMagick deployments block the delegate/file-read/SSRF format families this page already catalogs — a zero-policy process means every domain="mask" restriction (read/write/exec path rules, MSL/SVG/EPHEMERAL blocks) is simply gone. Durable axis: security-config files that fail to parse often fail OPEN, silently, and the parse quirk is the bypass. This generalizes the wiki's recurring guard-is-a-shape-parser family (Airflow masker, Dancer2 guards, Composer fix-bypass): the policy engine is a parser, and parser edge-cases on the config side are bypasses on the enforcement side. Operator checks: on your own ImageMagick-backed targets, after any policy edit run magick -list policy and confirm rules actually appear — a policy file that yields an empty list under an alternate DOCTYPE proves the silent skip; on upload targets that reject an exotic format, test whether policy-gated formats (SVG→MSL chains, ephemeral: reads) still reach delegates via policy-xml-adjacent config drift or a deployment that hand-edited its policy with an alternate DOCTYPE. Report language: state the exact DOCTYPE bytes, the before/after -list policy output, and one restricted operation proven allowed; do not claim delegate RCE from policy-absence alone.

October 5 follow-up: encoder-side memory corruption as RCE in fork-based video services (libaom)

  • libaom (reference AV1 encoder) SVC layer-ID bounds gap → arbitrary command execution (CVE-2026-56211 / GHSA-qr4p-j2pf-4w4x, high): insufficient bounds validation in the encoder's SVC layer-ID control lets crafted video frame pixels overlap internal encoder layer-context structures; in fork-based video processing services the attacker hijacks the cyclic-refresh-map pointer and brute-forces the process base address via a crash oracle to redirect control flow. Preconditions are explicit: target must run libaom with SVC encoding enabled and accept attacker-supplied frames.
  • Durable axes: (1) the encoder is as attackable as the decoder — transcode services accept uploads and re-encode them, so encoder bugs (not just the classic decoder fuzzing surface) are reachable from an upload endpoint; include "does the service re-encode with AV1/SVC?" (ffmpeg -c:v libaom-av1 -speed … -tune sq/psnr with SVC layers, -la-simd defaults) in the fingerprint step before dismissing AV1 as exotic; (2) crash-oracle ASLR brute force in fork models — a service that forks per job reuses the same base address across children when ASLR entropy is low at that layer (no PIE, containerized runtime, preloaded libs): repeated crashes are an information channel, and this advisory is a published instance of using crash/no-crash differentials to defeat per-process randomization without leaking anything; (3) upload-to-transcode pipelines (video SaaS, thumbnail/preview farms, chat-app media processors) should be scoped as media-pipeline targets even when decoder fuzzing fails — layer/ID/structure fields in the output-rate control path are the under-fuzzed place.
  • Boundaries: this page's standing rule holds — reproduction stays in your own lab container with an inert marker payload; do not run crash-oracle loops against shared transcoding services (it is both an attack and a DoS). Generalizes alongside this page's FFmpeg/ConvertX legs: engine format lists are one axis, engine internal control structures fed by attacker frames are the second.

October 7 follow-up: Ghostscript -dSAFER escape to shell command execution via crafted PS/EPS (CVE-2026-101258 / GHSA-g96p-c3cq-wp28, 7.8)

A crafted PostScript/EPS document chains memory corruption in document parsing with runtime disabling of Ghostscript's internal path access controls, letting the render execute arbitrary shell commands as the Ghostscript process — the -dSAFER sandbox itself is bypassed, and delivery works through any format that delegates to Ghostscript (EPS import, print conversion, many image/document converters' gs backend).

  • Durable axis on this page: the sandbox of the delegated engine is the real security boundary, and it is a parser target. The Sept 20 ConvertX/ImageMagick legs proved backend format-list and policy-parse fail-opens; this proves the stronger case — an enabled, documented sandbox (the industry default for every web app that converts EPS/PS via gs) falls to a malformed-document input. Standing rules: (1) any upload path whose converter graph reaches Ghostscript (ImageMagick with gs present, CUPS/print pipelines, LibreOffice/Office PDF-export legs, scientific/graphics toolchains) is an RCE candidate even when the wrapper looks correctly locked down — fingerprint wrapper version AND gs version separately; the fix is in gs, not the wrapper. (2) The Red Hat tracker links a public poc repo (v12-security/pocs/ghostscript) — for authorized engagements this is a fingerprint-and-report item (version evidence + delegation-path proof: does the app shell out to gs? which formats route to it?) before anything else. Proof stays in an owned container running the affected gs with an inert marker payload; never weaponize against a customer's conversion service without explicit authorization.

October 7 follow-up: WeasyPrint hands attacker image bytes to Ghostscript with no format allowlist (CVE-2026-106443 / GHSA-r543-q48m-4c9j, ≤69.0, fixed 70.0)

The wrapper-side twin of the gs -dSAFER leg above, found on a real pentest (NLnet-funded, Radically Open Security). WeasyPrint's image pipeline reads any external response and calls Image.open(BytesIO(bytes)) — Pillow's format-agnostic dispatcher selects the handler from the byte signature, so EPS/PS magic routes to EpsImagePlugin, which shells out to Ghostscript whenever a gs binary is on PATH. Every content surface that can supply an image (an <img> URL, a CSS image() value, an SVG image reference, a data URI) drives untrusted PostScript into the interpreter; chained with any -dSAFER bypass (see the g96p leg directly above) that is RCE on the render host.

  • Durable axis: format dispatch IS the boundary — an allowlist of MIME strings or file extensions upstream proves nothing if the final handler selects by content sniffing. The fix in 70.0 is a raster-format allowlist at the dispatch point, not MIME validation. Audit rule for any HTML→PDF/render service (WeasyPrint, wkhtmltopdf/Qt WebKit legs, headless-Chrome print pipelines with plugin handlers, img2pdf, Docling's engine graph): fingerprint whether the delegation engine (gs, ffmpeg, LibreOffice) is installed, then feed one EPS/PS canary with %!PS magic through every image-supply channel the product accepts (remote URL, CSS url(), SVG href, data: URI) and log which channels reach the engine — remote-fetch-to-gs is usually a narrower path than the HTML body itself because the fetch happens server-side with the render host's network position. This is also the standing wrapper-vs-engine reminder in action: patched WeasyPrint on a host with old gs still routes other formats' quirks into it; fingerprint wrapper AND engine separately.

October 11 follow-up: bare-name loader launches the delegated engine — plant gswin64c.exe in the CWD (ImageMagick Windows), and Pandoc without --sandbox reads any file via RST include (ConvertX)

The Ghostscript legs above proved the engine falls; this pair proves the launcher and the wrapper flags fall on their own:

  • ImageMagick on Windows through 7.1.2-33 / 6.9.13-58 — uncontrolled search path in NTGhostscriptEPD/EXE (GHSA-92c8-5jfq-j2vj / CVE-2026-108693, high): when Ghostscript is not registered in the registry, NTGhostscriptEXE() launches gswin64c.exe by bare name — standard Windows CreateProcess search order puts the current working directory first, so an attacker who can plant a file in the CWD of the ImageMagick process gets code execution with ImageMagick's privileges whenever any PDF/PS/EPS conversion runs. Durable axes: (1) "engine missing/unregistered" is an attack path, not a dead end — delegation code paths have degraded fallback modes (bare-name lookup, PATH-relative spawn) that the hardened registry/config path never exercises; fingerprint whether the target's Ghostscript is registered, then treat CWD-writable locations (upload dirs that double as process CWD, share directories) as payload slots. (2) Windows bare-name spawn is its own standing checklist item for any converter appliance: identify the helper binaries each engine spawns and test whether the spawn is absolute-path, registry-resolved, or bare.
  • ConvertX through 0.19.0 — arbitrary file read because the Pandoc backend is invoked without --sandbox (GHSA-r4xg-82px-pwx6 / CVE-2026-108694, high): src/converters/pandoc.ts runs Pandoc on user uploads, and reStructuredText's include directive with an absolute path pulls any file the converter user can read into the conversion output, which the caller then downloads. This page's October 3 ConvertX leg was Calibre's recipe format; this is the same wrapper's second backend with a different defect shape: each backend in a multi-engine converter has its own hardening flag, and the wrapper only enables the ones the author remembered. Audit rule: enumerate every converter backend (calibre/pandoc/libreoffice/imagemagick/ffmpeg legs), then check per-engine sandbox/isolation flags in the process invocation (--sandbox, -dSAFER, -no-output, chroot/env) — via source for open converters, via process-tree observation (ps/procmon during one conversion per format family) for closed ones. RST/Markdown include-style directives are a file-read primitive on any document pipeline that trusts the engine defaults.
  • Validation: own lab Windows host + unregistered gs + CWD-writable marker (gswin64c.exe as a benign pop-marker stub), and a lab ConvertX container converting an RST that includes a synthetic canary file inside the container; never plant binaries or read files on shared systems.

Tracked from the same wave without publication

  • GHSA-x6f2-qp26-2xrh / CVE-2026-94111 (Tencent BrowserSkill local daemon accepts any chrome-extension:// origin whose ID is 32 chars in [a-p] → rogue extension registers as browser client, intercepts DOM/screenshots returned to the AI agent) — folded onto the Sept 15 MCP/local-surface page as a September 20 follow-up: character-set+length origin allowlists are forgeable by registration.
  • GHSA-m9g7-gc6x-55x7 / CVE-2026-94113 (Frappe ERPNext whitelisted timesheet methods skip doctype permissions → any authenticated user enumerates billable time logs, project names, billing amounts) — folded onto the Sept 18 ORM/sibling-action page (enumerate whitelisted RPC callables, not just UI routes).
  • GHSA-g4x6-pg76-867r / CVE-2026-94107 (NivoCart substr(md5(mt_rand()),0,10) reset tokens → admin ATO), GHSA-j95g-6g2h-3f33 / CVE-2026-94112 (ezBookkeeping TOTP codes valid for reuse ~90 s after use), and GHSA-ccc2-34g9-j7rg (Dancer2 silent fallback to rand()-derived session IDs when CSPRNG modules missing) — folded onto the Sept 18 token-entropy page.
  • GHSA-6rhq-q57h-9phf / CVE-2026-86555 (ZTE SmartLife plaintext hardcoded key decrypting account-server info) — adjacent twin of the Sept 20 app-auth page; marked processed there.
  • Comfast CF-N1-S get_css_path_from_uri stack overflow (CVE-2026-94003, exploit disclosed — router memory-safety class, tracked per router precedent), DedeCMS plus/mytag_js.php aid code injection with public exploit (CVE-2026-94004 — detail-sparse "unknown function", revisit when reproducible detail lands), NivoCart destructive config-write disabling password recovery (CVE-2026-94105 — availability-only), SourceCodester Drug Recommendation + code-projects Internship/Assessment SQLi/XSS singles (generic low-CVMS app singles), Tencent/NivoCart remainder items — memory-safety, detail-free, or already-covered axes.

October 6 follow-up: Docling — the opt-in local-fetch guard isn't wired into the browser-render backend (GHSA-q43m-vhcp-mhvm / CVE-2026-105750)

Docling (the document-conversion library widely embedded in RAG ingestion pipelines) enforces enable_local_fetch=False on its plain HTTP fetch path but the HTML browser-render backend (HTMLBackendOptions(render_page=True) + Playwright) allowed file: URLs unconditionally in _get_browser_request_block_reason — a crafted HTML file embedding <iframe src="file:///..."> has the local file's contents rendered into the page image attached to the returned Document. Passive disclosure (JS disabled since 2.91.0; only what renders in the viewport leaks), Path inputs only — but on a shared conversion host that means .env, credential files, and other tenants' documents via screenshot.

  • This is the guard-per-backend parity axis completed: a library exposes a security option, one backend honors it and a newer/alternate backend never wires it in. Same family as this page's ConvertX backend-format leg — the wrapper's guard list must be checked against each engine's own fetch/render surface, because new backends ship new fetchers. Audit rule for any converter/RAG ingestor with multiple backends (headless browser, native parser, subprocess engine): grep whether each backend's request filter reads the security option at all; versions 2.82.0–2.90.x did zero request filtering in render mode.
  • Second Docling leg the same wave: arbitrary local file read via draw:image xlink:href in the OpenDocument backend (4xhp/CVE-2026-105751, tracked on the Oct 5 vm2 page's tracked-list) — two backends, same missing-guard pattern in one product. RAG-ingestion relevant: on authorized AI-platform assessments, feed lab HTML/ODT canaries with a file:// marker reference through every supported upload backend and record which backends resolve local references; proofs stay on your own disposable conversion container.

October 10 follow-up: the injection site is protocol metadata rendered into a filename template — untrusted Caller-ID and Usenet titles reaching exec at download time (FusionPBX, Spotweb)

This page's filename-injection leg always assumed the attacker controls the name at upload. This wave proves the stronger, sneakier shape: the injected name arrives through a non-web protocol field the product happily formats into a download filename or command template, and execution is deferred to a privileged user's routine action.

  • FusionPBX ≤ 5.6.5 unauthenticated Caller-ID → command execution as the web server user (GHSA-9ph6-5hpm-hh94 / CVE-2026-108161, high): with the record_name filename template enabled, the Caller-ID name or number — attacker-supplied on any inbound call, zero credentials — carries $(...) payloads; they execute when a privileged user later downloads multiple call recordings as a ZIP. Delivery is trojan-by-operations: the attacker never touches the web UI; the admin's normal recording-review workflow is the trigger.
  • Spotweb ≤ 1.5.8 command execution via self-signed Usenet spot titles (GHSA-3cfw-8vg9-xhxx / CVE-2026-108546, high): spots published over Usenet (signature required only from the attacker's own key) carry shell metacharacters in the title; the runcommand NZB handler substitutes the title unescaped into $SPOTTITLE and passes the string to exec() when any user downloads the spot.
  • Durable axes: (1) enumerate protocol-adjacent identity fields as injection inputs — Caller-ID name/number, From/Reply-To, Usenet/NNTP headers, SIP display-name, MQTT client-id, Slack display_name. Any admin-side feature that names a file after these fields (recording templates, export names, log-bundle names, ticket attachment names) is this bug until proven otherwise. (2) Deferred trigger = different testing model: your payload fires on someone else's routine action later, so engagement proof is a code-path/parameter-substitution argument (template string reaches exec/system unescaped) plus one lab exec, not a live target probe. (3) Fingerprint preconditions: FusionPBX record_name is opt-in config; Spotweb runcommand is a site policy — confirm config state before reporting, and state the precondition explicitly.
  • Boundaries: place test calls to your own lab PBX extension registration with marker caller-ID, then exercise the ZIP download in the lab; publish nothing to real Usenet/newsservers — an owned local NNTP fixture with a self-signed spot is the proof. Never place spoofed-CID calls against a customer's live DID range.