Coverage Report

Created: 2018-09-25 14:53

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