Discussions » Creation Requests

prevent website from overriding userscript's JS methods

§
Posted: 2024-03-11

If you visit Furaffinity, you'll noticed that entering a literal Array.from in the devtools console will not spit out

from() { [native code] }

as expected, rather:

$A(c){if(!c){return[]}if("toArray" in Object(c)){return c.toArray()}var b=c.length||0,a=new Array(b);while(b--){a[b]=c[b]}return a}

This also happens inside userscripts. Like this one that uses a workaround via a spread syntax to convert a set back into an array. However, I'm worried that any JS method such as string.replace could also be overridden, and potentially break general-purpose userscripts designed to work with any website (such as a link extractor).

Is there a way to prevent sites from altering JS methods inside userscripts? My plan is to have it so that any methods during execution of the userscript will use unmodified default JS methods, and any code by the site will use whatever is modified. This is so to prevent the script or the site from breaking via wrong methods.

§
Posted: 2024-03-12

You can also run at document-start and backup the methods you need before they're altered by the website

§
Posted: 2024-03-12
Edited: 2024-03-12

You can also run at document-start and backup the methods you need before they're altered by the website

That sounds better, since creating an iframe just to restore methods seems be a little too extreme. But how do I do that? (I know @run-at document-start) I tried doing this to preserve the built in objects and its methods, such as Array:

let Array = JSON.parse(JSON.stringify(Array))

