userscripts icon indicating copy to clipboard operation
userscripts copied to clipboard

Support `unsafeWindow` API

Open MPThLee opened this issue 2 years ago • 19 comments

// ==UserScript==
// @name          TestScript
// @description   This is your new file, start writing code
// @match         <all_urls>
// @exclude-match *://*.google.com/*
// @grant unsafeWindow
// ==/UserScript==

unsafeWindow.console.log(`This is a test script - ${unsafeWindow.location.href}`);

System Information:

macOS or iOS version: iPadOs 15.4 Userscripts version: 1.2 (23) Safari version: iPadOs 15.4 bundled Is this issue related to script injection? Yes Did the test script (pasted above) successfully run on your machine? No

Since inject-into page has risk to detection, unsafeWindow is necessary to control the page.

MPThLee avatar May 08 '22 16:05 MPThLee

@MPThLee

Thank you for the suggestion. Are you willing to make a PR for this or do you have an example implementation I could look at?

quoid avatar May 08 '22 17:05 quoid

@quoid

Greasemonkey https://wiki.greasespot.net/UnsafeWindow https://github.com/greasemonkey/greasemonkey/blob/efd22a93121225ada47f2fe9f021af0ab6100c21/src/user-script-obj.js#L319-L340

Violentmonkey https://github.com/violentmonkey/violentmonkey/blob/925507139d92d09a4c83d32506c90ec691ea8247/src/injected/web/gm-global-wrapper.js https://github.com/violentmonkey/violentmonkey/blob/925507139d92d09a4c83d32506c90ec691ea8247/src/injected/web/gm-api-wrapper.js

Tampermonkey https://github.com/Tampermonkey/tampermonkey/blob/07f668cd1cabb2939220045839dec4d95d2db0c8/src/content.js#L455-L608

MPThLee avatar May 08 '22 18:05 MPThLee

@MPThLee thanks for the links.

If anyone wants to help out with this issue, please let me know. Time is limited for me, and this would be important to implement.

quoid avatar May 08 '22 18:05 quoid

I took a look at the links above, I am not sure it is even possible to implement unsafeWindow in the way they are done above. Safari does not have access to the wrappedJSObject that is being used in ViolentMonkey and GreaseMonkey. The TamperMonkey code is not proving useful as well.

In fact, I am not entirely sure how this model would work with Safari. When executing scripts in the context of the content script the user will not have ability to read or edit the properties of the page's window object. The content script context lives in an isolated world with no method available (that is known to me in Safari) for gaining access to that window object from there.

If the user wants to manipulate the window object in the context of the webpage, they can chose to use @inject-into page which will create a wrapped script element on the page and immediately execute it. They can freely access the window object that way.

Using @inject-into page is inherently less secure but it is the only method I know of, currently. Further, in the next update (or one after) I am considering disabling all GM apis when @inject-into page is used because of security concerns that I am not sure everyone is aware of - that or an alternative method of GM api execution will need to be developed when @inject-into page is used, but I digress.

My main point is that I am not sure the implementation of this is possible in a meaningful way. I am happy and eager to hear any other opinions here.

quoid avatar May 11 '22 02:05 quoid

@quoid

The reason unsafeWindow is unsafe and avoided is because it brings page context properties and functions into the content script context (some advanced APIs can be used), and incorrect script code can lead to security issues.

But I'm sure that as an scripts manager extension implement, you should never expose advanced APIs in content scripts to the page context, which is more dangerous and crazy behavior than unsafeWindow.

For several other extension implementations, it will only be injected into the page in the case of @grant none, otherwise it should always stay in the context of the content script (in order to use advanced APIs such as the GM API).

ACTCD avatar May 24 '22 08:05 ACTCD

@ACTCD

I agree with what you are saying here. I am unaware of a method of exposing the page context's window object to the content script context and thus allowing the content script context to update and manipulate the page context window object.

If you are aware of a method of implementation, please do share. None of the linked files gave me any insight.

The security issues you reference here is why I am going to be restricting injection even more in the next update. As I mentioned in my previous comment:

Further, in the next update (or one after) I am considering disabling all GM apis when @inject-into page is used because of security concerns that I am not sure everyone is aware of...

Currently users are allowed to do "unsafe" things, but knowing whether or not users actually know what they are doing is potentially an unsafe practice is not known. I figured users who are writing userscripts are aware of this, but I no longer think that is true.

An alternative metadata key could be used, for example @grantsInPage true or something could be implemented to make an active decision to do something unsafe. But even with that, a lot of users just download userscripts written by others without regard to their security.

I am more in favor of locking down and being secure, but I reckon most users are not.

That said, I already have the @grant changes running locally on my machine, and will likely be pushing to the next beta build which I am sure will result in a bunch of new issues of "why this doesn't work" :)

quoid avatar May 24 '22 14:05 quoid

@quoid

In fact, If not read this issue, and you mentioned to adjust this issue, from my understanding of Greasemonkey's description of unsafeWindow, as a userscript developer, I was unaware that there would be extension implementations that expose advanced APIs to the page context by default. I didn't realize this even I had read the instructions of @inject-into in Violentmonkey.

This is a complete opposite implementation of Greasemonkey's security model, and I'm not sure if Violentmonkey's implementation is really like that too. I might need to consider adding @inject-into content to each of my scripts to make sure this security issue isn't introduced, even I'm almost not using the GM API, I'm stiil not sure if it will introduce some advanced APIs that the page script can access which in content scripts (such as "a small subset of the WebExtension APIs").

I think implemented as a userscript manager extension, it should never be in a default unsafe way for compatibility with userscripts. As mentioned above, Greasemonkey's design of unsafeWindow is that it should always only be used in content scripts, and the name explicitly tells you it's unsafe, and you need @grant to use it. It never occurred to me that an extension would implement unsafeWindow in a way that might introduce the GM API into the page context, which I believe is pretty crazy behavior.

In fact, I don't think properly designed scripts should use unsafeWindow, its use case should be very rare. If I'm right, then I see no reason for an extension to have an unsafe implementation to be compatible with some ill-designed user script. I don't think it's necessary to design a new metadata (which incompatible with other extensions) like you mentioned @grantsInPage true, if the user script author is not good at reading documentation, the person will not add this metadata or really understand what this brings Security issues, this doesn't improve compatibility in any way, in fact it might encourage incorrect script design in some twisted way.

We should always encourage and guide users or script writers to achieve the desired functionality in a safe way, with better design, rather than in an easy way to satisfy poor design. This will not lead to any good direction for the entire user script market, but will only add more confusion. As I mentioned earlier, I might have to add @inject-into content to avoid the security implications that exist by default, which is very odd design. In any case we should assume that it is safe by default, and that using unsafe features should be very difficult, shouldn't be easy or even default.

ACTCD avatar May 24 '22 17:05 ACTCD

@ACTCD

I agree with what you've written, especially:

In fact, I don't think properly designed scripts should use unsafeWindow

I very rarely use userscripts written by other individuals, partially because I have small needs for my web browsing customization, but also because there is a lot of trust to be placed on a userscript author and there are security implications (separate from anything to do with unsafeWindow) when running someone else's code on your machine. This is also why I tend to trust GitHub much more than other sources for using 3rd party userscripts.

That being said, when my update goes into the next beta, these are some of the changes that will happen:

  • @inject-into content is the new default
  • when @grant has any value, @inject-into page and @inject-into auto will automatically be reverted to @inject-into content (with a message printed to the console)
  • unsafeWindow will not be available in this extension as there is simply not known implementation at this time (maybe at some point it will be accessible)
  • user can still inject into the page context, however they can not use GM apis in the page context

Just a note, currently when the user has @granted privileged APIs and also use @inject-into page those API methods are not injected into the page context. However, there could be a way for a page context to access those methods, but it is not as straightforward as calling the methods. I do not want to detail here how to do this, but it is possible.

quoid avatar May 25 '22 02:05 quoid

脚本管理器应当具备通用性,将安全设置交给用户选择,而不是突然地直接取消支持某一特性。 Script managers should be generic, leaving the security settings to the user to choose, rather than simply desupporting a feature.

fyy99 avatar Jul 20 '22 12:07 fyy99

@fyy99

Script managers should be generic, leaving the security settings to the user to choose, rather than simply desupporting a feature.

It sounds like you did not read through the issue. It doesn't matter what I think, unsafeWindow as it is implemented in other browsers is seemingly not possible in Safari.

It seems like you have a strong feelings about this feature. Would you like to contribute to this project and attempt to build out the feature?

quoid avatar Jul 20 '22 13:07 quoid

@fyy99

It looks like you misunderstood the extension's changes. No application should have security holes, you can't expect users to understand the risks, that's what all software and hardware do all the time to fix bugs.

The implementation of this extension is consistent with Greasemonkey (the reference standard for most script managers), it does not have generic contradictions, and also provides features that users can choose to be compatible with other script managers.

The problem you are having is probably as you said, the user script you are using is not generic, it may depend on a non-generic feature of some script manager, you should expect that script to be more generic, not this extension.

ACTCD avatar Jul 20 '22 17:07 ACTCD

@quoid With unsafeWindow not being available, what are the options to modify functions defined by the web page? One of the objectives on my userscript is to adjust those functions' behavior...

DBa2016 avatar Sep 06 '22 12:09 DBa2016

@DBa2016

My suggestions would be to use the following metadata in your userscripts:

@inject-into page
@run-at document-start

This will ensure your script run in the page context and give you complete access to the window object. So you can window.myFunc = () => {...} etc...

Some notes:

  • do not use any @grant APIs or you can not inject into the page context (security issues)
  • if a page has a strict content security policy, this will also prevent page context injection (there is no workaround for this)

quoid avatar Sep 06 '22 13:09 quoid

@quoid That's unfortunately a combo I definitely need: I need to use GM API, too. :( In particular, I needc to store some values (and localStorage is definitely not an option, it makes the entire browser slower down the road), and I also need to use some cross-site AJAX requests (GM.xmlHttpRequest)...

DBa2016 avatar Sep 06 '22 13:09 DBa2016

@DBa2016

You absolutely need to inject into the page context as well? If you can avoid that, then obviously you can use all the GM apis you'd like. If you absolutely must use GM apis in the page scope (which is unsafe), you could do something like this - but I understand that is an "annoying" implementation, but it works.

I am still thinking of ways to implement something like unsafeWindow, but I havent been able to figure anything out yet.

quoid avatar Sep 06 '22 13:09 quoid

Oof, that's a pretty ugly way... And I am using the backticks in my script, too (yes, this can be avoided, but...)...

DBa2016 avatar Sep 06 '22 15:09 DBa2016

Yeah, it's not exactly pretty. On the flip side, using the method does raise security concerns so it should probably be avoided.

quoid avatar Sep 07 '22 03:09 quoid

@DBa2016

That's unfortunately a combo I definitely need: I need to use GM API, too. :( In particular, I needc to store some values (and localStorage is definitely not an option, it makes the entire browser slower down the road), and I also need to use some cross-site AJAX requests (GM.xmlHttpRequest)...

Could you be specific about your use case? Or if possible, can you provide some sample code where you implement the relevant functionality?

I still think that in most cases, you don't need to use unsafeWindow to implement your needs safely. It does not affect your use of GM APIs while modifying functions in the page context in the same userscript. At the same time they can also exchange data, if you need to.

ACTCD avatar Sep 09 '22 18:09 ACTCD

For anyone who needs this, I made a demo to show that you probably don't need to use unsafeWindow. You can use GM APIs, and also change and get page scope properties, you can also through a safer way to exchange data between content script contexts and page script contexts.

Read its source code here. You can also install this demo script, go to this website example.com, open the browser devtools console, and see the logs there, you can try reading or writing the window.page_data property to see the changes in the logs. (Note: in the console you are default in the page scripts context)

ACTCD avatar Sep 09 '22 22:09 ACTCD

I would like to update some of my investigations on this issue.

Currently we cannot implement unsafeWindow properly due to lack of browser APIs or some tricks. But my latest opinion is that we shouldn't implement unsafeWindow if we lack the necessary use cases, even if we find a way. I'll explain why I think so later.

@quoid talked to me about adding window.unsafeWindow = window; to improve the compatibility of scripts, my suggestion is that we shouldn't, it doesn't really implement a correct unsafeWindow, it just suppresses some errors , but scripts that depend on it still don't work correctly.

Through some investigations, I think unsafeWindow currently has two main contradictions:

  • Some extensions does not implement this API in the correct way
  • User script author did not use this API properly or unnecessarily

In the long-term mutual influence of the above two aspects, many scripts in the script market actually use unsafeWindow in a wrong way. We should not implement unnecessary APIs in this extension in order to be compatible with those wrongly written scripts. And thus continue to contribute to the confusion in the scripting market.

In fact, most user scripts can implement the functions they need in almost all user script manager extensions through the above method, and unsafeWindow is an unnecessary and not recommended feature.

Finally, I'll attach a user script to help you test their implementation of unsafeWindow in different extensions, you'll see how they implement differently:

// ==UserScript==
// @name        debug.unsafeWindow
// @match       *://*/*
// @grant       GM.getValue
// @inject-into content
// ==/UserScript==

unsafeWindow.test1 = "debug1";
console.info(window, unsafeWindow);
const script = document.createElement("script");
script.textContent = `console.info("page - window.test1:", window.test1); test2="debug2";`;
document.head.append(script);

For the reasons above, I will close this issue unless we have the correct way to implement it and the necessary use cases, at which time we will reopen it.

ACTCD avatar Nov 02 '22 04:11 ACTCD

@quoid @ACTCD

如果是技术上的原因,无法实现unsafeWindow,我也无话可说。但如果是其他什么原因,我还是同意前面 @fyy99 的观点,应该把选择权交给用户,而不是对油猴脚本标准随意进行割裂。

首先我来说下我的unsafeWindow用例:

我开发了一个脚本(查看代码),需要一个很复杂的设置页面来使用。常规的做法可以把设置页面的代码写到脚本里面,但这会导致脚本的体积膨胀臃肿。其实这是完全没有必要的,因为使用的时候,不需每个页面都加载设置页面的代码。

所以我把设置页面的庞大代码分离了出来,并单独部署。然后设置页面的代码利用了unsafeWindow来访问GM的接口,这样非常的方便。特别是我的脚本和页面共用许多代码的情况下,并不需要考虑某段代码是在油猴脚本执行还是页面脚本执行。

否则的话,按你描述的方式,利用@inject-into代码注入,通过事件来通讯,我实在不知如何处理。因为事件模型一般用在单向信号传递,用来做数据交互非常麻烦而不实际。

所以我认为 unsafeWindow 是有必要的:

  • 如我的用例所描述,unsafeWindow在某些情况可以极大方便开发者。
  • 而且 unsafeWindowTampermonkey 里是标准提供的。

关于安全性的问题:

  • 根据我的理解,如果提供@inject-into注入代码的功能,那么是否提供unsafeWindow,安全性都是一样的。因为unsafeWindow可能触发的风险,@inject-into也能一样可以触发。唯一区别是unsafeWindow更方便而已。
  • 而且因这种不安全性受到损失的概率我想也是极低,不能因微小的几率而放弃极大便利,就如人们不会为了避免车祸而拒绝乘坐汽车。

当然,如果我的理解有误,欢迎指出。 或者针对我的用例,您有更好的解决方案,欢迎提出。

PS: 其实 Tampermonkey 没有在IOS safari上线才是我在这里留言的根本原因。:)

---------- translate by google brad: -----------------

@quoid @ACTCD

If it is technically impossible to implement unsafeWindow, I have nothing to say. But if it is for other reasons, I still agree with the opinion of @fyy99, that the choice should be given to the user, rather than arbitrarily breaking the Greasemonkey script standard.

First, let me talk about my use case of unsafeWindow:

I developed a script (view code) that needs a complex settings page to use. The conventional way is to write the settings page code into the script, but this would make the script bloated. In fact, this is completely unnecessary, because the settings page code does not need to be loaded on every page when used.

So I separated the large amount of code for the settings page and deployed it separately. Then the code for the settings page used unsafeWindow to access the GM interface, which is very convenient. Especially in the case where my script and the page share a lot of code, I don't need to consider whether a piece of code is executed in the Greasemonkey script or the page script.

Otherwise, according to the way you described, using @inject-into code injection and events to communicate, I really don't know how to deal with it. Because the event model is generally used for unidirectional signal transmission, it is very inconvenient and impractical for data interaction.

Therefore, I believe that unsafeWindow is necessary:

  • As described in my use case, unsafeWindow can greatly facilitate developers in some cases.
  • And unsafeWindow is a standard feature in Tampermonkey.

Regarding the security issue:

  • Based on my understanding, if the feature of injecting code using @inject-into is provided, the security is the same whether unsafeWindow is provided or not. Because the risks that unsafeWindow may trigger can also be triggered by @inject-into. The only difference is that unsafeWindow is more convenient.
  • And I think the probability of being harmed by this insecurity is also extremely low. We should not give up great convenience for a small chance, just as people would not refuse to ride a car to avoid car accidents.

Of course, if my understanding is wrong, please point it out. Or if you have a better solution for my use case, please propose it.

PS: Actually, the fundamental reason why I am commenting here is that Tampermonkey is not yet available on iOS Safari. :)

fishjar avatar Aug 28 '23 02:08 fishjar

@fishjar 感谢你的评论。

正如我们在前面提到的,到目前为止,我们仍然没有办法在 Safari 中实现它。 由于缺乏必要的上游相关 API 支持。这对于所有 Safari Web 扩展都是一样。 如果您知道可以在 Safari 中正确实现它的某种方法,欢迎您告诉我们。

