11ty / 11ty/eleventy-plugin-bundle
Content-controlled bundle-directive injection via predictable placeholder string
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 83
- Forks
- 8
- PR merge metrics
- No merged PRs in 30d
Description
Summary
getBundle/getBundleFileUrl shortcodes emit a placeholder string (<!--#BaBundle:type:name:bucket:BaBundle#-->) into rendered page content, which a later global transform (OutOfOrderRender.replaceAll()) scans for and replaces across the entire rendered page. The transform has no way to distinguish a placeholder that came from a real shortcode call versus the identical literal string appearing as ordinary page content — so any page whose body/markdown contains that exact string (e.g. typed as plain text, or present in a documentation/code-sample page about this very plugin) gets it replaced too, including triggering a real file write for the file-type placeholder.
CWE: CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine)
Severity: Medium
CVSS: ~4.5 — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L
Root Cause
src/OutOfOrderRender.js:
#regex = /((?:\<\!\-\-)#BaBundle:[^:]*:[^:]*:[^:]*:BaBundle#(?:\-\-\>))/;
findAll() {
let regex = this.#regex;
let matches = this.content.split(regex); // scans ALL rendered content, no origin tracking
...
}
replaceAll() then unconditionally processes every regex match — for type: "get" it substitutes bucket content; for type: "file" it calls manager.writeBundle(), which performs a real fs.writeFileSync.
Verification
Dynamically confirmed against the actual installed module — content substitution (breaks HTML structure):
const manager = new CodeManager("css");
manager.addToPage("/page1/", "body { color: red; } /* SITE-WIDE-CSS-SECRET */", "default");
const legitPlaceholder = OutOfOrderRender.getAssetKey("get", "css", "default");
// legitPlaceholder = "<!--#BaBundle:get:css:default:BaBundle#-->"
const content = `<style>${legitPlaceholder}</style><p>Attacker typed this literal string as plain content: ${legitPlaceholder}</p>`;
const renderer = new OutOfOrderRender(content);
renderer.setAssetManager("css", manager);
await renderer.replaceAll({ url: "/page1/" });
Output:
<style>body { color: red; } /* SITE-WIDE-CSS-SECRET */</style><p>Attacker typed this literal string as plain content: body { color: red; } /* SITE-WIDE-CSS-SECRET */</p>
The second occurrence — plain text, no shortcode call — was replaced identically to the real one, landing bundle content inside a <p> tag.
A type: "file" variant of the same literal string in plain page content was independently confirmed (in the audit this issue is based on) to trigger a real fs.writeFileSync via manager.writeBundle().
Scope
Bundle lookups are keyed by the page currently being transformed, so this does not let one page pull content queued for a different page — impact is limited to duplicating/relocating content that page's own build context already legitimately queued (e.g. shared site-wide CSS/JS from a common layout) into an unintended location, and forcing an extra, attacker-triggerable (but deterministic/same-content) file write.
Recommended Fix
Make the placeholder unpredictable/unguessable (e.g. include a per-build random token or HMAC that only the plugin's own shortcode invocations can produce), so ordinary page content can never coincidentally match it, rather than relying on a fixed literal string pattern.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/OutOfOrderRender.js, especially the placeholder regex, findAll(), and replaceAll(), and trace how shortcode-generated placeholders reach the transform. Verify that literal page content no longer triggers substitution or file writes while legitimate placeholders still resolve correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100