DEV Community

loading...
Cover image for I tried to mount a client-side "attack" on a news website poll by using only Javascript. And I failed miserably.

I tried to mount a client-side "attack" on a news website poll by using only Javascript. And I failed miserably.

Ivan Spoljaric
I love coding and drinking large amounts of coffee.
Originally published at dev.to ・3 min read

Alt Text

First step - Running the script locally

For academic purposes I tried to create a client-side script to manipulate the results of a random poll on a Croatian news portal

The poll is open at the moment writing, but it probably won't stay that way for long.

Here's the code

The code consists of these steps:

  • waiting for "DOMContentLoaded" event

  • closing the cookie banner

  • selecting a poll answer

  • MutationObserver indicates changes in the DOMTree target iframe. This means that the results are "in". Then the localStorage is cleared.

  • a timer, which started running immediately after "DOMContentLoaded", reloads the page after 2 seconds. And the script starts from the beginning

It works as intended if you run it directly in the dev tools console.

You'll probably notice how the code is tightly coupled with the html/css implementation of the web page.

Since I was creating a proof of concept I didn't bother to write the functions in a generalised way.

I used the exact CSS class names from the site, and targeted the poll iframe based on its position in the HTML.

I had a pretty strong hunch that it won't work anyway (not that it stopped me from trying).

Second step - Automating the script

The next step was to think of a way to run the script automatically, without the need to paste the code in the console every time.

So, I created a custom browser extension, which has only one additional manifest.json file.

And that didn't work.

Line 2 is the problem.

  document.getElementsByTagName('iframe')[3].contentDocument;
Enter fullscreen mode Exit fullscreen mode

It doesn't work because of the "Same Origin Policy".

It's a "critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin".

And this also applies to iframes.

"External" iframe's can't be accessed, nor manipulated from a document which is not served on the same origin (domain).

...

For completeness sake, I also tried to use the 3 most popular browser extensions that enable running custom scripts on any web page;

  • GreaseMonkey
  • TamperMonkey
  • ViolentMonkey.

I tested out a few StackOverflow suggestions, related to the configuration of those extensions, in a foolish attempt to beat the system.

But with no luck.

You can't beat the system by breaking its hard rules. Unless you're the One. And it turns out I'm not. At least not yet.

A glimmer of hope

Fortunately not all of my work was in vain.

As I was slowly accepting my fate, and getting ready to completely give up, I stumbled on an alternative approach to this problem.

There's a method called Window.postMessage

And its API looks kind of promising (with regard to CORS issues caused by external iframe communication).

So the story continues. Stay tuned. :)

Conclusion

Do you know any other way, or a hack, to bypass the Same Origin Policy?

Is there another approach to the "external iframe" problem, which I didn't think of?

Or is it just plain impossible to do this on the client (FE) side (which is a good thing I suppose, because it prevents malicious behaviour).

Discussion (16)

Collapse
lukeshiru profile image
LUKESHIRU • Edited

If you add ?pbdebug=true to the URL, you get a lot of useful information in the console about the sdk they used for the poll. It seems they have a script just before the div that holds the iframe with this on it:

(function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(d.getElementById(id))return;js=d.createElement(s);js.id=id;js.src='https://embed.ex.co/sdk.js';fjs.parentNode.insertBefore(js,fjs);}(document,'script','exco-sdk'));
Enter fullscreen mode Exit fullscreen mode

So we can make use of their own sdk to reload the iframe without reloading the entire page, by creating a bookmarklet and doing that.

Still, my recommendation for this kind of things is to click on the answer you want to spam while you have the network panel of chrome devtools open, and then open the context menu over that network request and click "copy > copy as fetch". That will give you a code like this:

fetch("https://voting.ex.co/poll/", {
    "headers": {
        "accept": "application/x-www-form-urlencoded",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/x-www-form-urlencoded",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "cross-site",
        "sec-gpc": "1"
    },
    "referrer": "https://www.index.hr/",
    "referrerPolicy": "no-referrer-when-downgrade",
    "body": "sectionId=ae72bb2c-e667-43a2-ab2e-5eb6ace904f3&questionId=7e5e4ba0-4881-4f8c-9c6d-e006a656028d&resultId=6e22dbf0-bc5b-4a35-825d-b2c683b774a3&pageIdentifier=fb424e5b-f1b1-4383-a4d6-f8b5e1349782&allowedVoteCount=1",
    "method": "POST",
    "mode": "cors",
    "credentials": "omit"
});
Enter fullscreen mode Exit fullscreen mode

