FramingChecker.cpp (8376B)
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 9 #include <stdint.h> // uint32_t 10 11 #include "mozilla/Assertions.h" 12 #include "mozilla/RefPtr.h" 13 #include "mozilla/Services.h" 14 #include "mozilla/dom/WindowGlobalParent.h" 15 #include "mozilla/net/HttpBaseChannel.h" 16 #include "nsCOMPtr.h" 17 #include "nsContentSecurityUtils.h" 18 #include "nsContentUtils.h" 19 #include "nsDebug.h" 20 #include "nsError.h" 21 #include "nsGlobalWindowOuter.h" 22 #include "nsHttpChannel.h" 23 #include "nsIContentPolicy.h" 24 #include "nsIObserverService.h" 25 #include "nsIScriptError.h" 26 #include "nsLiteralString.h" 27 #include "nsStringFwd.h" 28 #include "nsTArray.h" 29 30 using namespace mozilla; 31 using namespace mozilla::dom; 32 33 /* static */ 34 void FramingChecker::ReportError(const char* aMessageTag, 35 nsIHttpChannel* aChannel, nsIURI* aURI, 36 const nsAString& aPolicy) { 37 MOZ_ASSERT(aChannel); 38 MOZ_ASSERT(aURI); 39 40 nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aChannel); 41 if (!httpChannel) { 42 return; 43 } 44 45 // Get the URL spec 46 nsAutoCString spec; 47 nsresult rv = aURI->GetAsciiSpec(spec); 48 if (NS_FAILED(rv)) { 49 return; 50 } 51 52 nsTArray<nsString> params; 53 params.AppendElement(aPolicy); 54 params.AppendElement(NS_ConvertUTF8toUTF16(spec)); 55 56 httpChannel->AddConsoleReport(nsIScriptError::errorFlag, "X-Frame-Options"_ns, 57 nsContentUtils::eSECURITY_PROPERTIES, spec, 0, 58 0, nsDependentCString(aMessageTag), params); 59 60 // we are notifying observers for testing purposes because there is no event 61 // to gather that an iframe load was blocked or not. 62 nsCOMPtr<nsIObserverService> observerService = 63 mozilla::services::GetObserverService(); 64 nsAutoString policy(aPolicy); 65 observerService->NotifyObservers(aURI, "xfo-on-violate-policy", policy.get()); 66 } 67 68 // Ignore x-frame-options if CSP with frame-ancestors exists 69 static bool ShouldIgnoreFrameOptions(nsIChannel* aChannel, 70 nsIContentSecurityPolicy* aCSP) { 71 NS_ENSURE_TRUE(aChannel, false); 72 if (!aCSP) { 73 return false; 74 } 75 76 bool enforcesFrameAncestors = false; 77 aCSP->GetEnforcesFrameAncestors(&enforcesFrameAncestors); 78 if (!enforcesFrameAncestors) { 79 // if CSP does not contain frame-ancestors, then there 80 // is nothing to do here. 81 return false; 82 } 83 84 return true; 85 } 86 87 // Check if X-Frame-Options permits this document to be loaded as a 88 // subdocument. This will iterate through and check any number of 89 // X-Frame-Options policies in the request (comma-separated in a header, 90 // multiple headers, etc). 91 // This is based on: 92 // https://html.spec.whatwg.org/multipage/document-lifecycle.html#the-x-frame-options-header 93 /* static */ 94 bool FramingChecker::CheckFrameOptions(nsIChannel* aChannel, 95 nsIContentSecurityPolicy* aCsp, 96 bool& outIsFrameCheckingSkipped) { 97 // Step 1. If navigable is not a child navigable return true 98 if (!aChannel) { 99 return true; 100 } 101 102 // xfo check only makes sense for subdocument and object loads, if this is 103 // not a load of such type, there is nothing to do here. 104 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); 105 ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); 106 if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT && 107 contentType != ExtContentPolicy::TYPE_OBJECT) { 108 return true; 109 } 110 111 // xfo can only hang off an httpchannel, if this is not an httpChannel 112 // then there is nothing to do here. 113 nsCOMPtr<nsIHttpChannel> httpChannel; 114 nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( 115 aChannel, getter_AddRefs(httpChannel)); 116 if (NS_WARN_IF(NS_FAILED(rv))) { 117 return true; 118 } 119 if (!httpChannel) { 120 return true; 121 } 122 123 // ignore XFO checks on channels that will be redirected 124 uint32_t responseStatus; 125 rv = httpChannel->GetResponseStatus(&responseStatus); 126 if (NS_FAILED(rv)) { 127 // GetResponseStatus returning failure is expected in several situations, so 128 // do not warn if it fails. 129 return true; 130 } 131 if (mozilla::net::nsHttpChannel::IsRedirectStatus(responseStatus)) { 132 return true; 133 } 134 135 nsAutoCString xfoHeaderValue; 136 (void)httpChannel->GetResponseHeader("X-Frame-Options"_ns, xfoHeaderValue); 137 138 // Step 10. (paritally) if the only header we received was empty, then we 139 // process it as if it wasn't sent at all. 140 if (xfoHeaderValue.IsEmpty()) { 141 return true; 142 } 143 144 // Step 2. xfo checks are ignored in the case where CSP frame-ancestors is 145 // present, if so, there is nothing to do here. 146 if (ShouldIgnoreFrameOptions(aChannel, aCsp)) { 147 outIsFrameCheckingSkipped = true; 148 return true; 149 } 150 151 static const char kASCIIWhitespace[] = "\t "; 152 153 // Step 3-4. reduce the header options to a unique set and count how many 154 // unique values (that we track) are encountered. this avoids using a set to 155 // stop attackers from inheriting arbitrary values in memory and reduce the 156 // complexity of the code. 157 XFOHeader xfoOptions; 158 for (const nsACString& next : xfoHeaderValue.Split(',')) { 159 nsAutoCString option(next); 160 option.Trim(kASCIIWhitespace); 161 162 if (option.LowerCaseEqualsLiteral("allowall")) { 163 xfoOptions.ALLOWALL = true; 164 } else if (option.LowerCaseEqualsLiteral("sameorigin")) { 165 xfoOptions.SAMEORIGIN = true; 166 } else if (option.LowerCaseEqualsLiteral("deny")) { 167 xfoOptions.DENY = true; 168 } else { 169 xfoOptions.INVALID = true; 170 } 171 } 172 173 nsCOMPtr<nsIURI> uri; 174 httpChannel->GetURI(getter_AddRefs(uri)); 175 176 // Step 6. if header has multiple contradicting directives return early and 177 // prohibit the load. ALLOWALL is considered here for legacy reasons. 178 uint32_t xfoUniqueOptions = xfoOptions.DENY + xfoOptions.ALLOWALL + 179 xfoOptions.SAMEORIGIN + xfoOptions.INVALID; 180 if (xfoUniqueOptions > 1 && 181 (xfoOptions.DENY || xfoOptions.ALLOWALL || xfoOptions.SAMEORIGIN)) { 182 ReportError("XFrameOptionsInvalid", httpChannel, uri, u"invalid"_ns); 183 return false; 184 } 185 186 // Step 7 (multiple INVALID values) and partially Step 10 (single INVALID 187 // value). if header has any invalid options, but no valid directives (DENY, 188 // ALLOWALL, SAMEORIGIN) then allow the load. 189 if (xfoOptions.INVALID) { 190 ReportError("XFrameOptionsInvalid", httpChannel, uri, u"invalid"_ns); 191 return true; 192 } 193 194 // Step 8. if the value of the header is DENY prohibit the load. 195 if (xfoOptions.DENY) { 196 ReportError("XFrameOptionsDeny", httpChannel, uri, u"deny"_ns); 197 return false; 198 } 199 200 // Step 9. If the X-Frame-Options value is SAMEORIGIN, then the top frame in 201 // the parent chain must be from the same origin as this document. 202 RefPtr<mozilla::dom::BrowsingContext> ctx; 203 loadInfo->GetBrowsingContext(getter_AddRefs(ctx)); 204 205 while (ctx && xfoOptions.SAMEORIGIN) { 206 nsCOMPtr<nsIPrincipal> principal; 207 // Generally CheckFrameOptions is consulted from within the 208 // DocumentLoadListener in the parent process. For loads of type object and 209 // embed it's called from the Document in the content process. 210 if (XRE_IsParentProcess()) { 211 WindowGlobalParent* window = ctx->Canonical()->GetCurrentWindowGlobal(); 212 if (window) { 213 // Using the URI of the Principal and not the document because 214 // window.open inherits the principal and hence the URI of the opening 215 // context needed for same origin checks. 216 principal = window->DocumentPrincipal(); 217 } 218 } else if (nsPIDOMWindowOuter* windowOuter = ctx->GetDOMWindow()) { 219 principal = nsGlobalWindowOuter::Cast(windowOuter)->GetPrincipal(); 220 } 221 222 if (principal && principal->IsSystemPrincipal()) { 223 return true; 224 } 225 226 // one of the ancestors is not same origin as this document 227 if (!principal || !principal->IsSameOrigin(uri)) { 228 ReportError("XFrameOptionsDeny", httpChannel, uri, u"sameorigin"_ns); 229 return false; 230 } 231 ctx = ctx->GetParent(); 232 } 233 234 // Step 10. 235 return true; 236 }