Need (?) to encode backslashes in the query string

This follows on from the disappearing resource tree thread. In desperation, I start a new topic, now that the hosting company is putting the blame on MODX for not encoding backslashes in its query strings (assuming that is indeed the case, and not just an unjustified slur).

According to the hosting company, LiteSpeed considers backslashes in query strings a security threat. However, if they are encoded (%5C), the query runs normally (and so the resource tree reappears).

The advice I have been given is this:

The best course of action is patching the MODX to use URL encoded backslashes instead of explicit ones in the query string.

It may be possible to catch the query string being written and doing a search and replace manually. This can be shown by injecting the following in a developer console for the page:

(function() {
    function fixUrl(url) {
        if (typeof url === 'string' && url.indexOf('\\') !== -1) {
            return url.replace(/\\/g, '%5C');
        }
        return url;
    }
    var origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        arguments[1] = fixUrl(url);
        return origOpen.apply(this, arguments);
    };
})();

This just replaces \ with the URL encoding of it (%5C) whenever a HTTP request is generated by the page. If added in the console then the refresh icon on the resources is clicked then the pages load in the list.

The hosting support person kindly wrote the following plugin:

if ($modx->context->key !== 'mgr') {
    return;
}

/** @var \MODX\Revolution\Controllers\modManagerController $controller */
$controller = $scriptProperties['controller'];
if (!$controller) {
    return;
}

$js = <<<'EOD'
<script>
(function() {
    function fixUrl(url) {
        if (typeof url === 'string' && url.indexOf('\\') !== -1) {
            return url.replace(/\\/g, '%5C');
        }
        return url;
    }

    var origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        arguments[1] = fixUrl(url);
        return origOpen.apply(this, arguments);
    };

    if (window.fetch) {
        var origFetch = window.fetch;
        window.fetch = function(input, init) {
            if (typeof input === 'string') input = fixUrl(input);
            return origFetch.call(this, input, init);
        };
    }
})();
</script>
EOD;

$controller->addHtml($js);

Apparently, the plugin needs to be bound to the OnManagerPageBeforeRender system event, which injects the script immediately into the page.

The questions that occur to the uninitiated in these dark arts are two:

  1. Does this even make sense?

  2. If it does make sense, what needs to be done to make it work? At the moment, I have that plugin in the plugins list, but still no resources visible.

This appears to work as a fix for me after a quick test.

In the plugin - did you click the System Events tab?

In there - find OnManagerPageBeforeRender and check it, then save.

I hadn’t noticed the deactivation option for the plugin. Now I see it. I activate the plugin, then empty the cache, refresh and…BAM!..resource tree back.

Happy. And yet not so happy at the prospect of having to add that plugin to the 59 other websites that I am responsible for.

Hi, tried your plugin and it works for the resources tree, but found there is still problem when changing resource template. It trow error 500 with http2 error. Do you have same?

EDIT

so this is example path after changing my template for resourc with ID:2:
/manager/?a=resource/update&id=2&reload=6a6ce409e7e5a1.67689763&class_key=MODX**\Revolution\**modDocument&context_key=web

there are 2 backslashes in this url so it means the LiteSpeed problem exists in all Modx Ajax calls + POST/GET requests. If i change them manually with %5C site load and works.

This is fixed plugin (with Gemini)

/**
 * Plugin: LiteSpeedHttp2Fix
 * Events: OnManagerPageInit
 * 
 * Fixes HTTP/2 ERR_HTTP2_PROTOCOL_ERROR on LiteSpeed servers caused by 
 * unencoded backslashes (\) in URLs generated by MODX 3 / PHP 8 namespaces.
 */

if ($modx->context->key !== 'mgr') {
    return;
}

/** @var \MODX\Revolution\Controllers\modManagerController $controller */
$controller = $scriptProperties['controller'] ?? $modx->controller ?? null;

if (!$controller) {
    return;
}

