javascriptclosurescode-injectionbookmarkletjavascript-injection

JS: inject into BoundFunctionObject for bookmarklet script


Suppose I am writing a bookmarklet script to modify a webpage. Most websites (using webpack, etc) follow a structure something like this:

<html>
    <script type="text/javascript">
        const buttonClicked = (() => {
            let internal_variable = 0;
            const _internal_buttonClicked = () => {
                internal_variable++;
                document.getElementById("myButton").innerText = `Clicked ${internal_variable} times!`;
            };
            return _internal_buttonClicked.bind(this);
        })();
    </script>
    <body>
        <button id="myButton" onclick="buttonClicked()">Clicked 0 times!</button>
    </body>
</html>

I would like to access internal_variable and set it to -100. With a debugger, this is simple; I breakpoint inside _internal_buttonClicked(), and then change internal_variable directly.

How can I do this without

I'm aware that the simple answer is "you can't, intentionally", but I'm willing to accept any roundabout solutions, solutions that involve browser extensions/privileged execution, solutions specific to require, etc.

Related:


Solution

  • It is not possible to assign a new value to internal_variable as it is not in scope for your code. You can also not assign a new function to buttonClicked, because it is a constant.

    But depending on the actual situation you can go for some other approaches. Here are a few that would work for your example case:

    1. Override innerText

    Have it change the number that is displayed on the button by always subtracting 100 from the value that is about to be displayed.

    Your code would have this:

        Object.defineProperty(document.getElementById("myButton"), "innerText", {
            set(text) {
                // Subtract 100 from the number in the text "Clicked <number> times!"
                text = text.replace(/(Clicked )(\d+)( times!)/, (_, a, b, c) => a + (b - 100) + c);
                // Call the prototype setter for innerText
                Object.getOwnPropertyDescriptor(HTMLElement.prototype, "innerText").set.call(this, text);
            }
        });
    

    Flattened to a bookmarklet:

    javascript: {Object.defineProperty(document.getElementById("myButton"),"innerText",{set(text) {text=text.replace(/(Clicked )(\d+)( times!)/,(_,a,b,c)=>a+(b-100)+c);Object.getOwnPropertyDescriptor(HTMLElement.prototype,"innerText").set.call(this, text);}});}
    

    2. Correct button output after it changed

    This is similar to the previous solution, but the change to the text is made after it has already been written to the button's innerText. Add a click handler (that will execute after the one that is already attached) that performs this fix:

    document.getElementById("myButton").addEventListener("click", function () {
        this.innerText = this.innerText.replace(/(Clicked )(\d+)( times!)/, (_, a, b, c) => a + (b - 100) + c);
    });
    

    Flattened to a bookmarklet:

    javascript: document.getElementById("myButton").addEventListener("click",function(){this.innerText=this.innerText.replace(/(Clicked )(\d+)( times!)/,(_,a,b,c)=>a+(b-100)+c);})
    

    3. Create a new function buttonClicked2

    It should have the code with the modification to the counter. Then change every call to buttonClicked to buttenClicked2. Here I assume it is like in the example, where such reference is only present in the onclick attribute of the button. The more places this call is made from, the more functions you may need to copy as new functions, rewiring also the refences to that function, ...etc. But here it is only the onclick attribute that is affected:

        var buttonClicked2 = (() => {
            let internal_variable = -100; // Our update
            const _internal_buttonClicked = () => {
                internal_variable++;
                document.getElementById("myButton").innerText = `Clicked ${internal_variable} times!`;
            };
            return _internal_buttonClicked.bind(this);
        })();
        // Redirect the click
        document.getElementById("myButton").setAttribute("onclick", "buttonClicked2()");
    

    Flattened to a bookmarklet:

    javascript: {var buttonClicked2=(()=>{let internal_variable=-100;const _internal_buttonClicked=()=>{internal_variable++;document.getElementById("myButton").innerText=`Clicked ${internal_variable} times!`;};return _internal_buttonClicked.bind(this);})();document.getElementById("myButton").setAttribute("onclick", "buttonClicked2()");}
    

    4. Create a page-filling <iframe>

    In that <iframe> put the current HTML, but with the modification to the code you want to have. So the page will reload, but with your modification and in an <iframe>.

    if (window === top) {
        // Patch the HTML
        let html = document.documentElement.innerHTML
                    .replace("internal_variable = 0;", "internal_variable = -100;");
        let iframe = `<body style="margin:0"><iframe style="display:block;border:0;height:100%;width:100%"></iframe></body>`;
        // Replace the current document with just an <iframe>
        document.write(iframe);
        // Write the patched HTML to the frame document
        document.querySelector("iframe").srcdoc = html;
    }
    

    Flattened to a bookmarklet:

    javascript: if(window===top){let html = document.documentElement.innerHTML.replace("internal_variable = 0;", "internal_variable = -100;");let iframe = `<body style="margin:0"><iframe style="display:block;border:0;height:100%;width:100%"></iframe></body>`;document.write(iframe);document.querySelector("iframe").srcdoc=html;}
    

    Limitations

    Each of the above solutions has its limitations. Whether one of them is suitable in real-life cases will depend much on the page's functionalities.