And you can change that code slightly to make it spam requests:

const vote = () =>
    fetch("https://voting.ex.co/poll/", {
        headers: {
            accept: "application/x-www-form-urlencoded",
            "accept-language": "en-US,en;q=0.9,es-AR;q=0.8,es;q=0.7",
            "content-type": "application/x-www-form-urlencoded",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "cross-site",
            "sec-gpc": "1"
        },
        referrer: "https://www.index.hr/",
        referrerPolicy: "no-referrer-when-downgrade",
        body: new URLSearchParams({
            allowedVoteCount: "1",
            pageIdentifier: "fb424e5b-f1b1-4383-a4d6-f8b5e1349782",
            questionId: "7e5e4ba0-4881-4f8c-9c6d-e006a656028d",
            resultId: "6e22dbf0-bc5b-4a35-825d-b2c683b774a3",
            sectionId: "ae72bb2c-e667-43a2-ab2e-5eb6ace904f3"
        }).toString(),
        method: "POST",
        mode: "cors",
        credentials: "omit"
    }).then(vote);
vote();
Enter fullscreen mode Exit fullscreen mode

Every time the promise finishes, it calls the same function again. Then you can put that in a bookmarklet, and you have made a one-click spammer:

javascript:(()=>{const vote=()=>fetch("https://voting.ex.co/poll/",{headers:{accept:"application/x-www-form-urlencoded","accept-language":"en-US,en;q=0.9,es-AR;q=0.8,es;q=0.7","content-type":"application/x-www-form-urlencoded","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","sec-gpc":"1"},referrer:"https://www.index.hr/",referrerPolicy:"no-referrer-when-downgrade",body:new URLSearchParams({allowedVoteCount:"1",pageIdentifier:"fb424e5b-f1b1-4383-a4d6-f8b5e1349782",questionId:"7e5e4ba0-4881-4f8c-9c6d-e006a656028d",resultId:"6e22dbf0-bc5b-4a35-825d-b2c683b774a3",sectionId:"ae72bb2c-e667-43a2-ab2e-5eb6ace904f3"}).toString(),method:"POST",mode:"cors",credentials:"omit"}).then(vote);vote();})();
Enter fullscreen mode Exit fullscreen mode

Hope it helps in future endeavors :D

Cheers!

Collapse
ispoljari profile image
Ivan Spoljaric Author • Edited

I love this approach.
It's doing the same thing I wanted in essence, but without the hassle with iframes. Much cleaner and streamlined.

CORS might be an issue though.. The iframe has it's own origin. And the server is configured to accept requests to /poll from within it. I think calling this endpoint from the console, or a bookmarklet, won't work because of this reason.

I will try it out anyway. Thx :)

Collapse
jonsamp profile image
Jon Samp

If you’re open to running a script from outside the website (a node script), you could use a tool like puppeteer to open a web page, then have it click on the poll, then have it repeat the process indefinitely. It would mimic a real user so there would be no CORS issues. This is similar to how you would end-to-end test a website, but in this case you’d be “testing” another site.

Collapse
ispoljari profile image
Ivan Spoljaric Author • Edited

Hey. Thx for the comment. I thought of a Node.js approach to bypass CORS. Not sure if it would work though - because iframes are weird. In any case that wouldn't be a client-side "attack" anymore.

Collapse
Sloan, the sloth mascot
Comment deleted
ispoljari profile image
Ivan Spoljaric Author • Edited

True. But unlike browsers Node.js servers don't implement the Same Origin Policy. So technically speaking, yeah, you are still a "client" for the target BE - although somewhat different - even though you are running your script from a server. Maybe I should have been more precise and called it a "browser-side attack".

Based on experience, i know it would be easier to try this from the server side because there are no CORS related issues. I am just not sure what would happen if I tampered with iframes in this scenario. I'll have to test this out.

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Sloan, the sloth mascot
Comment deleted
Collapse
aheisleycook profile image
privatecloudev

Try using selenium js