TL;DR
Pluck CMS 4.7.20 and earlier ships a stored cross-site scripting flaw in its blog module (CVE-2026-78746). When an administrator saves a blog post, the content is stored through a sanitiser that is quietly told not to encode HTML. The raw markup is then echoed straight into the page when anyone reads the blog. An authenticated admin who pastes <img src=x onerror=alert(1)> into a post turns that post into a script that runs in every visitor's browser. Block8.ai's Security Research Team rates it CVSS 3.1 of 4.8 (Medium), CWE-79.
What a blog module is supposed to do
A blog module has one modest job: take some text an author types, keep it safe on disk, and show it to readers later. The author expects formatting to survive. The reader expects to read, not to be reprogrammed.
The trust assumption hiding in there is simple. The author is allowed to write words. The reader's browser should only ever display those words, never treat them as instructions. The moment a reader's browser starts executing what an author typed, the whole arrangement has quietly changed from "publishing" to "remote control".
Pluck CMS gets this subtly, importantly wrong.
Where the trust breaks
Pluck does have a sanitiser. It is called sanitizePageContent(), and it is called when a post is saved by blog_save_post() in data/modules/blog/functions.php. So far, so reassuring.
The problem is the second argument:
sanitizePageContent($content, false)
That false is the HTML flag. When it is false, the function does the bookkeeping you would want for safely writing the content into a PHP data file, escaping backslashes and single quotes, but it skips the one step that matters for the reader: htmlspecialchars(). Nothing turns < into <. The HTML tags you typed stay exactly as HTML tags.
So the content lands on disk raw. Here is a confirmed stored payload from a test instance, sitting in data/settings/modules/blog/posts/1.block8xss.php:
$post_content = '<img src=x onerror=alert("BLOG_B8")>';
That is not escaped text waiting to be displayed. That is a working script, parked and ready.
Under the bonnet
The second half of the bug is on the output side, in blog_theme_main() within data/modules/blog/blog.site.php. When a visitor loads any page that embeds the blog module, the stored content is written to the response like this:
echo $post['content'];
No encoding on the way in. No encoding on the way out. The <img> tag is parsed by the browser as a real image element, the bogus src=x fails to load, the onerror handler fires, and the attacker's JavaScript executes in the context of the site.
Reproducing it is short work:
- Log in as admin.
- Visit any page that shows the blog module.
- The
onerrorhandler fires. In testing,alert("BLOG_B8")rendered raw in the HTML and ran.
POST to admin.php?module=blog&page=editpost with:
cont1=testpost&cont2=General&cont3=<img src=x onerror=alert("XSS")>&save_exit=Save
The cont3 parameter is the post body. The form handler for all of this is blog_page_admin_editpost() in blog.admin.php, which hands the content down into blog_save_post() and that fateful false.
This is the same class of mistake as CVE-2026-31205, which covered the page editor via editpage.php. Same root cause, different doorway. This one lives entirely inside the blog module's own content path, so patching the page editor does not close it.
Why it matters
Stored XSS is the patient kind. The payload is written once and then served to everyone, no further interaction from the attacker required. Because the script runs with the privileges of the site in the visitor's browser, the practical outcomes include session token theft, credential phishing dressed up in the real site's chrome, defacement, and drive-by malware delivery.
There is a privilege cost to triggering it: you need an authenticated administrator account to author the post, which is why the CVSS base score sits at 4.8 rather than higher. But the blast radius is every reader of the blog, and the CVSS scope is marked changed for exactly that reason. One compromised or careless admin account becomes a problem for the entire audience.
How to fix it
The fix is small and specific: stop telling the sanitiser to skip HTML encoding for content that will be echoed into a page.
- Call
sanitizePageContent()with the HTML flag set totruefor blog content, or applyhtmlspecialchars()explicitly at the output point inblog.site.phpbefore theecho. - If the product genuinely needs to allow some markup in posts, switch to an allowlist-based HTML sanitiser rather than passing raw input through untouched. A boolean "encode / don't encode" switch is the wrong tool for "allow bold but not scripts".
- Encode on output as a rule, not just on input. Output encoding is the layer that actually protects the reader.
Until a patched release is available, operators should treat blog authorship as a sensitive capability and restrict it to trusted accounts.
Closing
This one is a good reminder that a sanitiser you called is not the same as a sanitiser that ran. The function was right there in the code path, invoked on exactly the right data, and still let a live script through because of a single false. The Block8.ai Security Research Team flagged it during authorised research into Pluck CMS; the lesson travels well beyond this one module. When user content ends up in a page, encode it on the way out, every time, and never trust a flag to remember to do it for you.