Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "FramingChecker.h"
8 : #include "nsCharSeparatedTokenizer.h"
9 : #include "nsCSPUtils.h"
10 : #include "nsDocShell.h"
11 : #include "nsIChannel.h"
12 : #include "nsIConsoleService.h"
13 : #include "nsIContentSecurityPolicy.h"
14 : #include "nsIScriptError.h"
15 : #include "nsNetUtil.h"
16 : #include "nsQueryObject.h"
17 : #include "mozilla/dom/nsCSPUtils.h"
18 :
19 : using namespace mozilla;
20 :
21 : /* static */ bool
22 0 : FramingChecker::CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel,
23 : const nsAString& aPolicy,
24 : nsIDocShell* aDocShell)
25 : {
26 : static const char allowFrom[] = "allow-from";
27 0 : const uint32_t allowFromLen = ArrayLength(allowFrom) - 1;
28 : bool isAllowFrom =
29 0 : StringHead(aPolicy, allowFromLen).LowerCaseEqualsLiteral(allowFrom);
30 :
31 : // return early if header does not have one of the values with meaning
32 0 : if (!aPolicy.LowerCaseEqualsLiteral("deny") &&
33 0 : !aPolicy.LowerCaseEqualsLiteral("sameorigin") &&
34 0 : !isAllowFrom) {
35 0 : return true;
36 : }
37 :
38 0 : nsCOMPtr<nsIURI> uri;
39 0 : aHttpChannel->GetURI(getter_AddRefs(uri));
40 :
41 : // XXXkhuey when does this happen? Is returning true safe here?
42 0 : if (!aDocShell) {
43 0 : return true;
44 : }
45 :
46 : // We need to check the location of this window and the location of the top
47 : // window, if we're not the top. X-F-O: SAMEORIGIN requires that the
48 : // document must be same-origin with top window. X-F-O: DENY requires that
49 : // the document must never be framed.
50 0 : nsCOMPtr<nsPIDOMWindowOuter> thisWindow = aDocShell->GetWindow();
51 : // If we don't have DOMWindow there is no risk of clickjacking
52 0 : if (!thisWindow) {
53 0 : return true;
54 : }
55 :
56 : // GetScriptableTop, not GetTop, because we want this to respect
57 : // <iframe mozbrowser> boundaries.
58 0 : nsCOMPtr<nsPIDOMWindowOuter> topWindow = thisWindow->GetScriptableTop();
59 :
60 : // if the document is in the top window, it's not in a frame.
61 0 : if (thisWindow == topWindow) {
62 0 : return true;
63 : }
64 :
65 : // Find the top docshell in our parent chain that doesn't have the system
66 : // principal and use it for the principal comparison. Finding the top
67 : // content-type docshell doesn't work because some chrome documents are
68 : // loaded in content docshells (see bug 593387).
69 : nsCOMPtr<nsIDocShellTreeItem> thisDocShellItem(
70 0 : do_QueryInterface(static_cast<nsIDocShell*>(aDocShell)));
71 0 : nsCOMPtr<nsIDocShellTreeItem> parentDocShellItem;
72 0 : nsCOMPtr<nsIDocShellTreeItem> curDocShellItem = thisDocShellItem;
73 0 : nsCOMPtr<nsIDocument> topDoc;
74 : nsresult rv;
75 : nsCOMPtr<nsIScriptSecurityManager> ssm =
76 0 : do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
77 0 : if (!ssm) {
78 0 : MOZ_CRASH();
79 : }
80 :
81 : // Traverse up the parent chain and stop when we see a docshell whose
82 : // parent has a system principal, or a docshell corresponding to
83 : // <iframe mozbrowser>.
84 0 : while (NS_SUCCEEDED(
85 0 : curDocShellItem->GetParent(getter_AddRefs(parentDocShellItem))) &&
86 0 : parentDocShellItem) {
87 0 : nsCOMPtr<nsIDocShell> curDocShell = do_QueryInterface(curDocShellItem);
88 0 : if (curDocShell && curDocShell->GetIsMozBrowser()) {
89 0 : break;
90 : }
91 :
92 0 : bool system = false;
93 0 : topDoc = parentDocShellItem->GetDocument();
94 0 : if (topDoc) {
95 0 : if (NS_SUCCEEDED(
96 0 : ssm->IsSystemPrincipal(topDoc->NodePrincipal(), &system)) &&
97 : system) {
98 : // Found a system-principled doc: last docshell was top.
99 0 : break;
100 : }
101 : } else {
102 0 : return false;
103 : }
104 0 : curDocShellItem = parentDocShellItem;
105 : }
106 :
107 : // If this document has the top non-SystemPrincipal docshell it is not being
108 : // framed or it is being framed by a chrome document, which we allow.
109 0 : if (curDocShellItem == thisDocShellItem) {
110 0 : return true;
111 : }
112 :
113 : // If the value of the header is DENY, and the previous condition is
114 : // not met (current docshell is not the top docshell), prohibit the
115 : // load.
116 0 : if (aPolicy.LowerCaseEqualsLiteral("deny")) {
117 0 : ReportXFOViolation(curDocShellItem, uri, eDENY);
118 0 : return false;
119 : }
120 :
121 0 : topDoc = curDocShellItem->GetDocument();
122 0 : nsCOMPtr<nsIURI> topUri;
123 0 : topDoc->NodePrincipal()->GetURI(getter_AddRefs(topUri));
124 :
125 : // If the X-Frame-Options value is SAMEORIGIN, then the top frame in the
126 : // parent chain must be from the same origin as this document.
127 0 : if (aPolicy.LowerCaseEqualsLiteral("sameorigin")) {
128 0 : rv = ssm->CheckSameOriginURI(uri, topUri, true);
129 0 : if (NS_FAILED(rv)) {
130 0 : ReportXFOViolation(curDocShellItem, uri, eSAMEORIGIN);
131 0 : return false; /* wasn't same-origin */
132 : }
133 : }
134 :
135 : // If the X-Frame-Options value is "allow-from [uri]", then the top
136 : // frame in the parent chain must be from that origin
137 0 : if (isAllowFrom) {
138 0 : if (aPolicy.Length() == allowFromLen ||
139 0 : (aPolicy[allowFromLen] != ' ' &&
140 0 : aPolicy[allowFromLen] != '\t')) {
141 0 : ReportXFOViolation(curDocShellItem, uri, eALLOWFROM);
142 0 : return false;
143 : }
144 0 : rv = NS_NewURI(getter_AddRefs(uri), Substring(aPolicy, allowFromLen));
145 0 : if (NS_FAILED(rv)) {
146 0 : return false;
147 : }
148 :
149 0 : rv = ssm->CheckSameOriginURI(uri, topUri, true);
150 0 : if (NS_FAILED(rv)) {
151 0 : ReportXFOViolation(curDocShellItem, uri, eALLOWFROM);
152 0 : return false;
153 : }
154 : }
155 :
156 0 : return true;
157 : }
158 :
159 : // Ignore x-frame-options if CSP with frame-ancestors exists
160 : static bool
161 3 : ShouldIgnoreFrameOptions(nsIChannel* aChannel, nsIPrincipal* aPrincipal)
162 : {
163 3 : NS_ENSURE_TRUE(aChannel, false);
164 3 : NS_ENSURE_TRUE(aPrincipal, false);
165 :
166 6 : nsCOMPtr<nsIContentSecurityPolicy> csp;
167 3 : aPrincipal->GetCsp(getter_AddRefs(csp));
168 3 : if (!csp) {
169 : // if there is no CSP, then there is nothing to do here
170 3 : return false;
171 : }
172 :
173 0 : bool enforcesFrameAncestors = false;
174 0 : csp->GetEnforcesFrameAncestors(&enforcesFrameAncestors);
175 0 : if (!enforcesFrameAncestors) {
176 : // if CSP does not contain frame-ancestors, then there
177 : // is nothing to do here.
178 0 : return false;
179 : }
180 :
181 : // log warning to console that xfo is ignored because of CSP
182 0 : nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
183 0 : uint64_t innerWindowID = loadInfo ? loadInfo->GetInnerWindowID() : 0;
184 : const char16_t* params[] = { u"x-frame-options",
185 0 : u"frame-ancestors" };
186 0 : CSP_LogLocalizedStr(u"IgnoringSrcBecauseOfDirective",
187 0 : params, ArrayLength(params),
188 0 : EmptyString(), // no sourcefile
189 0 : EmptyString(), // no scriptsample
190 : 0, // no linenumber
191 : 0, // no columnnumber
192 : nsIScriptError::warningFlag,
193 0 : "CSP", innerWindowID);
194 :
195 0 : return true;
196 : }
197 :
198 : // Check if X-Frame-Options permits this document to be loaded as a subdocument.
199 : // This will iterate through and check any number of X-Frame-Options policies
200 : // in the request (comma-separated in a header, multiple headers, etc).
201 : /* static */ bool
202 25 : FramingChecker::CheckFrameOptions(nsIChannel* aChannel,
203 : nsIDocShell* aDocShell,
204 : nsIPrincipal* aPrincipal)
205 : {
206 25 : if (!aChannel || !aDocShell) {
207 22 : return true;
208 : }
209 :
210 3 : if (ShouldIgnoreFrameOptions(aChannel, aPrincipal)) {
211 0 : return true;
212 : }
213 :
214 : nsresult rv;
215 6 : nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
216 3 : if (!httpChannel) {
217 : // check if it is hiding in a multipart channel
218 2 : rv = nsDocShell::Cast(aDocShell)->GetHttpChannel(aChannel, getter_AddRefs(httpChannel));
219 2 : if (NS_FAILED(rv)) {
220 0 : return false;
221 : }
222 : }
223 :
224 3 : if (!httpChannel) {
225 2 : return true;
226 : }
227 :
228 2 : nsAutoCString xfoHeaderCValue;
229 3 : Unused << httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("X-Frame-Options"),
230 2 : xfoHeaderCValue);
231 2 : NS_ConvertUTF8toUTF16 xfoHeaderValue(xfoHeaderCValue);
232 :
233 : // if no header value, there's nothing to do.
234 1 : if (xfoHeaderValue.IsEmpty()) {
235 1 : return true;
236 : }
237 :
238 : // iterate through all the header values (usually there's only one, but can
239 : // be many. If any want to deny the load, deny the load.
240 0 : nsCharSeparatedTokenizer tokenizer(xfoHeaderValue, ',');
241 0 : while (tokenizer.hasMoreTokens()) {
242 0 : const nsAString& tok = tokenizer.nextToken();
243 0 : if (!CheckOneFrameOptionsPolicy(httpChannel, tok, aDocShell)) {
244 : // cancel the load and display about:blank
245 0 : httpChannel->Cancel(NS_BINDING_ABORTED);
246 0 : if (aDocShell) {
247 0 : nsCOMPtr<nsIWebNavigation> webNav(do_QueryObject(aDocShell));
248 0 : if (webNav) {
249 0 : nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->GetLoadInfo();
250 : nsCOMPtr<nsIPrincipal> triggeringPrincipal = loadInfo
251 0 : ? loadInfo->TriggeringPrincipal()
252 0 : : nsContentUtils::GetSystemPrincipal();
253 0 : webNav->LoadURI(u"about:blank",
254 : 0, nullptr, nullptr, nullptr,
255 0 : triggeringPrincipal);
256 : }
257 : }
258 0 : return false;
259 : }
260 : }
261 :
262 0 : return true;
263 : }
264 :
265 : /* static */ void
266 0 : FramingChecker::ReportXFOViolation(nsIDocShellTreeItem* aTopDocShellItem,
267 : nsIURI* aThisURI,
268 : XFOHeader aHeader)
269 : {
270 0 : MOZ_ASSERT(aTopDocShellItem, "Need a top docshell");
271 :
272 0 : nsCOMPtr<nsPIDOMWindowOuter> topOuterWindow = aTopDocShellItem->GetWindow();
273 0 : if (!topOuterWindow) {
274 0 : return;
275 : }
276 :
277 0 : nsPIDOMWindowInner* topInnerWindow = topOuterWindow->GetCurrentInnerWindow();
278 0 : if (!topInnerWindow) {
279 0 : return;
280 : }
281 :
282 0 : nsCOMPtr<nsIURI> topURI;
283 :
284 0 : nsCOMPtr<nsIDocument> document = aTopDocShellItem->GetDocument();
285 0 : nsresult rv = document->NodePrincipal()->GetURI(getter_AddRefs(topURI));
286 0 : if (NS_FAILED(rv)) {
287 0 : return;
288 : }
289 :
290 0 : if (!topURI) {
291 0 : return;
292 : }
293 :
294 0 : nsCString topURIString;
295 0 : nsCString thisURIString;
296 :
297 0 : rv = topURI->GetSpec(topURIString);
298 0 : if (NS_FAILED(rv)) {
299 0 : return;
300 : }
301 :
302 0 : rv = aThisURI->GetSpec(thisURIString);
303 0 : if (NS_FAILED(rv)) {
304 0 : return;
305 : }
306 :
307 : nsCOMPtr<nsIConsoleService> consoleService =
308 0 : do_GetService(NS_CONSOLESERVICE_CONTRACTID);
309 : nsCOMPtr<nsIScriptError> errorObject =
310 0 : do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
311 :
312 0 : if (!consoleService || !errorObject) {
313 0 : return;
314 : }
315 :
316 0 : nsString msg = NS_LITERAL_STRING("Load denied by X-Frame-Options: ");
317 0 : msg.Append(NS_ConvertUTF8toUTF16(thisURIString));
318 :
319 0 : switch (aHeader) {
320 : case eDENY:
321 0 : msg.AppendLiteral(" does not permit framing.");
322 0 : break;
323 : case eSAMEORIGIN:
324 0 : msg.AppendLiteral(" does not permit cross-origin framing.");
325 0 : break;
326 : case eALLOWFROM:
327 0 : msg.AppendLiteral(" does not permit framing by ");
328 0 : msg.Append(NS_ConvertUTF8toUTF16(topURIString));
329 0 : msg.Append('.');
330 0 : break;
331 : }
332 :
333 0 : rv = errorObject->InitWithWindowID(msg, EmptyString(), EmptyString(), 0, 0,
334 : nsIScriptError::errorFlag,
335 : "X-Frame-Options",
336 0 : topInnerWindow->WindowID());
337 0 : if (NS_FAILED(rv)) {
338 0 : return;
339 : }
340 :
341 0 : consoleService->LogMessage(errorObject);
342 : }
|