wintercms / wintercms/winter

Snowboard's Url utility mangles protocol-relative asset URLs, hanging AJAX handlers

Open Beginner friendly
#1,538 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

low priority needs pr
Dominant language
PHP
Stars
1.5k
Forks
246
Avg merge
19h 2m
Merged PRs (30d)
7

Description

Winter CMS Build

dev-develop

PHP Version

8.4

Database engine

MySQL/MariaDB

Plugins installed

Winter.Location, LukeTowers.EasyAudit

Issue description

Snowboard's Url utility does not recognise protocol-relative URLs (//example.com/script.js). Both to() and asset() test the URL against a regex that requires an explicit scheme, and when it doesn't match they strip all leading slashes and resolve the remainder against the site:

https://github.com/wintercms/winter/blob/develop/modules/system/assets/js/snowboard/utilities/Url.js#L27-L56

const urlRegex = /^(?:[^:]+:\/\/)[-a-z0-9@:%._+~#=]{1,256}\b([-a-z0-9()@:%_+.~#?&//=]*)/i;

if (url.match(urlRegex)) {
    return url;
}

const theUrl = url.replace(/^\/+/, '');   // <- strips both slashes of "//host/path"

return `${this.assetUrl()}${theUrl}`;

So //maps.googleapis.com/maps/api/js becomes https://mysite.test/maps.googleapis.com/maps/api/js.

This is mostly invisible until an asset is injected over AJAX, because AssetMaker::getAssetPath() and getAssetScheme() on the PHP side both pass protocol-relative URLs through untouched, so a full page render emits a working <script src="//host/...">. It's only when the same asset arrives via X_WINTER_ASSETS and goes through AssetLoader.loadScript()Url.asset() that it gets rewritten.

The failure is worse than a 404 on an unused asset: AssetLoader.loadScript() rejects its promise when the script errors, and handleUpdateResponse awaits it, so the AJAX handler never completes. In my case that left a popup spinning forever with no visible error other than the 404 in the console.

This affects Winter.Location's AddressFinder form widget out of the box, which registers the Maps API this way:

https://github.com/wintercms/wn-location-plugin/blob/main/formwidgets/AddressFinder.php#L92

$this->addJs('//maps.googleapis.com/maps/api/js?libraries=places&key='.$apiKey);

So any backend form containing an AddressFinder breaks on its first AJAX request. I hit it via LukeTowers.EasyAudit, whose audit log tab opens a popup over AJAX, but any handler on such a form would do it. I'm submitting a PR against that plugin to switch the widget to https://, but the underlying Url behaviour seems worth fixing regardless — a protocol-relative URL is perfectly valid and is being silently turned into a same-origin path.

Observed console output:

GET https://mysite.test/maps.googleapis.com/maps/api/js?libraries=places&key=… net::ERR_ABORTED 404 (Not Found)
    at loadScript (system.js)
    at load (system.js)
    at handleUpdateResponse (framework.js:398)

Uncaught (in promise) Error: Unable to load script file:
"https://mysite.test/maps.googleapis.com/maps/api/js?libraries=places&key=…"
Steps to replicate
  1. In a backend controller, register a protocol-relative asset, e.g. $this->addJs('//code.jquery.com/jquery-3.7.1.min.js');
  2. Trigger any AJAX handler on that page.
  3. Observe the request for https://<site>/code.jquery.com/jquery-3.7.1.min.js returning 404, and the handler's response never being applied.

The same result can be reproduced against the regex alone:

const urlRegex = /^(?:[^:]+:\/\/)[-a-z0-9@:%._+~#=]{1,256}\b([-a-z0-9()@:%_+.~#?&//=]*)/i;
'//code.jquery.com/jquery-3.7.1.min.js'.match(urlRegex);   // null
Workaround

Registering the asset with an explicit scheme (https://…) avoids it, since the URL then matches the regex and is returned unchanged.

Suggested fix

Return protocol-relative URLs unchanged in both to() and asset(), alongside the existing check:

if (url.startsWith('//') || url.match(urlRegex)) {
    return url;
}

Happy to put a PR together for this if the approach looks right — I wasn't sure whether you'd prefer returning it as-is or expanding it to the page's current protocol.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in modules/system/assets/js/snowboard/utilities/Url.js, focusing on the to() and asset() methods and their URL checks. Reproduce the protocol-relative URL case from the issue, then verify that both methods preserve it and that AJAX-loaded assets no longer become same-origin paths or leave the handler hanging.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, php
Domain
frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.