Skip to content

🐛 Make native GM_download honor @connect, like GM_xmlhttpRequest#1506

Open
DudeAint wants to merge 1 commit into
scriptscat:mainfrom
DudeAint:fix/native-gm-download-connect
Open

🐛 Make native GM_download honor @connect, like GM_xmlhttpRequest#1506
DudeAint wants to merge 1 commit into
scriptscat:mainfrom
DudeAint:fix/native-gm-download-connect

Conversation

@DudeAint

@DudeAint DudeAint commented Jun 12, 2026

Copy link
Copy Markdown

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

GM_xmlhttpRequest runs every request through a confirm check (@connect, blacklist, site-access).
GM_download in native mode does its request by calling GM_xmlhttpRequest directly:

// 如果downloadMode为native则走GM_xmlhttpRequest
if (params.downloadMode === "native") {
  return this.GM_xmlhttpRequest(request satisfies GMApiRequest<[GMSend.XHRDetails?]>, sender);
}

That call skips handlerRequest, so the check never runs. Since native is the default, a script with
@grant GM_download can fetch a host outside its @connect list (which GM_xmlhttpRequest would refuse)
and skip the blacklist. Native download is GM_xmlhttpRequest underneath, so it should get the same check.

Fix: extracted the check into a private verifyXhrConnect and run it from both APIs; GM_download
only for downloadMode === "native" (browser/blob unchanged).

Behavior change: native downloads now show the site-access prompt for un-granted hosts, same as
GM_xmlhttpRequest.

Tested: a native GM_download to a non-@connect host is now refused (was downloading); @connect'd
and browser/blob downloads still work. tsc/eslint/tests green.

File Change
src/app/service/service_worker/gm_api/gm_api.ts Extract verifyXhrConnect; gate native GM_download.

Screenshots / 截图

Native-mode GM_download calls GM_xmlhttpRequest directly to perform the
cross-origin request, which skipped GM_xmlhttpRequest's @connect / blacklist /
site-access checks. Extract that check into verifyXhrConnect and have native
download reuse it, so both paths behave the same.
@CodFrm

CodFrm commented Jun 12, 2026

Copy link
Copy Markdown
Member

This looks acceptable to me.

@cyfung1031 cyfung1031 self-requested a review June 12, 2026 09:05
@cyfung1031

cyfung1031 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

This looks acceptable to me.

@CodFrm No need for Tampermonkey compatibility? The following script can run without @connect.

@DudeAint Currently it is intended to enforce @connect to xhr/fetch (via GM_xhr) only. This is to match the same behaviour as TM. This is well documented in TM docs - https://www.tampermonkey.net/documentation.php?locale=en&q=connect. If we check in GM_download as well, it is a breaking change to existing usage with GM_download.

// ==UserScript==
// @name         GM_download API Demonstration
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a floating button to download a file using GM_download
// @author       You
// @match        *://*/*?test_GM_download
// @grant        GM_download
// ==/UserScript==

(function() {
    'use strict';

    // 1. Create a floating button on the page
    const btn = document.createElement('button');
    btn.textContent = '🚀 Download File';
    btn.style.position = 'fixed';
    btn.style.top = '20px';
    btn.style.left = '20px';
    btn.style.zIndex = '99999';
    btn.style.padding = '10px 15px';
    btn.style.backgroundColor = '#007bff';
    btn.style.color = '#fff';
    btn.style.border = 'none';
    btn.style.borderRadius = '5px';
    btn.style.cursor = 'pointer';
    btn.style.fontWeight = 'bold';

    document.body.appendChild(btn);

    // 2. Set up the download event
    btn.addEventListener('click', () => {
        btn.textContent = '⏳ Downloading...';
        btn.disabled = true;

        GM_download({
            url: 'https://www.w3.org/assets/logos/w3c/w3c-no-bars.svg',
            name: 'w3c-logo-downloaded.svg',
            saveAs: false, // Set to true if you want the browser's "Save As" prompt to appear
            onload: function() {
                alert('Download completed successfully!');
                btn.textContent = '🚀 Download File';
                btn.disabled = false;
            },
            onerror: function(error) {
                alert('Download failed: ' + error.error);
                btn.textContent = '❌ Failed';
                btn.disabled = false;
                console.error(error);
            },
            onprogress: function(progress) {
                if (progress.lengthComputable) {
                    const percent = Math.round((progress.loaded / progress.total) * 100);
                    btn.textContent = `⏳ ${percent}%`;
                }
            },
            ontimeout: function() {
                alert('Download timed out!');
                btn.textContent = '🚀 Download File';
                btn.disabled = false;
            }
        });
    });
})();

@cyfung1031

cyfung1031 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

@CodFrm @DudeAint

Personally speaking, @connect seems a rubbish design for me. Only TM implemented this and no one else follow except ScriptCat. Why it is rubbish design? Because the UserScript can directly do fetch(...) or new XMLHttpRequest in your pages. There is no guard. So what is the meaning for blocking the usage for network API only?

The use of API is to avoid the cookie sending or doing cross domain request. In modern design, all the network API endpoints should have token/bearer to protect the unauthorized usage. Even you can do the cors request, the script still cannot steal your personal information or do something danger on behalf of you.

