|
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 #include "nsDocShell.h" |
|
7 #include "nsDSURIContentListener.h" |
|
8 #include "nsIChannel.h" |
|
9 #include "nsServiceManagerUtils.h" |
|
10 #include "nsDocShellCID.h" |
|
11 #include "nsIWebNavigationInfo.h" |
|
12 #include "nsIDocument.h" |
|
13 #include "nsIDOMWindow.h" |
|
14 #include "nsNetUtil.h" |
|
15 #include "nsAutoPtr.h" |
|
16 #include "nsIHttpChannel.h" |
|
17 #include "nsIScriptSecurityManager.h" |
|
18 #include "nsError.h" |
|
19 #include "nsCharSeparatedTokenizer.h" |
|
20 #include "nsIConsoleService.h" |
|
21 #include "nsIScriptError.h" |
|
22 #include "nsDocShellLoadTypes.h" |
|
23 |
|
24 using namespace mozilla; |
|
25 |
|
26 //***************************************************************************** |
|
27 //*** nsDSURIContentListener: Object Management |
|
28 //***************************************************************************** |
|
29 |
|
30 nsDSURIContentListener::nsDSURIContentListener(nsDocShell* aDocShell) |
|
31 : mDocShell(aDocShell), |
|
32 mParentContentListener(nullptr) |
|
33 { |
|
34 } |
|
35 |
|
36 nsDSURIContentListener::~nsDSURIContentListener() |
|
37 { |
|
38 } |
|
39 |
|
40 nsresult |
|
41 nsDSURIContentListener::Init() |
|
42 { |
|
43 nsresult rv; |
|
44 mNavInfo = do_GetService(NS_WEBNAVIGATION_INFO_CONTRACTID, &rv); |
|
45 NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to get webnav info"); |
|
46 return rv; |
|
47 } |
|
48 |
|
49 |
|
50 //***************************************************************************** |
|
51 // nsDSURIContentListener::nsISupports |
|
52 //***************************************************************************** |
|
53 |
|
54 NS_IMPL_ADDREF(nsDSURIContentListener) |
|
55 NS_IMPL_RELEASE(nsDSURIContentListener) |
|
56 |
|
57 NS_INTERFACE_MAP_BEGIN(nsDSURIContentListener) |
|
58 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURIContentListener) |
|
59 NS_INTERFACE_MAP_ENTRY(nsIURIContentListener) |
|
60 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) |
|
61 NS_INTERFACE_MAP_END |
|
62 |
|
63 //***************************************************************************** |
|
64 // nsDSURIContentListener::nsIURIContentListener |
|
65 //***************************************************************************** |
|
66 |
|
67 NS_IMETHODIMP |
|
68 nsDSURIContentListener::OnStartURIOpen(nsIURI* aURI, bool* aAbortOpen) |
|
69 { |
|
70 // If mDocShell is null here, that means someone's starting a load |
|
71 // in our docshell after it's already been destroyed. Don't let |
|
72 // that happen. |
|
73 if (!mDocShell) { |
|
74 *aAbortOpen = true; |
|
75 return NS_OK; |
|
76 } |
|
77 |
|
78 nsCOMPtr<nsIURIContentListener> parentListener; |
|
79 GetParentContentListener(getter_AddRefs(parentListener)); |
|
80 if (parentListener) |
|
81 return parentListener->OnStartURIOpen(aURI, aAbortOpen); |
|
82 |
|
83 return NS_OK; |
|
84 } |
|
85 |
|
86 NS_IMETHODIMP |
|
87 nsDSURIContentListener::DoContent(const char* aContentType, |
|
88 bool aIsContentPreferred, |
|
89 nsIRequest* request, |
|
90 nsIStreamListener** aContentHandler, |
|
91 bool* aAbortProcess) |
|
92 { |
|
93 nsresult rv; |
|
94 NS_ENSURE_ARG_POINTER(aContentHandler); |
|
95 NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE); |
|
96 |
|
97 // Check whether X-Frame-Options permits us to load this content in an |
|
98 // iframe and abort the load (unless we've disabled x-frame-options |
|
99 // checking). |
|
100 if (!CheckFrameOptions(request)) { |
|
101 *aAbortProcess = true; |
|
102 return NS_OK; |
|
103 } |
|
104 |
|
105 *aAbortProcess = false; |
|
106 |
|
107 // determine if the channel has just been retargeted to us... |
|
108 nsLoadFlags loadFlags = 0; |
|
109 nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(request); |
|
110 |
|
111 if (aOpenedChannel) |
|
112 aOpenedChannel->GetLoadFlags(&loadFlags); |
|
113 |
|
114 if(loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) |
|
115 { |
|
116 // XXX: Why does this not stop the content too? |
|
117 mDocShell->Stop(nsIWebNavigation::STOP_NETWORK); |
|
118 |
|
119 mDocShell->SetLoadType(aIsContentPreferred ? LOAD_LINK : LOAD_NORMAL); |
|
120 } |
|
121 |
|
122 rv = mDocShell->CreateContentViewer(aContentType, request, aContentHandler); |
|
123 |
|
124 if (rv == NS_ERROR_REMOTE_XUL) { |
|
125 request->Cancel(rv); |
|
126 *aAbortProcess = true; |
|
127 return NS_OK; |
|
128 } |
|
129 |
|
130 if (NS_FAILED(rv)) { |
|
131 // we don't know how to handle the content |
|
132 *aContentHandler = nullptr; |
|
133 return rv; |
|
134 } |
|
135 |
|
136 if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) { |
|
137 nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(static_cast<nsIDocShell*>(mDocShell)); |
|
138 NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE); |
|
139 domWindow->Focus(); |
|
140 } |
|
141 |
|
142 return NS_OK; |
|
143 } |
|
144 |
|
145 NS_IMETHODIMP |
|
146 nsDSURIContentListener::IsPreferred(const char* aContentType, |
|
147 char ** aDesiredContentType, |
|
148 bool* aCanHandle) |
|
149 { |
|
150 NS_ENSURE_ARG_POINTER(aCanHandle); |
|
151 NS_ENSURE_ARG_POINTER(aDesiredContentType); |
|
152 |
|
153 // the docshell has no idea if it is the preferred content provider or not. |
|
154 // It needs to ask its parent if it is the preferred content handler or not... |
|
155 |
|
156 nsCOMPtr<nsIURIContentListener> parentListener; |
|
157 GetParentContentListener(getter_AddRefs(parentListener)); |
|
158 if (parentListener) { |
|
159 return parentListener->IsPreferred(aContentType, |
|
160 aDesiredContentType, |
|
161 aCanHandle); |
|
162 } |
|
163 // we used to return false here if we didn't have a parent properly |
|
164 // registered at the top of the docshell hierarchy to dictate what |
|
165 // content types this docshell should be a preferred handler for. But |
|
166 // this really makes it hard for developers using iframe or browser tags |
|
167 // because then they need to make sure they implement |
|
168 // nsIURIContentListener otherwise all link clicks would get sent to |
|
169 // another window because we said we weren't the preferred handler type. |
|
170 // I'm going to change the default now...if we can handle the content, |
|
171 // and someone didn't EXPLICITLY set a nsIURIContentListener at the top |
|
172 // of our docshell chain, then we'll now always attempt to process the |
|
173 // content ourselves... |
|
174 return CanHandleContent(aContentType, |
|
175 true, |
|
176 aDesiredContentType, |
|
177 aCanHandle); |
|
178 } |
|
179 |
|
180 NS_IMETHODIMP |
|
181 nsDSURIContentListener::CanHandleContent(const char* aContentType, |
|
182 bool aIsContentPreferred, |
|
183 char ** aDesiredContentType, |
|
184 bool* aCanHandleContent) |
|
185 { |
|
186 NS_PRECONDITION(aCanHandleContent, "Null out param?"); |
|
187 NS_ENSURE_ARG_POINTER(aDesiredContentType); |
|
188 |
|
189 *aCanHandleContent = false; |
|
190 *aDesiredContentType = nullptr; |
|
191 |
|
192 nsresult rv = NS_OK; |
|
193 if (aContentType) { |
|
194 uint32_t canHandle = nsIWebNavigationInfo::UNSUPPORTED; |
|
195 rv = mNavInfo->IsTypeSupported(nsDependentCString(aContentType), |
|
196 mDocShell, |
|
197 &canHandle); |
|
198 *aCanHandleContent = (canHandle != nsIWebNavigationInfo::UNSUPPORTED); |
|
199 } |
|
200 |
|
201 return rv; |
|
202 } |
|
203 |
|
204 NS_IMETHODIMP |
|
205 nsDSURIContentListener::GetLoadCookie(nsISupports ** aLoadCookie) |
|
206 { |
|
207 NS_IF_ADDREF(*aLoadCookie = nsDocShell::GetAsSupports(mDocShell)); |
|
208 return NS_OK; |
|
209 } |
|
210 |
|
211 NS_IMETHODIMP |
|
212 nsDSURIContentListener::SetLoadCookie(nsISupports * aLoadCookie) |
|
213 { |
|
214 #ifdef DEBUG |
|
215 nsRefPtr<nsDocLoader> cookieAsDocLoader = |
|
216 nsDocLoader::GetAsDocLoader(aLoadCookie); |
|
217 NS_ASSERTION(cookieAsDocLoader && cookieAsDocLoader == mDocShell, |
|
218 "Invalid load cookie being set!"); |
|
219 #endif |
|
220 return NS_OK; |
|
221 } |
|
222 |
|
223 NS_IMETHODIMP |
|
224 nsDSURIContentListener::GetParentContentListener(nsIURIContentListener** |
|
225 aParentListener) |
|
226 { |
|
227 if (mWeakParentContentListener) |
|
228 { |
|
229 nsCOMPtr<nsIURIContentListener> tempListener = |
|
230 do_QueryReferent(mWeakParentContentListener); |
|
231 *aParentListener = tempListener; |
|
232 NS_IF_ADDREF(*aParentListener); |
|
233 } |
|
234 else { |
|
235 *aParentListener = mParentContentListener; |
|
236 NS_IF_ADDREF(*aParentListener); |
|
237 } |
|
238 return NS_OK; |
|
239 } |
|
240 |
|
241 NS_IMETHODIMP |
|
242 nsDSURIContentListener::SetParentContentListener(nsIURIContentListener* |
|
243 aParentListener) |
|
244 { |
|
245 if (aParentListener) |
|
246 { |
|
247 // Store the parent listener as a weak ref. Parents not supporting |
|
248 // nsISupportsWeakReference assert but may still be used. |
|
249 mParentContentListener = nullptr; |
|
250 mWeakParentContentListener = do_GetWeakReference(aParentListener); |
|
251 if (!mWeakParentContentListener) |
|
252 { |
|
253 mParentContentListener = aParentListener; |
|
254 } |
|
255 } |
|
256 else |
|
257 { |
|
258 mWeakParentContentListener = nullptr; |
|
259 mParentContentListener = nullptr; |
|
260 } |
|
261 return NS_OK; |
|
262 } |
|
263 |
|
264 bool nsDSURIContentListener::CheckOneFrameOptionsPolicy(nsIHttpChannel *httpChannel, |
|
265 const nsAString& policy) { |
|
266 static const char allowFrom[] = "allow-from"; |
|
267 const uint32_t allowFromLen = ArrayLength(allowFrom) - 1; |
|
268 bool isAllowFrom = |
|
269 StringHead(policy, allowFromLen).LowerCaseEqualsLiteral(allowFrom); |
|
270 |
|
271 // return early if header does not have one of the values with meaning |
|
272 if (!policy.LowerCaseEqualsLiteral("deny") && |
|
273 !policy.LowerCaseEqualsLiteral("sameorigin") && |
|
274 !isAllowFrom) |
|
275 return true; |
|
276 |
|
277 nsCOMPtr<nsIURI> uri; |
|
278 httpChannel->GetURI(getter_AddRefs(uri)); |
|
279 |
|
280 // XXXkhuey when does this happen? Is returning true safe here? |
|
281 if (!mDocShell) { |
|
282 return true; |
|
283 } |
|
284 |
|
285 // We need to check the location of this window and the location of the top |
|
286 // window, if we're not the top. X-F-O: SAMEORIGIN requires that the |
|
287 // document must be same-origin with top window. X-F-O: DENY requires that |
|
288 // the document must never be framed. |
|
289 nsCOMPtr<nsIDOMWindow> thisWindow = do_GetInterface(static_cast<nsIDocShell*>(mDocShell)); |
|
290 // If we don't have DOMWindow there is no risk of clickjacking |
|
291 if (!thisWindow) |
|
292 return true; |
|
293 |
|
294 // GetScriptableTop, not GetTop, because we want this to respect |
|
295 // <iframe mozbrowser> boundaries. |
|
296 nsCOMPtr<nsIDOMWindow> topWindow; |
|
297 thisWindow->GetScriptableTop(getter_AddRefs(topWindow)); |
|
298 |
|
299 // if the document is in the top window, it's not in a frame. |
|
300 if (thisWindow == topWindow) |
|
301 return true; |
|
302 |
|
303 // Find the top docshell in our parent chain that doesn't have the system |
|
304 // principal and use it for the principal comparison. Finding the top |
|
305 // content-type docshell doesn't work because some chrome documents are |
|
306 // loaded in content docshells (see bug 593387). |
|
307 nsCOMPtr<nsIDocShellTreeItem> thisDocShellItem(do_QueryInterface( |
|
308 static_cast<nsIDocShell*> (mDocShell))); |
|
309 nsCOMPtr<nsIDocShellTreeItem> parentDocShellItem, |
|
310 curDocShellItem = thisDocShellItem; |
|
311 nsCOMPtr<nsIDocument> topDoc; |
|
312 nsresult rv; |
|
313 nsCOMPtr<nsIScriptSecurityManager> ssm = |
|
314 do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); |
|
315 if (!ssm) { |
|
316 MOZ_CRASH(); |
|
317 } |
|
318 |
|
319 // Traverse up the parent chain and stop when we see a docshell whose |
|
320 // parent has a system principal, or a docshell corresponding to |
|
321 // <iframe mozbrowser>. |
|
322 while (NS_SUCCEEDED(curDocShellItem->GetParent(getter_AddRefs(parentDocShellItem))) && |
|
323 parentDocShellItem) { |
|
324 |
|
325 nsCOMPtr<nsIDocShell> curDocShell = do_QueryInterface(curDocShellItem); |
|
326 if (curDocShell && curDocShell->GetIsBrowserOrApp()) { |
|
327 break; |
|
328 } |
|
329 |
|
330 bool system = false; |
|
331 topDoc = do_GetInterface(parentDocShellItem); |
|
332 if (topDoc) { |
|
333 if (NS_SUCCEEDED(ssm->IsSystemPrincipal(topDoc->NodePrincipal(), |
|
334 &system)) && system) { |
|
335 // Found a system-principled doc: last docshell was top. |
|
336 break; |
|
337 } |
|
338 } |
|
339 else { |
|
340 return false; |
|
341 } |
|
342 curDocShellItem = parentDocShellItem; |
|
343 } |
|
344 |
|
345 // If this document has the top non-SystemPrincipal docshell it is not being |
|
346 // framed or it is being framed by a chrome document, which we allow. |
|
347 if (curDocShellItem == thisDocShellItem) |
|
348 return true; |
|
349 |
|
350 // If the value of the header is DENY, and the previous condition is |
|
351 // not met (current docshell is not the top docshell), prohibit the |
|
352 // load. |
|
353 if (policy.LowerCaseEqualsLiteral("deny")) { |
|
354 ReportXFOViolation(curDocShellItem, uri, eDENY); |
|
355 return false; |
|
356 } |
|
357 |
|
358 topDoc = do_GetInterface(curDocShellItem); |
|
359 nsCOMPtr<nsIURI> topUri; |
|
360 topDoc->NodePrincipal()->GetURI(getter_AddRefs(topUri)); |
|
361 |
|
362 // If the X-Frame-Options value is SAMEORIGIN, then the top frame in the |
|
363 // parent chain must be from the same origin as this document. |
|
364 if (policy.LowerCaseEqualsLiteral("sameorigin")) { |
|
365 rv = ssm->CheckSameOriginURI(uri, topUri, true); |
|
366 if (NS_FAILED(rv)) { |
|
367 ReportXFOViolation(curDocShellItem, uri, eSAMEORIGIN); |
|
368 return false; /* wasn't same-origin */ |
|
369 } |
|
370 } |
|
371 |
|
372 // If the X-Frame-Options value is "allow-from [uri]", then the top |
|
373 // frame in the parent chain must be from that origin |
|
374 if (isAllowFrom) { |
|
375 if (policy.Length() == allowFromLen || |
|
376 (policy[allowFromLen] != ' ' && |
|
377 policy[allowFromLen] != '\t')) { |
|
378 ReportXFOViolation(curDocShellItem, uri, eALLOWFROM); |
|
379 return false; |
|
380 } |
|
381 rv = NS_NewURI(getter_AddRefs(uri), |
|
382 Substring(policy, allowFromLen)); |
|
383 if (NS_FAILED(rv)) |
|
384 return false; |
|
385 |
|
386 rv = ssm->CheckSameOriginURI(uri, topUri, true); |
|
387 if (NS_FAILED(rv)) { |
|
388 ReportXFOViolation(curDocShellItem, uri, eALLOWFROM); |
|
389 return false; |
|
390 } |
|
391 } |
|
392 |
|
393 return true; |
|
394 } |
|
395 |
|
396 // Check if X-Frame-Options permits this document to be loaded as a subdocument. |
|
397 // This will iterate through and check any number of X-Frame-Options policies |
|
398 // in the request (comma-separated in a header, multiple headers, etc). |
|
399 bool nsDSURIContentListener::CheckFrameOptions(nsIRequest *request) |
|
400 { |
|
401 nsresult rv; |
|
402 nsCOMPtr<nsIChannel> chan = do_QueryInterface(request); |
|
403 if (!chan) { |
|
404 return true; |
|
405 } |
|
406 |
|
407 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(chan); |
|
408 if (!httpChannel) { |
|
409 // check if it is hiding in a multipart channel |
|
410 rv = mDocShell->GetHttpChannel(chan, getter_AddRefs(httpChannel)); |
|
411 if (NS_FAILED(rv)) |
|
412 return false; |
|
413 } |
|
414 |
|
415 if (!httpChannel) { |
|
416 return true; |
|
417 } |
|
418 |
|
419 nsAutoCString xfoHeaderCValue; |
|
420 httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("X-Frame-Options"), |
|
421 xfoHeaderCValue); |
|
422 NS_ConvertUTF8toUTF16 xfoHeaderValue(xfoHeaderCValue); |
|
423 |
|
424 // if no header value, there's nothing to do. |
|
425 if (xfoHeaderValue.IsEmpty()) |
|
426 return true; |
|
427 |
|
428 // iterate through all the header values (usually there's only one, but can |
|
429 // be many. If any want to deny the load, deny the load. |
|
430 nsCharSeparatedTokenizer tokenizer(xfoHeaderValue, ','); |
|
431 while (tokenizer.hasMoreTokens()) { |
|
432 const nsSubstring& tok = tokenizer.nextToken(); |
|
433 if (!CheckOneFrameOptionsPolicy(httpChannel, tok)) { |
|
434 // cancel the load and display about:blank |
|
435 httpChannel->Cancel(NS_BINDING_ABORTED); |
|
436 if (mDocShell) { |
|
437 nsCOMPtr<nsIWebNavigation> webNav(do_QueryObject(mDocShell)); |
|
438 if (webNav) { |
|
439 webNav->LoadURI(MOZ_UTF16("about:blank"), |
|
440 0, nullptr, nullptr, nullptr); |
|
441 } |
|
442 } |
|
443 return false; |
|
444 } |
|
445 } |
|
446 |
|
447 return true; |
|
448 } |
|
449 |
|
450 void |
|
451 nsDSURIContentListener::ReportXFOViolation(nsIDocShellTreeItem* aTopDocShellItem, |
|
452 nsIURI* aThisURI, |
|
453 XFOHeader aHeader) |
|
454 { |
|
455 nsresult rv = NS_OK; |
|
456 |
|
457 nsCOMPtr<nsPIDOMWindow> topOuterWindow = do_GetInterface(aTopDocShellItem); |
|
458 if (!topOuterWindow) |
|
459 return; |
|
460 |
|
461 NS_ASSERTION(topOuterWindow->IsOuterWindow(), "Huh?"); |
|
462 nsPIDOMWindow* topInnerWindow = topOuterWindow->GetCurrentInnerWindow(); |
|
463 if (!topInnerWindow) |
|
464 return; |
|
465 |
|
466 nsCOMPtr<nsIURI> topURI; |
|
467 |
|
468 nsCOMPtr<nsIDocument> document; |
|
469 |
|
470 document = do_GetInterface(aTopDocShellItem); |
|
471 rv = document->NodePrincipal()->GetURI(getter_AddRefs(topURI)); |
|
472 if (NS_FAILED(rv)) |
|
473 return; |
|
474 |
|
475 if (!topURI) |
|
476 return; |
|
477 |
|
478 nsCString topURIString; |
|
479 nsCString thisURIString; |
|
480 |
|
481 rv = topURI->GetSpec(topURIString); |
|
482 if (NS_FAILED(rv)) |
|
483 return; |
|
484 |
|
485 rv = aThisURI->GetSpec(thisURIString); |
|
486 if (NS_FAILED(rv)) |
|
487 return; |
|
488 |
|
489 nsCOMPtr<nsIConsoleService> consoleService = |
|
490 do_GetService(NS_CONSOLESERVICE_CONTRACTID); |
|
491 nsCOMPtr<nsIScriptError> errorObject = |
|
492 do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); |
|
493 |
|
494 if (!consoleService || !errorObject) |
|
495 return; |
|
496 |
|
497 nsString msg = NS_LITERAL_STRING("Load denied by X-Frame-Options: "); |
|
498 msg.Append(NS_ConvertUTF8toUTF16(thisURIString)); |
|
499 |
|
500 switch (aHeader) { |
|
501 case eDENY: |
|
502 msg.AppendLiteral(" does not permit framing."); |
|
503 break; |
|
504 case eSAMEORIGIN: |
|
505 msg.AppendLiteral(" does not permit cross-origin framing."); |
|
506 break; |
|
507 case eALLOWFROM: |
|
508 msg.AppendLiteral(" does not permit framing by "); |
|
509 msg.Append(NS_ConvertUTF8toUTF16(topURIString)); |
|
510 msg.AppendLiteral("."); |
|
511 break; |
|
512 } |
|
513 |
|
514 rv = errorObject->InitWithWindowID(msg, EmptyString(), EmptyString(), 0, 0, |
|
515 nsIScriptError::errorFlag, |
|
516 "X-Frame-Options", |
|
517 topInnerWindow->WindowID()); |
|
518 if (NS_FAILED(rv)) |
|
519 return; |
|
520 |
|
521 consoleService->LogMessage(errorObject); |
|
522 } |