How Do Userscripts Handle Cross-Origin Requests?
Userscripts bypass the web browser’s standard Same-Origin Policy
(SOP) by leveraging special APIs provided by userscript managers like
Tampermonkey, Violentmonkey, or Greasemonkey. While normal client-side
JavaScript running in a web page is restricted from making
XMLHttpRequests or fetch requests to different domains, userscripts
execute with elevated permissions. By using dedicated API functions—most
notably GM_xmlhttpRequest or
GM.xmlHttpRequest—userscripts delegate cross-origin network
calls to the browser extension background context, bypassing traditional
CORS restrictions while managing security risks through specific header
declarations.
The Same-Origin Policy Barrier
In standard web development, the Same-Origin Policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. An origin is defined by the scheme (protocol), host (domain), and port.
When a standard web page script attempts to fetch data from another
domain, the browser checks for Cross-Origin Resource Sharing (CORS)
headers. If the destination server does not explicitly allow the
requesting origin via the Access-Control-Allow-Origin
header, the browser blocks the response.
Standard Web Page:
[ Script on Domain A ] ---> (Standard fetch/XHR) ---> [ Domain B ] (Blocked by SOP/CORS)
How Userscript Managers Bypass CORS
Userscript managers operate as browser extensions, giving them access to extension-level capabilities that regular web pages do not possess. Extension background scripts are not bound by the Same-Origin Policy in the same way content scripts or page scripts are.
Userscript Architecture:
[ Userscript (Domain A) ]
|
v (Calls GM_xmlhttpRequest)
[ Userscript Manager Extension ]
|
v (Executes Privileged Network Request)
[ Server (Domain B) ]
When a userscript invokes a manager-provided network function, the following process occurs:
- API Interception: The userscript calls a
specialized manager function instead of the native
window.fetchorXMLHttpRequest. - Context Escalation: The request details are passed from the page context to the extension’s background script context via inter-process communication.
- Execution: The extension’s background script executes the HTTP request directly. Because it runs in an extension context, the browser permits cross-origin communication without enforcing standard CORS checks against the active web page’s origin.
- Data Return: The response data is passed back down to the userscript executing on the page.
The GM_xmlhttpRequest API
The primary mechanism for cross-origin requests in userscripts is
GM_xmlhttpRequest (or its modern Promise-based equivalent,
GM.xmlHttpRequest). This function shares a similar
interface with the standard XMLHttpRequest object but
includes extended parameters.
Request Syntax Example
To use cross-origin capabilities, userscripts must explicitly grant
themselves permission using the @grant directive in the
metadata block:
// ==UserScript==
// @name Cross-Origin Data Fetcher
// @namespace http://example.com/
// @version 1.0
// @match https://example.com/*
// @grant GM_xmlhttpRequest
// me ==/UserScript==
GM_xmlhttpRequest({
method: "GET",
url: "https://api.thirdparty.com/data",
headers: {
"Accept": "application/json"
},
onload: function(response) {
console.log("Status:", response.status);
console.log("Data:", response.responseText);
},
onerror: function(error) {
console.error("Request failed:", error);
}
});Modern Async/Await Usage
Modern userscript managers support GM.xmlHttpRequest
(using dot notation), which returns a Promise and can be used seamlessly
with async and await:
// ==UserScript==
// @name Async Fetcher
// @match https://example.com/*
// @grant GM.xmlHttpRequest
// ==/UserScript==
async function fetchData() {
try {
const response = await GM.xmlHttpRequest({
method: "GET",
url: "https://api.thirdparty.com/data"
});
const data = JSON.parse(response.responseText);
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}Security
Considerations and the @connect Directive
Allowing arbitrary cross-origin requests poses significant security risks, such as silent data exfiltration or unauthorized actions on third-party sites where the user is authenticated. To mitigate these risks, modern managers enforce domain restriction policies.
Domain Whitelisting with
@connect
Managers like Tampermonkey and Violentmonkey require scripts to
declare intended cross-origin targets using the @connect
directive in the metadata header.
// ==UserScript==
// @name Secure Request Script
// @match https://example.com/*
// @grant GM_xmlhttpRequest
// @connect api.thirdparty.com
// @connect cdn.anotherdomain.org
// ==/UserScript==Permission Enforcement Rules
- Declared Domains: If a domain is explicitly listed
in
@connect, the manager allows the request without prompting the user. - Subdomains and Wildcards: Developers can use
wildcards (e.g.,
@connect *.example.com) or specify*to request access to all domains. - User Prompts: If a userscript attempts to make a
cross-origin request to a domain not listed in
@connect(or if@connect *is used), the userscript manager displays a dynamic prompt to the user, asking whether to allow or block the request to that specific origin.
Comparison: Native Fetch vs. GM_xmlhttpRequest
| Feature | Native fetch / XHR |
GM_xmlhttpRequest |
|---|---|---|
| Same-Origin Policy | Strictly enforced by browser | Bypassed via extension privileges |
| CORS Header Requirement | Required on target server | Not required |
| Cookie Handling | Subject to credentials mode and SameSite rules |
Can send cookies associated with target domain |
| User Permission | None (constrained by browser sandbox) | Controlled via @grant and @connect |
| Custom Headers | Restricted (e.g., forbidden headers like
User-Agent) |
Less restricted (can override restricted headers) |