Open dotproto opened 1 year ago
Well, I can physically make such a site myself, so I physically can cite a URL. It's just meaningless because it won't change the fact that it's physically possible. There are sites who don't like extensions e.g. those that can help the visitors cheat if it's a game or a test or something more sensitive, so they do have an incentive to do extra stuff to fight extensions.
As for the sites dig, I guess you're trying to make a joke about my imprecise usage of the term "site", but it's widely used (I preemptively decline a citation request) to denote the contents of a local browser's tab, so naturally it can remove anything from the tab's DOM where the web page of the web site is shown.
Well, I can physically make such a site myself, so I physically can cite a URL.
No, you can't.
You will need to demonstrate how you will make a Web site which will remove the <iframe>
that I append to the document
I am viewing in my browser on my machine.
Well, I can physically make such a site myself, so I physically can cite a URL.
Further, per your claim the Web site can remove any code whatsoever, HTML, CSS, JavaScript, etc., whatever you consider to be a "secure" implementation of communication - including chrome
itself
chrome = null;
Now what do you do?
No, you can't.
I can. You seem to keep milking the joke about differentiating remote sites and their local rendition in a browser tab. Here's the simplest demo of such a web site html:
<script>addEventListener('DOMNodeInserted', e => e.target.remove())</script>
A more "advanced" version would also remove the stuff added by a content script at document_start:
<script>
document.documentElement.querySelectorAll('*').forEach(el => {
if (el !== document.head) el.remove();
});
addEventListener('DOMNodeInserted', e => e.target.remove());
</script>
Next the site can show its own elements and keep track of them in an internal WeakSet.
chrome = null; Now what do you do?
Assuming it's in the web page's MAIN world, it doesn't affect the isolated world of the content scripts.
Well, I can just remove that code.
It will be obfuscated in the real web page and it'll contain the main functionality of the page, so removing it is not an option. What an extension can do is to preemptively cloak the various DOM methods.
It will be obfuscated in the real web page and it'll contain the main functionality of the page, so removing it is not an option. What an extension can do is to preemptively cloak the various DOM methods.
No it won't.
There is no obfuscation for hackers.
It will be interspersed with the real code of the web page that has already been executed, so removing the script element after the fact won't help. Cloaking the DOM methods would help, but there's a lot to cover, especially if we include the possibility of a web site using a fresh src-less iframe to extract the original un-cloaked methods and .call them on the main document. Anyway, I agree it's not something most extensions need to worry about.
I think it is important to point out while the process I use to stream data does work, it could be written out to avoid using an iframe
, and be ergonomic and overt to the developer.
Technically you can just fetch()
whatever data you want using fetch('chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/path/to/resource')
or import('chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/path/to/resource')
on any arbitrary Web site to avoid iframe
altogether with "web_accessible_resources"
. The only site where I have had issues doing that is Twitter.
That is why the use-case(s) description must be detailed and precise, so Chromium authors can't be ambiguous in their specification language and implementation should we get that far.
You have to be real pecise when dealing with technical documents. E.g., Chrome claims to capture "system audio" for getDisplayMedia()
, yet there are exceptions which I requested be disclosed to the developers at large, yet the reply was a "small percentage" of users would even be aware of speechSynthesis
https://github.com/GoogleChrome/developer.chrome.com/pull/3947 - until you try to capture speechSynthesis.speak()
yourself for a requirement.
So, I would be very technical about exactly what is being requested here for a feature - with use cases and non-goals clearly spelled out so confusion as to scope cannot rationally be claimed.
Dang, y'all got into quite a bit of a discussion. I don't have time to catch up on it right now, but I swung by to add some notes and figured I should address one point that stood out in passing.
Goal of the demo transfer it to a page's main world
It's the wrong goal in the context of the linked issue.
I was initially aiming to tackle what I expected to be the more complicated version of the same basic use case; I figured if we could solve for passing a Blob to a main world content script, then we'd probably also solve for passing a Blob to an isolated world content script as well.
Unfortunately, my initial experiments seem to indicate that transferables cannot be sent across origin. At the moment my planned work around is to create object URL in the extension iframe on the page. That may change after I read through the full discussion in the comments here.
my initial experiments seem to indicate that transferables cannot be sent across origin
Yes they can be, if we are talking about from chrome://<id>
to arbitrary Web page, and vice versa.
@tophf
Reviewing my own approach, I don;t think we have to keep the iframe
in the Web page once we transfer the ReadableStream
to the Web page. So the iframe
would only serve to transfer the ReadableStream
to the Web page then we can remove it ourselves, which should take 1 second or so.
I'll test later today or tomorrow.
I would just transfer port2 of a MessageChannel as you already suggested, so that it can be also used for future communication.
@tophf Is there any reason you don't use "externally_connectable"
?
The linked issue is about an entirely different use case: a content script. Content scripts run in an isolated world which cannot be spoofed or intercepted or broken by the web page via getters/setters on various global types and prototypes.
my initial experiments seem to indicate that transferables cannot be sent across origin
Yes they can be, if we are talking about from
chrome://<id>
to arbitrary Web page, and vice versa.
Thanks for the clarification. I was experimenting with websites, not extension contexts. I should have known better 😅
The linked issue is about an entirely different use case: a content script. Content scripts run in an isolated world which cannot be spoofed or intercepted or broken by the web page via getters/setters on various global types and prototypes.
Doesn't really matter. You can stream data using message passing. There is nothing special about a Blob
. JSON can be streamed. Does "content script" exclude chrome.scripting.executeScript()
?
You asked about externally_connectable, which is about the opposite task: collaborating with the main world of the page.
I am trying to understand exactly what you are trying to achieve that you can't now.
In the content script you can just do
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// do stuff
});
in the ServiceWorker
chrome.action.onClicked.addListener(async (tab) => {
const [{ id }] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
chrome.tabs.sendMessage(id, { name: 'blob' }, async (message) => {
console.log(message);
});
});
We can serialize data of you really have a Blob
or TypedArray
using TextEncoder()
and deserialize with TextDecoder()
.
The linked issue is about transferring huge amounts of data "instantly", i.e. in a matter of a few milliseconds, while staying wholly within the isolated world so that nothing in the main world can see the data or break the communication.
It means that current chrome
messaging is immediately out of the question because it would take like a minute to transfer 1GB of JSON-compatible binary data on an average not-so-fast notebook.
The workaround via a web_accessible_resources iframe is practically acceptable for most extensions that can accept the theoretical risks of being detected/broken from the main world and the added 50+ms overhead to create the iframe environment. To clarify "detection": assuming we use a closed ShadowDOM the web page can only detect that an empty element was added, whereas a content script of another extension can peek into the closed ShadowDOM and see there's an iframe (it won't be able to peek into iframe though without resorting to chrome.debugger API which is an entirely different story).
The linked issue is about transferring huge amounts of data "instantly", i.e. in a matter of a few milliseconds, while staying wholly within the isolated world so that nothing in the main world can see the data or break the communication.
It means that current chrome messaging is immediately out of the question because it would take like a minute to transfer 1GB of JSON-compatible binary data on an average not-so-fast notebook.
Are you sure you tested that?
What data are you transferring from the ServiceWorker
to the content script where the transfer needs to be 1GB or more in under 1 second and instantaneous?
What is the use case?
I can stream real-time audio using data URL's or JSON with onMessage
. Streaming audio ain't easy. It is easy to detech glitches, gaps in playback.
What I would do is transfer a FileSystemFileHandle
to the ServiceWorker
, write to the file, then either close the stream or transfer the FileSystemFileHandle
back to the content script, call getFile()
then call arrayBuffer()
on the File
object.
Or transfer a WebAssembly.Memory
object and write and read essentially in parallel.
I am not sure why the context restriction is content scripts, which are inherently unreliable.
For more reliability you can just open the document listed in "web_accessible_resources"
as a Tab, then use fetch()
, intercept the request and send data back using respondWith()
.
The
on an average not-so-fast notebook
criteria will remain the same using any approach.
@dotproto
Changing the name to "Sample: Transfer a blob from a background context to a page" is still not techically accurate, because Blob
s are not transferable objects. The last time I checked Chrome still had not implemented Blob
type for WebRTC data channels.
The sample request could be solved at once by various means:
"web_accessible_resources"
and apply those to allowlist BroadcastChannel
from chrome-extension://"web_accessible_resources"
and apply those to allowlist the content script as a WindowClient
of MV3 ServiceWorker
Either will provide a means to communicate between the content script/Web page and the ServiceWorker
.
A content script as WindowClient
will alos allow intercepting fetch()
requests so we can implement half-duplex streaming.
The BroadcastChannel
solution appears to be the simplest to implement in extension source code, as ServiceWorker
was not designed for extensions.
The linked issue is about transferring huge amounts of data "instantly", i.e. in a matter of a few milliseconds, while staying wholly within the isolated world so that nothing in the main world can see the data or break the communication. The workaround via a web_accessible_resources iframe is practically acceptable for most extensions that can accept the theoretical risks of being detected/broken from the main world and the added 50+ms overhead to create the iframe environment. To clarify "detection": assuming we use a closed ShadowDOM the web page can only detect that an empty element was added, whereas a content script of another extension can peek into the closed ShadowDOM and see there's an iframe (it won't be able to peek into iframe though without resorting to chrome.debugger API which is an entirely different story).
There is nothing in developer.chrome that specifies a means to transfer data between MV3 ServiceWorker
and content script, or Web page contexts.
Blob
s are not transferable.
So this issue amounts to a feature request.
If the concern is an arbitrary Web page running code continuously to check check and see if an iframe
is appended to the document
, which I have not observed in the wild whatsoever, you can open a new window
, which the Web page has no control over observing or closing and achieve the same transfer of data between iframe
and Web page.
Something like I do here https://github.com/guest271314/sw-extension-audio/blob/main/background.js
const createAudioWindow = async (notification = false) => {
let url = chrome.runtime.getURL('audio.html');
if (notification) {
url += '?notification=1';
}
console.log(url);
({ id } = await chrome.windows.create({
type: 'popup',
focused: false,
top: 1,
left: 1,
height: 1,
width: 1,
url,
}));
await chrome.windows.update(id, { focused: false });
return id;
};
Now you can communicate using opener.postMessage()
from the window
and onmessage
in the Web page, then close the window
when done with the transfer.
Evidently setSelfAsOpener
option of chrome.windows.create()
doesn't work with ServiceWorker
, even though it is not documented to throw.
Per this https://stackoverflow.com/a/72124984 Chrome maximum ArrayBuffer
size is 2145386496 (1.998 GB).
new ArrayBuffer(1072693248)
half of the maximum value per that test crashed the window
on *nix.
WebAssembly.Memory
can grow()
, the last time I checked was limited as well, though there was activity to increase to 4GB https://v8.dev/blog/4gb-wasm-memory, https://github.com/WebAssembly/spec/issues/1116.
new Array(536346624)
.499 GB did not throw using the following approach; was not instant. You should be able to run the test linked above between 536346624 and 1072693248 to determine when Chrome or Chromium will crash on your machine. The sole purpose is to transfer data from ServiceWorker
to content script (or Web page) then close the window
manifest.json
{
"name": "ServiceWorker => ArrayBuffer => Content script",
"version": "1.0",
"description": "Transfer data from ServiceWorker to content script",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"contentscript.js"
],
"run_at": "document_start"
}
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"permissions": [
"activeTab",
"tabs",
"windows"
],
"web_accessible_resources": [{
"resources": ["*.html", "*.js"],
"matches": ["<all_urls>"],
"extensions": []
}]
}
background.js
const bc = new BroadcastChannel('transfer');
const buffer = new ArrayBuffer(536346624);
bc.onmessage = async (e) => {
console.log(e);
const transfer = structuredClone(buffer, {transfer: [buffer]});
bc.postMessage(transfer, [transfer]);
console.assert(buffer.byteLength === 0, {buffer});
}
chrome.action.onClicked.addListener(async (tab) => {
chrome.tabs.sendMessage(tab.id, { url: chrome.runtime.getURL('index.html') });
});
contentscript.js
{
chrome.runtime.onMessage.addListener((message) => {
const handleMessage = (e) => {
if (e.origin === new URL(message.url).origin) {
console.log(e.data);
removeEventListener('message', handleMessage);
}
}
addEventListener('message', handleMessage);
window.open(message.url, location.href, 'menubar=no,location=no,resizable=no,scrollbars=no,status=no,width=50,height=50');
});
}
index.html
<script src="./script.js"></script>
script.js
const bc = new BroadcastChannel('transfer');
bc.onmessage = (e) => {
opener.postMessage(e.data, name, [e.data]);
bc.close();
close();
}
bc.postMessage(null);
The linked issue is indeed a feature request to implement the means to transfer huge data "instantly". I've edited it to avoid the fixation on blobs as it's not about blobs in particular, but any binary data.
I've retested the actual time:
There are many use cases where huge binary data needs to be sent "instantly", for which chrome
sendMessage is 10-100 times slower because it applies JSON.stringify that escapes some characters and then Chrome internally encodes the result to UTF-8 again in its Mojo IPC, so anything non-ASCII you provide to sendMessage, including text you just manually encoded to UTF-8 or any other at least 8-bit encoding, will be re-encoded slowly again. Then the reverse will be applied in the receiver.
It means that chrome
sendMessage stresses the CPU excessively, which can be observed in profiler and task manager. It blocks the extension process and the receiver process and sometimes the browser process as well, introducing lags and jank.
I am not sure why the context restriction is content scripts, which are inherently unreliable.
It's the opposite. Anything outside the content script's isolated world is unreliable, i.e. the main world of a web page where any prototype or a standard global type can be spoofed (maliciously) or broken (inadvertently e.g. by a weird polyfill). The content script is the only reliable script an extension can use in a web page, although of course it's less reliable than the environment of its own origin iframe, but that's an entirely different cross-origin page environment.
How did you send a Blob
? A Blob
is not transferable. None of these transfers occur "instantly".
I was comparing chrome.scripting.executeScript()
to a content script.
You have not described a use case where speed of transfer is important or required.
You cannot spoof, intercept of otherwise do anything about the content script or Web page itself opening a new window.
I've used different terms specifically to indicate the difference between sending and transferring. Sending is
port.postMessage(blob)
, transferring is port.postMessage(buf, [buf])
. My measurements for sending a blob in the comment above were incorrect as I've included the time to fill the blob with random values. The actual time is less than 1ms i.e. practically instantaneous. This was what confused me the first time I investigated transferability of blobs. Apparently blobs are using a shared repository, you've mentioned something like that, so when we send a blob only the handle is actually sent. Time to transfer a 500MB buffer was correct, it's 0.5sec;
chrome.scripting.executeScript() injects a content script by default unless you specify world: 'MAIN'
and the injected script becomes a "page script", not a "content script". I name it a "page script" because it runs in the JS environment of the page, so it plays by the same rules, the only difference being that it's not subjected to the CSP of the page.
Speed of transfer is important or required depending on the amount of data and whether sending it introduces lags or stresses the CPU so much that its fan becomes audible on a low-power device like notebooks. Most extensions don't need this feature. In your case the difference might be negligible, otherwise you would have probably noticed these problems.
Opening a new window is a viable workaround, however depending on the desktop environment and its settings it may introduce noticeable movement in some area of the screen and it may be also closed by another extension before the script in the window initializes.
Speed of transfer is important or required depending on the amount of data and whether sending it introduces lags or stresses the CPU so much that its fan becomes audible on a low-power device like notebooks.
I am still not certain what use case onMessage
doesn't work with.
Why would we need to transfer 500MB of data to the content script?
If the use case is streaming media, onMessage
works.
If the use case is downloading files we use chrome.downloads
or File System Access API on the main thread - or we can transfer the FileSystemFileHandle
to the ServiceWorker
and write to the user file system there.
If you really want real-time communication you can use Native Messaging with libdatachannel as part of the host, so you can create a data channel connection from the content script.
Opening a new window is a viable workaround, however depending on the desktop environment and its settings it may introduce noticeable movement in some area of the screen and it may be also closed by another extension before the script in the window initializes.
The idea is not to occlude the window
, rather make the window obvious so the user knows that window is only being used to transfer data to the content script.
Another extension can close any window or tab at any time, anyway.
Time to transfer a 500MB buffer was correct, it's 0.5sec;
Where does that data come from?
You can just use fetch('chrome-extension:/path/to/file')
in the content script, then you have the speed of Fetch
and ReadableStream
as response which you can read while the stream is still in progress.
I think we can write the data directly with executeScript()
to avoid having to transfer data
chrome.action.onClicked.addListener(async (tab) => {
const [{ result }] = await chrome.scripting.executeScript({
target: {
tabId: tab.id,
},
world: 'ISOLATED',
func: () => {
const buffer = new ArrayBuffer(2145386496 / 2);
console.log(buffer);
return buffer.byteLength;
}
});
console.log(result);
});
Only func's code is sent as a string to the tab where a new function is created from this text, not the data. You can send the data via args
parameter but internally that's the same as sendMessage because it also uses JSON.stringify + IPC. This API is just a polyfill for MV2 executeScript with code: `(${func})(${JSON.stringify(args).slice(1,-1)})
.
I found a workaround that may be unbreakable:
window.stop()
in your content script that runs in that iframe at document_start, it's temporarily registered for the duration of the whole thing using chrome.scripting API, it should check location.ancestorOrigins to see whether it's embedded inside an extension document.BroadcastChannel
using this id as a nameBlob
, which takes less than 1 millisecondIt may need setting up declarativeNetRequest rule to strip X-Frame-Options:DENY and another rule to strip referer
with the extension's id of the embedder origin if it's sent.
Currently I don't know how to obtain the window
of the tab via this approach in order to be able to transfer stuff like a MessagePort. Might be impossible.
Another disadvantage is a possibly long time to open a slow server URL.
Only func's code is sent as a string to the tab where a new function is created from this text, not the data.
What difference does it make?
- create an offscreen document in SW
What is that? Something Chromium extension authors have been talking about?
Currently I don't know how to obtain the window of the tab via this approach
Not sure what you mean.
The difference is that your code doesn't send any data, it just sends func.toString().
That is not observble. You get the same data.
What does the data in the Blob
consist of? What is the use case?
No, you don't get any data at all. The data is not a part of the function's code string.
The data is any binary generated or downloaded stuff. It is not a part of any function.
It doesn't matter. In the clicked window
all you know is there is you Blob
,
It won't contain data from SW, it's a useless exercise in semantics.
I don't think it matters. You create the data structure you want to be exposed in the window
in the ServiceWorker
. The clicked window
gets that data.
So far you have not described exactly what the data consists of, and why it is important to transfer the data.
Is there any reason you don't just use fetch('chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
from the arbitrary window
to get the data?
I was giving the idea of an arbitrary Web site removing an iframe
from their site yesterday.
If that happens once, then you can simply remove that code from the site after your initial test.
Given the challenge of writing source code that removes any arbitrary iframe
from a Web site and prevents said source code from being removed, such a requirement would be essentially impossible.
If you already have the data that needs to be transferred you can just set "web_accessible_resources"
in the manifest.json then use fetch('chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/path/to/file')
or import json from 'chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/path/to/file' assert { type: "json" }
on the Web site itself.
We're going in circles for like 100th time, but it's fun so let's do it once again.
A web page can trivially delete any element immediately once it's added as I've shown using DOMNodeInserted event. It can be done using MutationObserver too, as well as using a periodic check, it doesn't matter which one as they all are trivial to implement. This code can't be removed automatically from an unknown web page not because it specifically resists removal but because such code will be an integral part of a large minified bundle, possibly with obfuscation - you'd have to analyze it manually. Such code can be added by another extension as well, not that it's likely, but still possible and trivial to implement. It is trivial, there's no challenge in writing it. Of course there's a way for an extension to protect the iframe: it can override all DOM methods that can be used to remove an element or to rewrite/reload the page or to extract such methods from a new src-less iframe.
executeScript fetch import
All of these are for static data included with the extension, it's not what the linked issue is about. The issue is about data in SW context (and nowhere else within the extension). It can be generated by JS or downloaded from an external URL or captured from desktop. It's not a part of the func's code or of the extension package.
This code can't be removed automatically from an unknown web page not because it specifically resists removal but because such code will be an integral part of a large minified bundle, possibly with obfuscation - you'd have to analyze it manually.
The code can be removed. If need be we can use Local Modifications to run the local code of our choosing.
The issue is about data in SW context (and nowhere else within the extension). It can be generated by JS or downloaded from an external URL or captured from desktop. It's not a part of the func's code or of the extension package.
However the code is generated it can be written to a local file and fetched with fetch()
or import()
, or import
.
you'd have to analyze it manually.
Yes, one time. At the initial visit to any Web site that happens to runs such code, which I have never encountered in the field.
Thereafter we don't have that issue ever again.
All of these are for static data included with the extension, it's not what the linked issue is about. The issue is about data in SW context (and nowhere else within the extension). It can be generated by JS or downloaded from an external URL or captured from desktop. It's not a part of the func's code or of the extension package.
The download and capture from desktop use cases can be excluded altogether.
If we are really downloading content in the ServiceWorker
we don't need to transfer that data to the content script at all. Why would we do that? We just write the data directly to the user file system with FileSystemFileHandle
. If we are capturing content from the desktop, again, why would be send that data to the ServiceWorker
then back to the content script?
A web page can trivially delete any element immediately once it's added as I've shown using DOMNodeInserted event. It can be done using MutationObserver too, as well as using a periodic check, it doesn't matter which one as they all are trivial to implement. This code can't be removed automatically from an unknown web page not because it specifically resists removal but because such code will be an integral part of a large minified bundle, possibly with obfuscation - you'd have to analyze it manually. Such code can be added by another extension as well, not that it's likely, but still possible and trivial to implement. It is trivial, there's no challenge in writing it.
I really don't think you can write such code that cannot be removed. In absense of an actual proof-of-concept of such code working in the field I don;t think you have demonstrated the feasibility of someone even trying to do that.
Although Local Modifications is not a part of the extensions API/platform, but indeed its functionality can be replicated via chrome.debugger API, and indeed it can be used to remove the code once we identified it manually. I didn't contest this part. My point is that the analysis for such code can't be reliably automated. Also, chrome.debugger is not an API a typical user would want to allow for an extension that's not related to debugging.
Writing to a local file inside the installed extension directory is indeed a possibility but in addition to being slower than sending a Blob directly between contexts it would require either installing the extension inside the default downloads directory or to use a nativeMessaging app to write the file into the directory of an installed extension, finding which reliably and automatically is not always trivial since the browser may be launched with --user-data-dir
parameter.
@tophf I am curious what your proposed solution to this is?
Goal of the demo
Demonstrate how an extension can create a blob in the extension's service worker and transfer it to a page's main world.
Suggested implementation
Sketch of my initial implementation plan
navigator.serviceWorker.controller.postMessage()
structuredClone()
to create a copy of a blob.event.source.postMessage(msg, [transferable])
to reply to the client and transfer the blobwindow.parent.postMessage(msg, "<origin>", [transferable])
to transfer the data to the pageRelated links
Notes
Initial findings suggest that it's not possible to transfer variables across origins. I'm tentatively thinking that to work around this, we can use
URL.createObjectURL()
in the iframe and directly reference the object URL somewhere that's easily visible to the end user (e.g. canvas, Audio/Video element, etc.).