ScriptCat just follow Tampermonkey to do these silly things. And I can tell you that the deeper blockage is not implemented in SC like TM. In the past year my focus is just to fix the compatibility issues to make ScriptCat can be used - not to duplicate Tampermonkey. For example, don't let the @connect silly things affect the normal working scripts.

Don't need to bother with those security things too much. There is no security once you have installed the malware userscripts.

Or can we tell users that, if the script does not declare @connect, then your script will not access the external resources? The answer is "no, we cannot say this".

@CodFrm

CodFrm commented Jun 13, 2026

Copy link
Copy Markdown
Member

@cyfung1031

@CodFrm No need for Tampermonkey compatibility? The following script can run without @connect.

From a code-execution standpoint this doesn't actually break TM compatibility, so I think it's acceptable.

From a security angle, native GM_download currently bypasses the cross-origin restriction entirely, so I think the check is warranted.

Personally speaking, @connect seems a rubbish design for me. ... So what is the meaning for blocking the usage for network API only?

Even without @connect there would have to be something gating access — much like a browser extension's host_permissions. Plain fetch/XMLHttpRequest can't bypass CORS (the browser is strict about reading cross-origin responses), which is precisely why GM_xmlhttpRequest exists — to break through that restriction. And it's because it can break through that @connect was introduced.

I don't think that's a bad design — I think it's a necessary one. Otherwise, while you're on x.com, a malicious script could directly reach youtube.com, grab your logged-in account info, and use it to push ads, all without your knowledge. With @connect, you can see at a glance which sites a script is allowed to reach, and spot anything suspicious.

Or can we tell users that, if the script does not declare @connect, then your script will not access the external resources? The answer is "no, we cannot say this".

At the very least we can tell users that the script won't do anything that affects sites other than the current one — fetch/XMLHttpRequest can't break through the browser's CORS restriction to read cross-origin data, as in my example above.

@cyfung1031

cyfung1031 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

From a code-execution standpoint this doesn't actually break TM compatibility, so I think it's acceptable.

If this PR merges, the following will only work in TM not SC.

// ==UserScript==
// @name         GM_download API Demonstration
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a floating button to download a file using GM_download
// @author       You
// @match        *://*/*?test_GM_download
// @connect     abc.com
// @grant        GM_download
// ==/UserScript==

(function() {
    'use strict';

    // 1. Create a floating button on the page
    const btn = document.createElement('button');
    btn.textContent = '🚀 Download File';
    btn.style.position = 'fixed';
    btn.style.top = '20px';
    btn.style.left = '20px';
    btn.style.zIndex = '99999';
    btn.style.padding = '10px 15px';
    btn.style.backgroundColor = '#007bff';
    btn.style.color = '#fff';
    btn.style.border = 'none';
    btn.style.borderRadius = '5px';
    btn.style.cursor = 'pointer';
    btn.style.fontWeight = 'bold';

    document.body.appendChild(btn);

    // 2. Set up the download event
    btn.addEventListener('click', () => {
        btn.textContent = '⏳ Downloading...';
        btn.disabled = true;

        GM_download({
            url: 'https://www.w3.org/assets/logos/w3c/w3c-no-bars.svg',
            name: 'w3c-logo-downloaded.svg',
            saveAs: false, // Set to true if you want the browser's "Save As" prompt to appear
            onload: function() {
                alert('Download completed successfully!');
                btn.textContent = '🚀 Download File';
                btn.disabled = false;
            },
            onerror: function(error) {
                alert('Download failed: ' + error.error);
                btn.textContent = '❌ Failed';
                btn.disabled = false;
                console.error(error);
            },
            onprogress: function(progress) {
                if (progress.lengthComputable) {
                    const percent = Math.round((progress.loaded / progress.total) * 100);
                    btn.textContent = `⏳ ${percent}%`;
                }
            },
            ontimeout: function() {
                alert('Download timed out!');
                btn.textContent = '🚀 Download File';
                btn.disabled = false;
            }
        });
    });
})();

@CodFrm

CodFrm commented Jun 13, 2026

Copy link
Copy Markdown
Member

From a code-execution standpoint this doesn't actually break TM compatibility, so I think it's acceptable.

If this PR merges, the following will only work in TM not SC.

I understand. Alternatively, could we change GM_download so that it shows a confirmation dialog and lets the user choose?

I’d like to close this security gap.

@cyfung1031

Copy link
Copy Markdown
Collaborator

From a code-execution standpoint this doesn't actually break TM compatibility, so I think it's acceptable.

If this PR merges, the following will only work in TM not SC.

I understand. Alternatively, could we change GM_download so that it shows a confirmation dialog and lets the user choose?

I’d like to close this security gap.

up to you.
please update the docs as well. because this is different from Tampermonkey.

Tampermonkey is intended to do it for xhr/fetch only. not for download.

@cyfung1031 cyfung1031 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Comment to Accept or Reject. Respect CodFrm's decision.

Remarks:

  • Breaking Change and leading incompatibilities to Tampermonkey scripts.
  • Requires Documentation Update
  • CodFrm / DudeAint shall test with different @connect declaring cases to confirm no issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants