How can I access built-in methods of an Object that is overridden?

吃可爱长大的小学妹 提交于 2021-01-04 02:49:23

问题


A webpage is setting a built-in javascript method to null, and I'm trying to find a way to call the overridden methods in a userscript.

Consider the following code:

// Overriding the native method to something else
document.querySelectorAll = null;

Now, if I try to execute document.querySelectorAll('#an-example'), I will get the exception Uncaught TypeError: null is not a function. The reason being the method has been changed to null and is no longer accessible.

I'm looking for a way to somehow restore the reference to the method in my userscript. The problem is that the website can override the reference to anything (even including the Document, Element and Object constructors).

Since the website can also easily set the reference to null, I need a way to find a way to access the querySelectorAll method that the website won't be able to override.

The challenge is that any method such as createElement and getElementsByTagName (in addition to their prototypes) can get overridden to null at the point my userscript is executed on the page.

My question is, how do I access the Document or HTMLDocument constructor methods, if they have also been overridden?


Note:

Since Tampermonkey due to browser limitations cannot run my script at the beginning of a document, I'm unable to save a reference to the method I'd like to use, with something like this:

// the following code cannot be run at the beginning of the document
var _originalQuerySelectorAll = document.querySelectorAll;

回答1:


There are at least 3 approaches:

  1. Use the userscript sandbox. Alas, this currently only works on Greasemonkey (including version 4+) due to Tampermonkey and Violentmonkey design flaws / bugs. More below.
  2. Use @run-at document-start. Except that this too will not work on fast pages.
  3. Delete the function override. This usually works, but is liable to more interference with/from the target page. and can be blocked if the page alters the prototype of the function.


See, also, Stop execution of Javascript function (client side) or tweak it


Note that all of the script and extension examples, below, are complete working code.
And you can test them against this JS Bin page by changing:
      *://YOUR_SERVER.COM/YOUR_PATH/*
to:
      https://output.jsbin.com/kobegen*



Userscript Sandbox:

This is the preferred method and works on Firefox+Greasemonkey (including Greasemonkey 4).

When setting @grant to other than none, the script engine is supposed to run the script in a sandbox that browsers specifically provide for that purpose.

In the proper sandbox, the target page can override document.querySelectorAll or other native functions all it wants, and the userscript will see its own, completely untouched instances, regardless.

This should always work:

// ==UserScript==
// @name     _Unoverride built in functions
// @match    *://YOUR_SERVER.COM/YOUR_PATH/*
// @grant    GM_addStyle
// @grant    GM.getValue
// ==/UserScript==
//- The @grant directives are needed to restore the proper sandbox.

console.log ("document.querySelectorAll: ", document.querySelectorAll);

and yield:

document.querySelectorAll: function querySelectorAll() { [native code] }

However, both Tampermonkey and Violentmonkey do not sandbox properly, in neither Chrome nor Firefox.
The target page can tamper with the native functions a Tampermonkey script sees, even with Tampermonkey's or Violentmonkey's version of the sandbox on.
This is not just a design flaw, it is a security flaw and a vector for potential exploits.

We know that Firefox and Chrome are not the culprits since (1) Greasemonkey-4 sets up the sandbox properly, and (2) a Chrome extension sets up the "Isolated World" properly. That is, this extension:

manifest.json:

{
    "manifest_version": 2,
    "content_scripts": [ {
        "js":               [ "Unoverride.js" ],
        "matches":          [ "*://YOUR_SERVER.COM/YOUR_PATH/*" ]
    } ],
    "description":  "Unbuggers native function",
    "name":         "Native function restore slash use",
    "version":      "1"
}

Unoverride.js:

console.log ("document.querySelectorAll: ", document.querySelectorAll);

Yields:

document.querySelectorAll: function querySelectorAll() { [native code] }

as it should.



Use @run-at document-start:

Theoretically, running the script at document-start should allow the script to catch the native function before it's altered.
EG:

// ==UserScript==
// @name     _Unoverride built in functions
// @match    *://YOUR_SERVER.COM/YOUR_PATH/*
// @grant    none
// @run-at   document-start
// ==/UserScript==

console.log ("document.querySelectorAll: ", document.querySelectorAll);

And this sometimes works on slow enough pages and/or networks.

But, as the OP already noted, neither Tampermonkey nor Violentmonkey actually inject and run before any other page code, so this method fails on fast pages.

Note that a Chrome-extension content script set with "run_at": "document_start" in the manifest, does run at the correct time and/or fast enough.



Delete the function override:

If the page (mildly) overrides a function like document.querySelectorAll, you can clear the override using delete, like so:

// ==UserScript==
// @name     _Unoverride built in functions
// @match    *://YOUR_SERVER.COM/YOUR_PATH/*
// @grant    none
// ==/UserScript==

delete document.querySelectorAll;

console.log ("document.querySelectorAll: ", document.querySelectorAll);

which yields:

document.querySelectorAll: function querySelectorAll() { [native code] }

The drawbacks are:

  1. Won't work if the page alters the prototype. EG:
    Document.prototype.querySelectorAll = null;
  2. The page can see or remake such changes, especially if your script fires too soon.

Mitigate item 2 by making a private copy:

// ==UserScript==
// @name     _Unoverride built in functions
// @match    *://YOUR_SERVER.COM/YOUR_PATH/*
// @grant    none
// ==/UserScript==

var foobarFunc = document.querySelectorAll;

delete document.querySelectorAll;

var _goodfunc = document.querySelectorAll;
var goodfunc  = function (params) {return _goodfunc.call (document, params); };

console.log (`goodfunc ("body"): `, goodfunc("body") );

which yields:

goodfunc ("body"): NodeList10: body, length: 1,...

And goodfunc() will continue to work (for your script) even if the page remolests document.querySelectorAll.




回答2:


Additional solution in Tampermonkey is to restore the original via an iframe - assuming the site's CSP allows it, which it usually does, AFAIK.

const builtin = new Proxy(document.createElement('iframe'), {
  get(frame, p) {
    if (!frame.parentNode) {
      frame.style.cssText = 'display:none !important';
      document.documentElement.appendChild(frame);
    }
    return frame.contentWindow[p];
  }
});

Usage:

console.log(builtin.document.querySelectorAll.call(document, '*'));

P.S. If the page isn't thorough, you can access the original via the prototype without iframe trick:

Document.prototype.querySelectorAll.call(document, '*')
Element.prototype.querySelectorAll.call(normalElements, '*')


来源:https://stackoverflow.com/questions/53977791/how-can-i-access-built-in-methods-of-an-object-that-is-overridden

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!