254

Disclaimer: I have minimal web-dev/security knowledge so please answer as if talking to a "layman."

I've heard that web-advertisements need to be able to run their own JavaScript so that they can verify they're being viewed by "real users." As this incident on StackOverflow shows, they're basically given free reign.

I also know that JavaScript can be used to capture keystrokes on a webpage.

So in a case like goodreads, where they have ads on the page and user/pass textboxes on the header, is there something in place to prevent the ad from reading keystrokes to record my credentials? Is reading keystrokes simply not possible from an ad?

If I see ads on a login page should I assume that the page is not safe to enter my credentials?

scohe001
  • 1,035
  • 2
  • 7
  • 13
  • 42
    It's worse than that. Web performance tools and similar not only read your credentials but they read the credentials you type and then delete. Very nasty. You might be able to get away with not typing, always pasting your credentials. But the javascript has access to your DOM so it can just read every element. The only way to stop that is not to use credentials but to use oAuth and hand you life over to Google. What could go wrong. – Unicorn Tears Aug 06 '19 at 16:01
  • 26
    I'm surprised the W3 didn't revise the DOM spec to prohibit *any* script access to `` - or least offer a flag attribute to block script access like they have for ` – Dai Aug 07 '19 at 05:54
  • 66
    BTW, most reputable websites don't have any ads on login pages. – Dmitry Grigoryev Aug 07 '19 at 07:40
  • 18
    @TheD That would mean you couldn't use client-side validation (e.g. "the two passwords you entered aren't the same) and you could only ever use a form POST to handle credentials. Both would mean that all the major browsers would simply ignore the spec anyway :) – Luaan Aug 07 '19 at 07:43
  • 3
    @TheD Many sites now have a password strength indicator as you type the password. That wouldn't work. (OTOH if the script server gets hacked, it could still remove the whole form and put another one that sends data elsewhere!) – curiousguy Aug 07 '19 at 08:32
  • 1
    You could turn off Javascript. some browser extensions make this possible on a per page basis. – sudo rm -rf slash Aug 07 '19 at 11:41
  • 5
    @DmitryGrigoryev Unfortunately, many reputable websites these days have a login option either in the header bar or as a popup rather than as its own dedicated page. – Mason Wheeler Aug 07 '19 at 14:52
  • 14
    For non-technical users who are worried about malicious ads, there are plenty of ad blocking extensions to all major browsers, as well as [Privacy Badger](https://www.eff.org/privacybadger), which will actively learn and block 3rd party tracking and advertising scripts as you browse. While not entirely bulletproof, this will prevent the vast majority of adverts and generally improve your web browsing experience. – DBS Aug 07 '19 at 15:24
  • 1
    re goodreads: most sites have dedicated [login page without ads](https://www.goodreads.com/user/sign_in), that is safer to use. If there's no direct link anywhere, usually entering fake credentials lands you on dedicated login page, along with "invalid username/password" message. – el.pescado - нет войне Aug 07 '19 at 19:52
  • @el.pescado that sounds like the start of a good answer for what us Users can do to protect ourselves. "Yes, ads can read your password, so beware of headers and make sure you find the dedicated login page" – scohe001 Aug 07 '19 at 20:11
  • @DmitryGrigoryev: Even reputable websites have trackers running on their login page. I don't see what's the point of focusing on ads if silent trackers are far worse. – user21820 Aug 09 '19 at 09:18
  • Note that StackExchange uses some well-known trackers including Google Analytics, QuantServe and ScoreCardResearch, even on the login page. In general, trackers are far more shady than advertisements, and use far more insidious scripts, because their intended purpose is to track you rather than to sell something to you. – user21820 Aug 09 '19 at 10:26
  • Another good option is the Brave browser. It blocks ads and trackers by default, and gives you quite a bit of data about how many and which ones, if you're interested. It's built on Chrome so it renders sites exactly as you're used to seeing them, has the same devtools, etc. – workerjoe Aug 09 '19 at 17:36

2 Answers2

223

Nothing prevents ads from reading your passwords.

Ads (or any other script like analytics or JavaScript libraries) have access to the main JavaScript scope, and are able to read a lot of sensitive stuff: financial information, passwords, CSRF tokens, etc.

Well, unless they're being loaded in a sandboxed iframe.

Loading an ad in a sandboxed iframe will add security restrictions to the JavaScript scope it has access to, so it won't be able to do nasty stuff.

Unfortunately, most of the third-party scripts are not sandboxed. This is because some of them require access to the main scope to work properly, so they're almost never sandboxed.


As a developer, what can I do?

Since any third-party script could compromise the security of all you personal data, all sensitive pages (like login forms or checkout pages) should be loaded on their own origin (a subdomain is fine).

Using another origin allows us to profit from the Same-Origin Policy: scripts running on the main origin can't access anything on the protected origin.

Note: Content Security Policy and Subresource Integrity could also be used if the third-party can be easily reviewed, but most ad networks couldn't work anymore if you used them.

Dmitry Grigoryev
  • 10,072
  • 1
  • 26
  • 56
Benoit Esnard
  • 13,942
  • 7
  • 65
  • 65
  • 13
    Is there some way a layman like me could tell the difference between a sandbox'ed ad vs. a non-sandbox'ed ad? – scohe001 Aug 06 '19 at 16:06
  • 2
    @scohe001: Do you know how to use the "Inspect element" tool in your browser? A sandboxed iframe has a "sandbox" attribute. I don't know any easy way to check this without any HTML knowledge unfortunately. :( – Benoit Esnard Aug 06 '19 at 16:12
  • 20
    @scohe001 This [Stylus](https://add0n.com/stylus.html) usersheet will put a super-annoying border around unsandboxed iframes and sandboxed iframes that can run scripts: `iframe:not([sandbox]),iframe[sandbox~=allow-scripts]{border:10px solid red !important;border-image:repeating-linear-gradient(45deg,red,red 5%,#ff0 5%,#ff0 10%)10 !important;}` – AuxTaco Aug 07 '19 at 01:18
  • 5
    Same-origin policy protects the external resource from being read by your scripts. If the malicious script is running on your page, it won't care you did load it from an other origin, it will just run and be able to **send** data anywhere it likes. – Kaiido Aug 07 '19 at 06:43
  • @Kaiido: the point is that if you're using ads on the main origin, it won't be able to load anything on the protected origin thanks to the same-origin policy. I'll edit my answer to reflect that idea in a better way. – Benoit Esnard Aug 07 '19 at 06:46
  • Well in that case, that's just an other form of a sandboxed iframe. – Kaiido Aug 07 '19 at 06:55
  • 17
    It is best practice to not have ads on login screens. If a site does not follow that, you can at least complain to them. – eckes Aug 07 '19 at 11:40
  • @AuxTaco That will show you unsafe iframes, but not scripts running in the context of the parent page itself. Unfortunately, I suspect those are just as common as iframes. – Michael Aug 07 '19 at 13:22
  • Wouldn't a sanboxed frame prevent the ad provider from reading the contents of the page to figure out which ad to deliver? (e.g., the ad might want to show motor oil ads on a car blog or something). – jrh Aug 07 '19 at 19:42
  • 1
    it is also best practice to not load arbitrary code from arbitrary third parties and run it on your customers' computers, yet here we are – Eevee Aug 09 '19 at 08:18
29

That depends on how the website loads the ads.

In the case of goodreads, their HTML contains javascript from the ad provider. Specifically, lines 81-145 of the HTML document returned by https://www.goodreads.com/ read:

<script>
  //<![CDATA[
    var gptAdSlots = gptAdSlots || [];
    var googletag = googletag || {};
    googletag.cmd = googletag.cmd || [];
    (function() {
      var gads = document.createElement("script");
      gads.async = true;
      gads.type = "text/javascript";
      var useSSL = "https:" == document.location.protocol;
      gads.src = (useSSL ? "https:" : "http:") +
      "//securepubads.g.doubleclick.net/tag/js/gpt.js";
      var node = document.getElementsByTagName("script")[0];
      node.parentNode.insertBefore(gads, node);
    })();
    // page settings
  //]]>
</script>
<script>
  //<![CDATA[
    googletag.cmd.push(function() {
      googletag.pubads().setTargeting("sid", "osid.bd63050e605ccee9f21515a2dedfdaea");
    googletag.pubads().setTargeting("grsession", "osid.bd63050e605ccee9f21515a2dedfdaea");
    googletag.pubads().setTargeting("surface", "desktop");
    googletag.pubads().setTargeting("signedin", "false");
    googletag.pubads().setTargeting("gr_author", "false");
    googletag.pubads().setTargeting("author", []);
      googletag.pubads().enableAsyncRendering();
      googletag.pubads().enableSingleRequest();
      googletag.pubads().collapseEmptyDivs(true);
      googletag.pubads().disableInitialLoad();
      googletag.enableServices();
    });
  //]]>
</script>
<script>
  //<![CDATA[
    ! function(a9, a, p, s, t, A, g) {
      if (a[a9]) return;

      function q(c, r) {
        a[a9]._Q.push([c, r])
      }
      a[a9] = {
      init: function() {
        q("i", arguments)
      },
      fetchBids: function() {
        q("f", arguments)
      },
      setDisplayBids: function() {},
        _Q: []
      };
      A = p.createElement(s);
      A.async = !0;
      A.src = t;
      g = p.getElementsByTagName(s)[0];
      g.parentNode.insertBefore(A, g)
    }("apstag", window, document, "script", "//c.amazon-adsystem.com/aax2/apstag.js");

    apstag.init({
      pubID: '3211', adServer: 'googletag', bidTimeout: 4e3
    });
  //]]>
</script>

As a consequence, the advertizer's javascript code runs in the same execution context as the website itself, and can do everything the website can, including observing all your interactions with the website.

If they had instead loaded the ads by embedding an iframe from a different origin, the advertizer's code would have run in its own execution context, and the browser would have blocked access to the surrounding website as a violation of the same origin policy.

In general, the only way to tell whether the website has isolated the advertizer's code is to inspect the code of the website.

meriton
  • 1,449
  • 1
  • 10
  • 13
  • 17
    Most advertisers these days don't allow you to load them in iframes. But they "promise" to load the third-party advertisements that they provide in iframes of their own. – Barmar Aug 07 '19 at 17:01