目前在 页面上下文扩展上下文 之间只能通过事件传递可序列化对象。 我在上面提供的 示例 则是按照推荐的更安全的方式提供的实现演示例。 即,使用随机名称的自定义事件替代默认的 window.postMessage 事件:

Web context scripts can use custom events to communicate with content scripts (with randomly generated event names, if needed, to prevent snooping from the guest page).

实际上通过上述方式应该很适合您提到用例,即设置页面的数据交换。

@inject-intounsafeWindow 的安全风险是不同的,前者正是有效隔离扩展高级 API 的一种方式。

正如原始 GM API 文档中提到的,unsafeWindow 是不安全的,应尽可能避免使用。绝大多数用例都可以通过更安全的方式实现。

ACTCD avatar Aug 28 '23 06:08 ACTCD

@ACTCD 感谢您的及时回复!

  • 如果没有办法在 safari 实现 unsafeWindow,那真的比较遗憾。
  • 看来我只能利用@inject-into来实现了,虽然复杂性增加了许多。
    • 需要设计一个机制来区分listenerdispatch的一一对应关系。
    • 或许还需要将Event进一步封装成一个Promise方便使用。
  • 关于安全性的理解,如果你很肯定,那也许你是对的,我接触油猴脚本的时间并不长。

fishjar avatar Aug 28 '23 08:08 fishjar

@fishjar

通过事件名称就可以做对应和区分了,提供的示例已经在两个方向上使用了不同的通道。

当然,您也可以通过消息内容对象的自定义键值做进一步区分,灵活性和方便性都足够。

ACTCD avatar Aug 28 '23 09:08 ACTCD

@ACTCD

感谢您的提示及帮助,虽然麻烦了一些,还好工作量比想象的要少。 目前我的项目(kiss-translator)可以在Userscripts Safari下运行了。

fishjar avatar Aug 29 '23 09:08 fishjar

@ACTCD 您好!

我的项目本来是在 FireFox 工作良好的,突然出现bug,然后就发现下面的事实:

  • @inject-into 方式在 Userscripts Safari 可以运行,但在 FireFox + Tampermonkey 却不行。
    • Firefox中,油猴脚本提示 window.dispatchEvent 方法不存在。
  • unsafeWindow 方式在 Userscripts Safari 不能用,但在 FireFox + Tampermonkey 却工作良好。

真是够折腾,为了弥补割裂的现实,或许只能增加更多的适配代码 :(

fishjar avatar Aug 31 '23 08:08 fishjar

@fishjar 是的,这就是我上面提到的不同的扩展以不同的方式实现了该方法。包括是否真实注入到了页面上下文,也是存在实现差异。这也是我撰写上述评论的初衷,即我们不想再为这些已经存在的混乱添加更多的“自定义”兼容实现,制造更多混乱。

dispatchEvent 是 Event API 的标准方法,在页面和内容脚本范围都适用。您可以测试到我上面提供的示例脚本在 Firefox + Greasemonkey / FireMonkey 等环境中都是正常工作的。

也就是说该项目并没有添加任何差异实现,或造成进一步割裂的自定义实现。而这些也是为什么我建议不要再实现该 unsafeWindow 方法,以及使用推荐的通用方式制作示例用户脚本的原因。这是目前我们能够做的。

ACTCD avatar Aug 31 '23 10:08 ACTCD

@fishjar 是的,这就是我上面提到的不同的扩展以不同的方式实现了该方法。包括是否真实注入到了页面上下文,也是存在实现差异。这也是我撰写上述评论的初衷,即我们不想再为这些已经存在的混乱添加更多的“自定义”兼容实现,制造更多混乱。

dispatchEvent 是 Event API 的标准方法,在页面和内容脚本范围都适用。您可以测试到我上面提供的示例脚本在 Firefox + Greasemonkey / FireMonkey 等环境中都是正常工作的。

也就是说该项目并没有添加任何差异实现,或造成进一步割裂的自定义实现。而这些也是为什么我建议不要再实现该 unsafeWindow 方法,以及使用推荐的通用方式制作示例用户脚本的原因。这是目前我们能够做的。

请问最新版userscripts是否已经支持 unsafeWindow 了?

fishjar avatar Nov 11 '23 06:11 fishjar

@fishjar

No, it doesn't. You can see this in the README.

If the project supports it, we will update API docs.

ACTCD avatar Nov 11 '23 08:11 ACTCD