$js = <<<'EOD'
<script>
(function() {
    'use strict';

    // Helper function to replace backslashes with %5C in URLs
    function sanitizeUrl(url) {
        if (!url) return url;
        if (typeof url === 'string' && url.indexOf('\\') !== -1) {
            return url.replace(/\\/g, '%5C');
        }
        if (typeof URL !== 'undefined' && url instanceof URL && url.href.indexOf('\\') !== -1) {
            return new URL(url.href.replace(/\\/g, '%5C'));
        }
        return url;
    }

    // 1 & 2. Intercept XMLHttpRequest & Fetch API (Standard background requests)
    if (window.XMLHttpRequest) {
        var origOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            var args = Array.prototype.slice.call(arguments);
            args[1] = sanitizeUrl(url);
            return origOpen.apply(this, args);
        };
    }

    if (window.fetch) {
        var origFetch = window.fetch;
        window.fetch = function(input, init) {
            if (typeof input === 'string' || (typeof URL !== 'undefined' && input instanceof URL)) {
                input = sanitizeUrl(input);
            } else if (typeof Request !== 'undefined' && input instanceof Request && input.url.indexOf('\\') !== -1) {
                input = new Request(sanitizeUrl(input.url), input);
            }
            return origFetch.call(this, input, init);
        };
    }

    // 3. Intercept window.open
    if (window.open) {
        var origWindowOpen = window.open;
        window.open = function() {
            var args = Array.prototype.slice.call(arguments);
            if (args[0]) args[0] = sanitizeUrl(args[0]);
            return origWindowOpen.apply(this, args);
        };
    }

    // 4. Intercept traditional HTML form submissions
    document.addEventListener('submit', function(e) {
        if (e.target && e.target.action) {
            e.target.action = sanitizeUrl(e.target.action);
        }
    }, true);

    /* --- NAVIGATION & HARD RELOAD FIXES --- */

    // 5. Intercept MODX router (ExtJS). Handles page reloads containing class_key parameters.
    // Poll for MODx object availability as it loads asynchronously.
    var patchModx = function() {
        if (typeof MODx !== 'undefined' && MODx.loadPage && !MODx.loadPage.isPatched) {
            var origLoadPage = MODx.loadPage;
            MODx.loadPage = function(action, parameters) {
                if (typeof parameters === 'string') {
                    parameters = sanitizeUrl(parameters);
                } else if (typeof parameters === 'object' && parameters !== null) {
                    for (var key in parameters) {
                        if (typeof parameters[key] === 'string') {
                            parameters[key] = sanitizeUrl(parameters[key]);
                        }
                    }
                }
                return origLoadPage.call(this, action, parameters);
            };
            MODx.loadPage.isPatched = true;
            clearInterval(modxInterval);
        }
    };
    var modxInterval = setInterval(patchModx, 50);
    setTimeout(function() { clearInterval(modxInterval); }, 5000); // Stop polling after 5 seconds

    // 6. Global click listener for <a> tags
    document.addEventListener('click', function(e) {
        var target = e.target.closest('a');
        if (target && target.href && target.href.indexOf('\\') !== -1) {
            target.href = sanitizeUrl(target.href);
        }
    }, true);

    // 7. Intercept programmatic redirects via window.location
    if (window.location.assign) {
        var origAssign = window.location.assign;
        window.location.assign = function(url) {
            return origAssign.call(window.location, sanitizeUrl(url));
        };
    }
    
    if (window.location.replace) {
        var origReplace = window.location.replace;
        window.location.replace = function(url) {
            return origReplace.call(window.location, sanitizeUrl(url));
        };
    }

})();
</script>
EOD;

$controller->addHtml($js);

thanks @Cottagestuff for this, working well. Had 3 sites effected so far.

Do we know if a core fix is planned?

Thanks, the updated code works well.

The recommended system event did not work for me, but onManagerPageBeforeRender works fine. Thank you!

Well, it works to solve the “Resource Tree not appearing” issue. However, when I attempt to create a new resource, I get an HTTP2 error:

This site can’t be reached

The webpage at https://etherweave.com/manager/?id=5&a=resource/create&class_key=MODX\Revolution\modDocument&parent=5&context_key=web might be temporarily down or it may have moved permanently to a new web address.

ERR_HTTP2_PROTOCOL_ERROR

This seems to be related to the same fundamental issue, but is deeper in the CMS in a core .js script or something? I can’t tell.

UPDATE: I am able to successfully create a new resource if I select the “New Page” link from the Dashboard; however, if I try to change the default template, I get the error. The error always appears when I attempt to create a child resource in the tree (using the ‘+’ option).

UPDATE: The above revised plugin script successfully resolved this issue of changing the template, which is good, but I still get the same error when attempting to add a new resource using the ‘+’ option in the tree itself.