1

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

  • (manually) using the debugger
  • having ownership over the page source (e.g., I can make modifications to it locally, but I can't just send a pull request to add a hook into the script)
  • making this a purely visual change to the DOM?

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:

5
  • That’s impossible. Commented Jun 24, 2024 at 22:18
  • I'm bountying this, though I know there may not be a good solution. I feel that there's a lot of high quality answers on how to debug, patch, or modify a binary, and not a whole lot of information on how to do the same to JS, and I'm hoping to learn how to approach this extremely-common situation a bit better. If there's genuinely no solution, then documentation of why (I assume there's XSS concerns?) would be the next-best thing. Commented Jun 28, 2024 at 20:13
  • If all references to that function are in HTML onclick attributes, then just define a new function and have it referenced in those attributes instead. Would this be enough? Commented Jun 28, 2024 at 20:33
  • 2
    Well, “solutions that involve browser extensions/privileged execution” is a whole different ballpark from “a bookmarklet”. Look into chrome.debugger/the remote debugging protocol. Commented Jun 28, 2024 at 20:52
  • Is this question specifically about inline scripts? Commented Jun 28, 2024 at 23:56

1 Answer 1

0
+250

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Many thanks for the attribution of the bounty. I really hope some parts of this answer were useful to you, @Kaia.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.