userscripting - #45
Merged
Merged
Conversation
|
✅ No security or compliance issues detected. Reviewed everything up to b750c21. Security Overview
Detected Code ChangesThe diff is too large to display a summary of code changes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
matching pages, managed at runtime from the web UI.
URL (e.g. from Greasyfork), toggle scripts individually, edit a script's
source in place, and uninstall it. A master switch disables the whole
engine without clearing the per-script selection. Each entry shows its
version,
@run-at, match patterns, grant count and@noframesstate, anda script whose stored body no longer parses is called out as not being
injected instead of failing silently.
[userscripts]configuration section holding the masterenabledflagand one
[[userscripts.scripts]]entry per script. As with filter lists,only metadata is stored in the configuration file; bodies live under
userscripts/in the configuration directory (override withPRIVAXY_USERSCRIPT_PATH), keyed by a hash of the source URL sore-installing the same script reuses its file. Configurations written
before this release parse unchanged and default to an enabled engine with
no scripts.
supporting
@match(Chrome match-pattern syntax including<all_urls>and*.hostwildcards),@include/@exclude(globs, or regular expressionswritten
/…/),@exclude-match,@run-at(
document-start/-body/-end/-idle) and@noframes. Exclusions takeprecedence over inclusions. A script declaring neither
@matchnor@include— or with a malformed pattern or no metadata block — is rejectedwith a
422at install time rather than being stored and never firing.GM_info,unsafeWindow,GM_addStyle(nonce-stamped so it survives the page's CSP),
GM_log,GM_openInTab,GM_setClipboard,GM_notification,GM_registerMenuCommandand thepromise-based
GM.*namespace.@requirelibraries and@resourcepayloads are fetched server-side andcached on disk under
userscripts/assets/, keyed by URL hash. Requires areevaluated inside the script's own wrapper ahead of its body, so their
top-level declarations are visible to the script without leaking into the
page, and
GM_getResourceText/GM_getResourceURLread the fetchedresources. Cached assets are never re-fetched (the convention is to pin a
versioned URL); delete the
assetsdirectory to refresh. An asset thatcannot be fetched degrades its script rather than dropping it, and the
failure is reported on the Userscripts page.
GM_setValue/GM_getValue/GM_deleteValue/GM_listValuesare persisted,scoped per script, in
userscripts/gm_storage.json(writes are coalescedon a short debounce and written atomically; the configuration file is
deliberately not used, since these are written far too often to
re-serialize it each time). Because
GM_getValueis synchronous in the GMAPI, values are preloaded into each script's descriptor at injection time
and read from that snapshot in-page, so an ordinary
GM_getValuecosts norequest. Uninstalling a script drops its values.
GM_xmlhttpRequestis relayed server-side, so it has no CORS restrictions— the one capability a real content script cannot have. Three independent
controls gate it: the origin-bound token, the requesting script's own
@connectdeclarations (as Tampermonkey requires, so compatibility isunaffected), and a filter rejecting loopback, RFC1918, carrier-grade NAT,
link-local (including the cloud metadata address) and IPv4-mapped
equivalents. Redirects are followed manually so
@connectand addressfiltering re-run on every hop rather than letting an allow-listed host
bounce the request to
127.0.0.1. Methods are restricted,Hostandhop-by-hop request headers cannot be set, and responses are size-capped.
@resourcepayloads may be binary. They are stored as bytes with thecontent type they were served as; text small enough to matter is inlined
into the script's descriptor so
GM_getResourceTextstays synchronous,while anything binary or oversized is reachable through
GM_getResourceURL, served from the reserved path with its original bytes,content type and
X-Content-Type-Options: nosniff.GM_getResourceTextona binary resource returns
nulland logs a note pointing atGM_getResourceURLrather than returning mojibake.GM_addValueChangeListener/GM_removeValueChangeListenerare implemented.Changes fire locally, reach other same-origin tabs over
BroadcastChannel(free, no request), and reach other origins and other devices behind the
same proxy by polling a read endpoint — which only runs while at least one
listener is registered, so a page with no listeners issues no extra
requests. The read endpoint requires the requesting URL to satisfy the
script's own
@match/@include, so it can never reveal more than thepage's own descriptor already contained.
GM_registerMenuCommandnow has a real surface: a small floating menu,injected only once a script actually registers a command, hosted in a closed
shadow root so neither the page's CSS nor Privaxy's can reach across.
Commands are still reachable from the console via
__privaxyUserscriptMenu().sessionStorage— per-tab by construction, so the proxy needs no notion ofa tab. Other tabs are unaffected;
__privaxyUserscriptsEnableTab()restoresthem, since disabling removes the menu that turned them off.
filter lists and recompiled in place, so upstream changes are picked up
without a restart. The refresh re-reads the configuration from disk rather
than reusing the updater's own copy: userscript changes deliberately bypass
that channel, so its copy is stale with respect to them and recompiling from
it would drop every script installed since startup.
@updateURLand@downloadURLare honored, both defaulting to the URL thescript was installed from. When they differ, only the (small)
@updateURLdocument is fetched to compare
@version, and the body is downloaded solelywhen that version is newer — versions are ordered as dotted numbers, so
1.2.10correctly supersedes1.2.9, falling back to plain inequality forschemes that cannot be ordered. Scripts that split metadata and body, a
common Greasyfork layout, previously re-downloaded the whole body on every
cycle.
instead of waiting out the timer, reporting per script whether it was
updated, already current, or failed and why. Unlike the periodic refresh it
holds the save lock, so a changed
@nameor@versionis persisted.userscripts.allow_private_network_requestssetting (default off, witha toggle on the Userscripts page) permits the relay to reach private
addresses. It is off by default because the relay runs server-side: the
proxy usually sits inside a LAN and can reach routers, admin panels and
metadata endpoints no page could contact. Changes apply immediately, with
no reload.
scripts live in a shared store that each API mutation replaces in place,
and the store is also refreshed on
SIGHUPso a hand-edited[userscripts]section takes effect. Each script is emitted in its ownnonced script element, so a syntax error in one script cannot abandon theothers or the ad-blocking payload, and the CSP nonce is kept in a closure
rather than published on
window.proxy, in the page's main world (a proxy has no isolated world to offer),
which is a wider blast radius than a browser extension installed in one
profile. The Userscripts page says so where scripts are added.
Tampermonkey parity, and some of what is missing is structural rather than
unfinished. Known gaps, so a script that misbehaves can be diagnosed instead
of guessed at:
main world, so
unsafeWindow === window, page scripts can read and clobberanything a userscript leaves reachable, and anti-adblock can detect the
injection. Each script is still wrapped in its own function, so its
var,let,const,functionandclassdeclarations do not leak to the page— only an undeclared assignment or an explicit
window.x = …does.worker serving navigations from cache,
file://,chrome://, browser-cachehits and any traffic not routed through Privaxy are all invisible to it, so
no script runs there. A browser extension sees all of them.
@grantis parsed and displayed but not enforced. Every script receivesevery implemented API regardless of what it declared, including
@grant none. This is deliberately forgiving — a script that forgot todeclare a grant still works — but it is a deviation.
GM_cookie(the proxy has no cookie jar for thebrowser's cookies; it only sees
Cookieheaders in flight),GM_getTab/GM_saveTab/GM_getTabs(no tab identity exists on the proxyside),
GM_download(it would mean the proxy writing files to its own diskon a page's behalf),
GM_addElement, the batchGM_setValues/GM_getValuesforms, and
window.onurlchange.GM_notificationwrites to the console rather than raising a realnotification, and
GM_setClipboardneeds a user gesture like any page-contextclipboard write.
@sandbox,@unwrap,@top-level-await,@icon,@supportURL,@antifeature. Unknown directives are skipped, not rejected.@resourcepayloads are byte-exact but not inlined as text when binary orover 256 KB —
GM_getResourceTextreturnsnullfor those and the data isreachable only through
GM_getResourceURL.GM_xmlhttpRequestis not a transparentXMLHttpRequest. Responses aredecoded as lossy UTF-8, so binary bodies are unusable;
abort()onlysuppresses the callbacks, since the server-side request is already in
flight; there is no
onprogress/onreadystatechange; redirects are cappedat 5 hops, timeouts at 60s (default 30s) and responses at 8 MB; and the
request only reaches hosts the script declared with
@connect.are updated immediately over
BroadcastChannel; a change made on adifferent origin or another device is picked up by a 15s poll, and only
while a listener is registered.
sessionStorage, sodisabling on one site does not disable on another in the same tab.
or fetched
@require/@resourceis capped at 2 MB. Writes are flushed on a500 ms debounce, so values set immediately before a crash can be lost.
configuration order, changeable only by editing the file.
read_configurationunwrapped the parse error, so a hand-edited file with (forexample) a duplicate TOML key panicked a worker on
SIGHUPand killed both theproxy and web-UI loops while the process kept running — both ports stopped
listening with no way back except a restart. The last configuration that parsed
is now kept and reused, so a reload over a broken file logs the error and
carries on serving with the previous settings, then picks up the corrected file
on the next reload. The CA reload in the same path no longer unwraps either.