and it errors out because JSON.stringify(Array) returns undefined. I'm attempting to try out deep-copying it so that all of its methods are kept. If anything, I would like to have it scoped so that the site's modified JS will be modified when executing (so FA's Array.from uses the modified function as expected), but inside the userscript, will not be modified (will be the default JS built-in objects). This prevents any breaks for both the website and the userscript.

§
Posted: 2024-03-12

Just tested this code:

// ==UserScript==
// @name        furaffinity.net preserve Array.from
// @namespace   Violentmonkey Scripts
// @match       https://www.furaffinity.net/*
// @grant       none
// @version     1.0
// @author      -
// @description 3/12/2024, 3:48:47 PM
// @run-at  document-start
// ==/UserScript==
(function () {
  let Array = Array
  setTimeout(testcode, 5000)
  function testcode() {
    let listWithDupes = [1,2,2,3]
    console.log(Array.from(new Set(listWithDupes)))
  }
})();

And the code executes before even the browser loads the default built-in objects, causing an error at line 12 (let Array = Array) because Array hasn't been defined yet.

§
Posted: 2024-03-12

@Konf Actually, the above post's code is bad (it does not restore the from method, it does a shallow copy (assuming you change let Array to let Array2 = Array), whose properties and methods affect each other), how do you backup the methods? Can you show me an example code?

§
Posted: 2024-03-12

My plan is to have it so that any methods during execution of the userscript will use unmodified default JS methods, and any code by the site will use whatever is modified

Considering this request, the iframe approach seems to be the best. I didn't find anything better. Methods backup approach is way too specific, and even for the classes such as Array you need a lot of backups.

// iframe approach
// @run-at document-body
(async function() {
  'use strict';

  const I = await new Promise((resolve) => {
    const iframe = document.createElement('iframe');
    const fragment = document.createDocumentFragment();

    fragment.appendChild(iframe);
    document.body.appendChild(fragment);

    const iframeWindow = iframe.contentWindow;

    iframe.remove();
    resolve(iframeWindow);
  });

  console.log(I.Array.from);
}());
// method backup approach
// @run-at document-start
(function() {
  'use strict';

  const makeArrayFrom = Array.from.bind(Array);

  console.log(makeArrayFrom);
}());

You can also check this, but this one actually seems a little too extreme

§
Posted: 2024-03-13

@Konf Thank you. Looks like iframe got second place in the best way to have all your methods not be altered without having 100s of lines of code for every object and its methods preserved. The code looks cleaner than mine. Reason for being 2nd place is because I have a feeling that in the event a sneaky page tries to alter Promise to prevent userscript from creating an iframe, then the last resort is to use @sandbox and travel to ISOLATED_WORLD or USERSCRIPT_WORLD in which the page cannot reach.

For anyone out there making userscripts for any web page, I strongly recommend seeing this thread so you don't end up with sites "hijacking" your JS built-in objects.

§
Posted: 2024-03-13

in the event a sneaky page tries to alter Promise

Yeah, about that. I accidentally forgor to remove the Promise usage from my previous attempts, it is absolutely unneeded:

// iframe approach
// @run-at document-body
(function() {
  'use strict';

  const I = (function() {
    const iframe = document.createElement('iframe');
    const fragment = document.createDocumentFragment();

    fragment.appendChild(iframe);
    document.body.appendChild(fragment);

    const iframeWindow = iframe.contentWindow;

    iframe.remove();

    return iframeWindow;
  }());

  console.log(I.Array.from);
}());
§
Posted: 2024-03-13
Edited: 2024-03-13

Cool, I also noticed that an identifier document are seemingly "frozen" (try entering document = "hello world!", then enter just document will show that it isn't modified, on the console log), that means potentially many identifiers and other stuff they cannot overwrite are either reserved words or symbols they can't alter.

§
Posted: 2024-03-13
Edited: 2024-03-13

Ooh, document.body.appendChild(fragment); have a seemingly tendency to error out sometimes, espically if you navigate to a different webpage.

§
Posted: 2024-03-13
Edited: 2024-03-13
(async function() {
  'use strict';

  /*
  * 1. @run-at document-start because @run-at document-body might have a chance to be late.
  * 2. Seems like iframe needs to be appended somewhere, so need to wait for document.documentElement
  *  node to arrive. @run-at document-start is sometimes faster than the node arrival.
  * 3. iframe.contentWindow has the original JS objects, but if iframe is not "ready",
  *  its contentWindow is empty. iframe.src = 'javascript:""' seems to be the best to get the
  *  contentWindow as fast as possible.
  * 4. iframe.src = 'javascript:""' seems to be readying up the iframe instantly right after
  *  document.documentElement.appendChild so you might not need iframe.onload
  * 5. Split up A and B before making any debug or performance measurements
  */

  // Wait for documentElement, wait for iframe load event
  const A = await new Promise(async (resolve) => {
    for (;;) {
      if (document.documentElement) break;

      await new Promise(resolve => setTimeout(resolve));
    }

    const iframe = document.createElement('iframe');

    iframe.src = 'javascript:""';
    iframe.onload = () => {
      const iframeWindow = iframe.contentWindow;

      iframe.remove();
      resolve(iframeWindow);
    };

    document.documentElement.appendChild(iframe);
  });

  // Don't wait for iframe load, to save up a few milliseconds.
  // It might have a chance to have empty iframe.contentWindow?
  const B = await (async function() {
    for (;;) {
      if (document.documentElement) break;

      await new Promise(resolve => setTimeout(resolve));
    }

    const iframe = document.createElement('iframe');

    iframe.src = 'javascript:""';

    document.documentElement.appendChild(iframe);

    const iframeWindow = iframe.contentWindow;

    iframe.remove();

    return iframeWindow;
  }());

  console.log(A.Array.from);
  console.log(B.Array.from);
})();
§
Posted: 2024-03-13

Thank you! I prefer the first one (A, not B), as waiting until the document loads is safer and less prone to errors.

§
Posted: 2024-03-14
Edited: 2024-03-14

Actually, I'm wrong. tested on firefox + greasemonkey (one of the few userscripts supporting @run-at document-start), and discovered an issue where A's function worked, then terminates the entire userscript after its function finishes, with Error: Promised response from onMessage listener went out of scope on the console log (tested on furaffinity).

// ==UserScript==
// @name     testscript123
// @version  1
// @grant    none
// @run-at  document-start
// ==/UserScript==
(async function() {
  'use strict';

  /*
  * 1. @run-at document-start because @run-at document-body might have a chance to be late.
  * 2. Seems like iframe needs to be appended somewhere, so need to wait for document.documentElement
  *  node to arrive. @run-at document-start is sometimes faster than the node arrival.
  * 3. iframe.contentWindow has the original JS objects, but if iframe is not "ready",
  *  its contentWindow is empty. iframe.src = 'javascript:""' seems to be the best to get the
  *  contentWindow as fast as possible.
  * 4. iframe.src = 'javascript:""' seems to be readying up the iframe instantly right after
  *  document.documentElement.appendChild so you might not need iframe.onload
  * 5. Split up A and B before making any debug or performance measurements
  */

  // Wait for documentElement, wait for iframe load event
  const A = await new Promise(async (resolve) => {
    for (;;) {
      if (document.documentElement) break;

      await new Promise(resolve => setTimeout(resolve));
    }

    const iframe = document.createElement('iframe');

    iframe.src = 'javascript:""';
    iframe.onload = () => {
      const iframeWindow = iframe.contentWindow;

      iframe.remove();
      resolve(iframeWindow);
    };

    document.documentElement.appendChild(iframe);
  });

  // Don't wait for iframe load, to save up a few milliseconds.
  // It might have a chance to have empty iframe.contentWindow?
  const B = await (async function() {
    for (;;) {
      if (document.documentElement) break;

      await new Promise(resolve => setTimeout(resolve));
    }

    const iframe = document.createElement('iframe');

    iframe.src = 'javascript:""';

    document.documentElement.appendChild(iframe);

    const iframeWindow = iframe.contentWindow;

    iframe.remove();

    return iframeWindow;
  }());

  console.log(A.Array.from);
  console.log(B.Array.from);
})();
§
Posted: 2024-03-14
const C = await (async function() {
  for (;;) {
    if (document.documentElement) break;

    await new Promise(resolve => setTimeout(resolve));
  }

  const iframe = document.createElement('iframe');

  iframe.src = 'javascript:""';

  document.documentElement.appendChild(iframe);

  for (;;) {
    if (iframe.contentWindow) break;

    await new Promise(resolve => setTimeout(resolve));
  }

  const iframeWindow = iframe.contentWindow;

  iframe.remove();

  return iframeWindow;
}());
§
Posted: 2024-03-14
Edited: 2024-03-14

in the event a sneaky page tries to alter Promise

Yeah, about that. I accidentally forgor to remove the Promise usage from my previous attempts, it is absolutely unneeded:

// iframe approach
// @run-at document-body
(function() {
'use strict';

const I = (function() {
const iframe = document.createElement('iframe');
const fragment = document.createDocumentFragment();

fragment.appendChild(iframe);  
document.body.appendChild(fragment);  

const iframeWindow = iframe.contentWindow;  

iframe.remove();  

return iframeWindow;  

}());

console.log(I.Array.from);
}());

the functions shall be binded before iframe.remove

Some browsers deny the access of the JS function once the iframe is removed.

§
Posted: 2024-03-15

Uhh some of the code on your post is not inside the <pre> tags. Can you show me the bounded and non-promise version?

§
Posted: 2024-03-15

Just don't iframe.remove(), I guess. But I have no idea what are these browsers you should care about

§
Posted: 2024-03-15

I use google chrome and mozilla firefox, on PC

§
Posted: 2024-03-17

I realize it is called “monkey patching”. In the furaffinity case, they monkeypatched Array.from.

Post reply

Sign in to post a reply.