1/* 2 * Copyright (C) 2007 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 14 * its contributors may be used to endorse or promote products derived 15 * from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29#include "config.h" 30#include "SecurityOrigin.h" 31 32#include "BlobURL.h" 33#include "FileSystem.h" 34#include "KURL.h" 35#include "SchemeRegistry.h" 36#include "SecurityPolicy.h" 37#include "ThreadableBlobRegistry.h" 38#include <wtf/MainThread.h> 39#include <wtf/StdLibExtras.h> 40#include <wtf/text/StringBuilder.h> 41 42namespace WebCore { 43 44const int InvalidPort = 0; 45const int MaxAllowedPort = 65535; 46 47static bool schemeRequiresHost(const KURL& url) 48{ 49 // We expect URLs with these schemes to have authority components. If the 50 // URL lacks an authority component, we get concerned and mark the origin 51 // as unique. 52 return url.protocolIsInHTTPFamily() || url.protocolIs("ftp"); 53} 54 55bool SecurityOrigin::shouldUseInnerURL(const KURL& url) 56{ 57#if ENABLE(BLOB) 58 // FIXME: Blob URLs don't have inner URLs. Their form is "blob:<inner-origin>/<UUID>", so treating the part after "blob:" as a URL is incorrect. 59 if (url.protocolIs("blob")) 60 return true; 61#endif 62#if ENABLE(FILE_SYSTEM) 63 if (url.protocolIs("filesystem")) 64 return true; 65#endif 66 UNUSED_PARAM(url); 67 return false; 68} 69 70// In general, extracting the inner URL varies by scheme. It just so happens 71// that all the URL schemes we currently support that use inner URLs for their 72// security origin can be parsed using this algorithm. 73KURL SecurityOrigin::extractInnerURL(const KURL& url) 74{ 75 if (url.innerURL()) 76 return *url.innerURL(); 77 // FIXME: Update this callsite to use the innerURL member function when 78 // we finish implementing it. 79 return KURL(ParsedURLString, decodeURLEscapeSequences(url.path())); 80} 81 82static PassRefPtr<SecurityOrigin> getCachedOrigin(const KURL& url) 83{ 84#if ENABLE(BLOB) 85 if (url.protocolIs("blob")) 86 return ThreadableBlobRegistry::getCachedOrigin(url); 87#else 88 UNUSED_PARAM(url); 89#endif 90 return 0; 91} 92 93static bool shouldTreatAsUniqueOrigin(const KURL& url) 94{ 95 if (!url.isValid()) 96 return true; 97 98 // FIXME: Do we need to unwrap the URL further? 99 KURL innerURL = SecurityOrigin::shouldUseInnerURL(url) ? SecurityOrigin::extractInnerURL(url) : url; 100 101 // FIXME: Check whether innerURL is valid. 102 103 // For edge case URLs that were probably misparsed, make sure that the origin is unique. 104 // This is an additional safety net against bugs in KURL parsing, and for network back-ends that parse URLs differently, 105 // and could misinterpret another component for hostname. 106 if (schemeRequiresHost(innerURL) && innerURL.host().isEmpty()) 107 return true; 108 109 // SchemeRegistry needs a lower case protocol because it uses HashMaps 110 // that assume the scheme has already been canonicalized. 111 String protocol = innerURL.protocol().lower(); 112 113 if (SchemeRegistry::shouldTreatURLSchemeAsNoAccess(protocol)) 114 return true; 115 116 // This is the common case. 117 return false; 118} 119 120SecurityOrigin::SecurityOrigin(const KURL& url) 121 : m_protocol(url.protocol().isNull() ? "" : url.protocol().lower()) 122 , m_host(url.host().isNull() ? "" : url.host().lower()) 123 , m_port(url.port()) 124 , m_isUnique(false) 125 , m_universalAccess(false) 126 , m_domainWasSetInDOM(false) 127 , m_storageBlockingPolicy(AllowAllStorage) 128 , m_enforceFilePathSeparation(false) 129 , m_needsDatabaseIdentifierQuirkForFiles(false) 130{ 131 // document.domain starts as m_host, but can be set by the DOM. 132 m_domain = m_host; 133 134 if (isDefaultPortForProtocol(m_port, m_protocol)) 135 m_port = InvalidPort; 136 137 // By default, only local SecurityOrigins can load local resources. 138 m_canLoadLocalResources = isLocal(); 139 140 if (m_canLoadLocalResources) 141 m_filePath = url.path(); // In case enforceFilePathSeparation() is called. 142} 143 144SecurityOrigin::SecurityOrigin() 145 : m_protocol("") 146 , m_host("") 147 , m_domain("") 148 , m_port(InvalidPort) 149 , m_isUnique(true) 150 , m_universalAccess(false) 151 , m_domainWasSetInDOM(false) 152 , m_canLoadLocalResources(false) 153 , m_storageBlockingPolicy(AllowAllStorage) 154 , m_enforceFilePathSeparation(false) 155 , m_needsDatabaseIdentifierQuirkForFiles(false) 156{ 157} 158 159SecurityOrigin::SecurityOrigin(const SecurityOrigin* other) 160 : m_protocol(other->m_protocol.isolatedCopy()) 161 , m_host(other->m_host.isolatedCopy()) 162 , m_domain(other->m_domain.isolatedCopy()) 163 , m_filePath(other->m_filePath.isolatedCopy()) 164 , m_port(other->m_port) 165 , m_isUnique(other->m_isUnique) 166 , m_universalAccess(other->m_universalAccess) 167 , m_domainWasSetInDOM(other->m_domainWasSetInDOM) 168 , m_canLoadLocalResources(other->m_canLoadLocalResources) 169 , m_storageBlockingPolicy(other->m_storageBlockingPolicy) 170 , m_enforceFilePathSeparation(other->m_enforceFilePathSeparation) 171 , m_needsDatabaseIdentifierQuirkForFiles(other->m_needsDatabaseIdentifierQuirkForFiles) 172{ 173} 174 175PassRefPtr<SecurityOrigin> SecurityOrigin::create(const KURL& url) 176{ 177 RefPtr<SecurityOrigin> cachedOrigin = getCachedOrigin(url); 178 if (cachedOrigin.get()) 179 return cachedOrigin; 180 181 if (shouldTreatAsUniqueOrigin(url)) { 182 RefPtr<SecurityOrigin> origin = adoptRef(new SecurityOrigin()); 183 184 if (url.protocolIs("file")) { 185 // Unfortunately, we can't represent all unique origins exactly 186 // the same way because we need to produce a quirky database 187 // identifier for file URLs due to persistent storage in some 188 // embedders of WebKit. 189 origin->m_needsDatabaseIdentifierQuirkForFiles = true; 190 } 191 192 return origin.release(); 193 } 194 195 if (shouldUseInnerURL(url)) 196 return adoptRef(new SecurityOrigin(extractInnerURL(url))); 197 198 return adoptRef(new SecurityOrigin(url)); 199} 200 201PassRefPtr<SecurityOrigin> SecurityOrigin::createUnique() 202{ 203 RefPtr<SecurityOrigin> origin = adoptRef(new SecurityOrigin()); 204 ASSERT(origin->isUnique()); 205 return origin.release(); 206} 207 208PassRefPtr<SecurityOrigin> SecurityOrigin::isolatedCopy() const 209{ 210 return adoptRef(new SecurityOrigin(this)); 211} 212 213void SecurityOrigin::setDomainFromDOM(const String& newDomain) 214{ 215 m_domainWasSetInDOM = true; 216 m_domain = newDomain.lower(); 217} 218 219bool SecurityOrigin::isSecure(const KURL& url) 220{ 221 // Invalid URLs are secure, as are URLs which have a secure protocol. 222 if (!url.isValid() || SchemeRegistry::shouldTreatURLSchemeAsSecure(url.protocol())) 223 return true; 224 225 // URLs that wrap inner URLs are secure if those inner URLs are secure. 226 if (shouldUseInnerURL(url) && SchemeRegistry::shouldTreatURLSchemeAsSecure(extractInnerURL(url).protocol())) 227 return true; 228 229 return false; 230} 231 232bool SecurityOrigin::canAccess(const SecurityOrigin* other) const 233{ 234 if (m_universalAccess) 235 return true; 236 237 if (this == other) 238 return true; 239 240 if (isUnique() || other->isUnique()) 241 return false; 242 243 // Here are two cases where we should permit access: 244 // 245 // 1) Neither document has set document.domain. In this case, we insist 246 // that the scheme, host, and port of the URLs match. 247 // 248 // 2) Both documents have set document.domain. In this case, we insist 249 // that the documents have set document.domain to the same value and 250 // that the scheme of the URLs match. 251 // 252 // This matches the behavior of Firefox 2 and Internet Explorer 6. 253 // 254 // Internet Explorer 7 and Opera 9 are more strict in that they require 255 // the port numbers to match when both pages have document.domain set. 256 // 257 // FIXME: Evaluate whether we can tighten this policy to require matched 258 // port numbers. 259 // 260 // Opera 9 allows access when only one page has set document.domain, but 261 // this is a security vulnerability. 262 263 bool canAccess = false; 264 if (m_protocol == other->m_protocol) { 265 if (!m_domainWasSetInDOM && !other->m_domainWasSetInDOM) { 266 if (m_host == other->m_host && m_port == other->m_port) 267 canAccess = true; 268 } else if (m_domainWasSetInDOM && other->m_domainWasSetInDOM) { 269 if (m_domain == other->m_domain) 270 canAccess = true; 271 } 272 } 273 274 if (canAccess && isLocal()) 275 canAccess = passesFileCheck(other); 276 277 return canAccess; 278} 279 280bool SecurityOrigin::passesFileCheck(const SecurityOrigin* other) const 281{ 282 ASSERT(isLocal() && other->isLocal()); 283 284 if (!m_enforceFilePathSeparation && !other->m_enforceFilePathSeparation) 285 return true; 286 287 return (m_filePath == other->m_filePath); 288} 289 290bool SecurityOrigin::canRequest(const KURL& url) const 291{ 292 if (m_universalAccess) 293 return true; 294 295 if (getCachedOrigin(url) == this) 296 return true; 297 298 if (isUnique()) 299 return false; 300 301 RefPtr<SecurityOrigin> targetOrigin = SecurityOrigin::create(url); 302 303 if (targetOrigin->isUnique()) 304 return false; 305 306 // We call isSameSchemeHostPort here instead of canAccess because we want 307 // to ignore document.domain effects. 308 if (isSameSchemeHostPort(targetOrigin.get())) 309 return true; 310 311 if (SecurityPolicy::isAccessWhiteListed(this, targetOrigin.get())) 312 return true; 313 314 return false; 315} 316 317bool SecurityOrigin::taintsCanvas(const KURL& url) const 318{ 319 if (canRequest(url)) 320 return false; 321 322 // This function exists because we treat data URLs as having a unique origin, 323 // contrary to the current (9/19/2009) draft of the HTML5 specification. 324 // We still want to let folks paint data URLs onto untainted canvases, so 325 // we special case data URLs below. If we change to match HTML5 w.r.t. 326 // data URL security, then we can remove this function in favor of 327 // !canRequest. 328 if (url.protocolIsData()) 329 return false; 330 331 return true; 332} 333 334bool SecurityOrigin::canReceiveDragData(const SecurityOrigin* dragInitiator) const 335{ 336 if (this == dragInitiator) 337 return true; 338 339 return canAccess(dragInitiator); 340} 341 342// This is a hack to allow keep navigation to http/https feeds working. To remove this 343// we need to introduce new API akin to registerURLSchemeAsLocal, that registers a 344// protocols navigation policy. 345// feed(|s|search): is considered a 'nesting' scheme by embedders that support it, so it can be 346// local or remote depending on what is nested. Currently we just check if we are nesting 347// http or https, otherwise we ignore the nesting for the purpose of a security check. We need 348// a facility for registering nesting schemes, and some generalized logic for them. 349// This function should be removed as an outcome of https://bugs.webkit.org/show_bug.cgi?id=69196 350static bool isFeedWithNestedProtocolInHTTPFamily(const KURL& url) 351{ 352 const String& urlString = url.string(); 353 if (!urlString.startsWith("feed", false)) 354 return false; 355 356 return urlString.startsWith("feed://", false) 357 || urlString.startsWith("feed:http:", false) || urlString.startsWith("feed:https:", false) 358 || urlString.startsWith("feeds:http:", false) || urlString.startsWith("feeds:https:", false) 359 || urlString.startsWith("feedsearch:http:", false) || urlString.startsWith("feedsearch:https:", false); 360} 361 362bool SecurityOrigin::canDisplay(const KURL& url) const 363{ 364 if (m_universalAccess) 365 return true; 366 367 String protocol = url.protocol().lower(); 368 369 if (isFeedWithNestedProtocolInHTTPFamily(url)) 370 return true; 371 372 if (SchemeRegistry::canDisplayOnlyIfCanRequest(protocol)) 373 return canRequest(url); 374 375 if (SchemeRegistry::shouldTreatURLSchemeAsDisplayIsolated(protocol)) 376 return m_protocol == protocol || SecurityPolicy::isAccessToURLWhiteListed(this, url); 377 378 if (SecurityPolicy::restrictAccessToLocal() && SchemeRegistry::shouldTreatURLSchemeAsLocal(protocol)) 379 return canLoadLocalResources() || SecurityPolicy::isAccessToURLWhiteListed(this, url); 380 381 return true; 382} 383 384bool SecurityOrigin::canAccessStorage(const SecurityOrigin* topOrigin, ShouldAllowFromThirdParty shouldAllowFromThirdParty) const 385{ 386 if (isUnique()) 387 return false; 388 389 if (m_storageBlockingPolicy == BlockAllStorage) 390 return false; 391 392 // FIXME: This check should be replaced with an ASSERT once we can guarantee that topOrigin is not null. 393 if (!topOrigin) 394 return true; 395 396 if (topOrigin->m_storageBlockingPolicy == BlockAllStorage) 397 return false; 398 399 if (shouldAllowFromThirdParty == AlwaysAllowFromThirdParty) 400 return true; 401 402 if ((m_storageBlockingPolicy == BlockThirdPartyStorage || topOrigin->m_storageBlockingPolicy == BlockThirdPartyStorage) && topOrigin->isThirdParty(this)) 403 return false; 404 405 return true; 406} 407 408SecurityOrigin::Policy SecurityOrigin::canShowNotifications() const 409{ 410 if (m_universalAccess) 411 return AlwaysAllow; 412 if (isUnique()) 413 return AlwaysDeny; 414 return Ask; 415} 416 417bool SecurityOrigin::isThirdParty(const SecurityOrigin* child) const 418{ 419 if (child->m_universalAccess) 420 return false; 421 422 if (this == child) 423 return false; 424 425 if (isUnique() || child->isUnique()) 426 return true; 427 428 return !isSameSchemeHostPort(child); 429} 430 431void SecurityOrigin::grantLoadLocalResources() 432{ 433 // Granting privileges to some, but not all, documents in a SecurityOrigin 434 // is a security hazard because the documents without the privilege can 435 // obtain the privilege by injecting script into the documents that have 436 // been granted the privilege. 437 m_canLoadLocalResources = true; 438} 439 440void SecurityOrigin::grantUniversalAccess() 441{ 442 m_universalAccess = true; 443} 444 445#if ENABLE(CACHE_PARTITIONING) 446String SecurityOrigin::cachePartition() const 447{ 448 if (m_storageBlockingPolicy != BlockThirdPartyStorage) 449 return String(); 450 451 if (m_protocol != "http" && m_protocol != "https") 452 return String(); 453 454 return host(); 455} 456#endif 457 458void SecurityOrigin::enforceFilePathSeparation() 459{ 460 ASSERT(isLocal()); 461 m_enforceFilePathSeparation = true; 462} 463 464bool SecurityOrigin::isLocal() const 465{ 466 return SchemeRegistry::shouldTreatURLSchemeAsLocal(m_protocol); 467} 468 469String SecurityOrigin::toString() const 470{ 471 if (isUnique()) 472 return "null"; 473 if (m_protocol == "file" && m_enforceFilePathSeparation) 474 return "null"; 475 return toRawString(); 476} 477 478String SecurityOrigin::toRawString() const 479{ 480 if (m_protocol == "file") 481 return "file://"; 482 483 StringBuilder result; 484 result.reserveCapacity(m_protocol.length() + m_host.length() + 10); 485 result.append(m_protocol); 486 result.append("://"); 487 result.append(m_host); 488 489 if (m_port) { 490 result.append(':'); 491 result.appendNumber(m_port); 492 } 493 494 return result.toString(); 495} 496 497PassRefPtr<SecurityOrigin> SecurityOrigin::createFromString(const String& originString) 498{ 499 return SecurityOrigin::create(KURL(KURL(), originString)); 500} 501 502static const char separatorCharacter = '_'; 503 504PassRefPtr<SecurityOrigin> SecurityOrigin::createFromDatabaseIdentifier(const String& databaseIdentifier) 505{ 506 // Make sure there's a first separator 507 size_t separator1 = databaseIdentifier.find(separatorCharacter); 508 if (separator1 == notFound) 509 return create(KURL()); 510 511 // Make sure there's a second separator 512 size_t separator2 = databaseIdentifier.reverseFind(separatorCharacter); 513 if (separator2 == notFound) 514 return create(KURL()); 515 516 // Ensure there were at least 2 separator characters. Some hostnames on intranets have 517 // underscores in them, so we'll assume that any additional underscores are part of the host. 518 if (separator1 == separator2) 519 return create(KURL()); 520 521 // Make sure the port section is a valid port number or doesn't exist 522 bool portOkay; 523 int port = databaseIdentifier.right(databaseIdentifier.length() - separator2 - 1).toInt(&portOkay); 524 bool portAbsent = (separator2 == databaseIdentifier.length() - 1); 525 if (!(portOkay || portAbsent)) 526 return create(KURL()); 527 528 if (port < 0 || port > MaxAllowedPort) 529 return create(KURL()); 530 531 // Split out the 3 sections of data 532 String protocol = databaseIdentifier.substring(0, separator1); 533 String host = databaseIdentifier.substring(separator1 + 1, separator2 - separator1 - 1); 534 535 host = decodeURLEscapeSequences(host); 536 return create(KURL(KURL(), protocol + "://" + host + ":" + String::number(port) + "/")); 537} 538 539PassRefPtr<SecurityOrigin> SecurityOrigin::create(const String& protocol, const String& host, int port) 540{ 541 if (port < 0 || port > MaxAllowedPort) 542 return createUnique(); 543 String decodedHost = decodeURLEscapeSequences(host); 544 return create(KURL(KURL(), protocol + "://" + host + ":" + String::number(port) + "/")); 545} 546 547String SecurityOrigin::databaseIdentifier() const 548{ 549 // Historically, we've used the following (somewhat non-sensical) string 550 // for the databaseIdentifier of local files. We used to compute this 551 // string because of a bug in how we handled the scheme for file URLs. 552 // Now that we've fixed that bug, we still need to produce this string 553 // to avoid breaking existing persistent state. 554 if (m_needsDatabaseIdentifierQuirkForFiles) 555 return "file__0"; 556 557 StringBuilder stringBuilder; 558 stringBuilder.append(m_protocol); 559 stringBuilder.append(separatorCharacter); 560 stringBuilder.append(encodeForFileName(m_host)); 561 stringBuilder.append(separatorCharacter); 562 stringBuilder.appendNumber(m_port); 563 564 return stringBuilder.toString(); 565} 566 567bool SecurityOrigin::equal(const SecurityOrigin* other) const 568{ 569 if (other == this) 570 return true; 571 572 if (!isSameSchemeHostPort(other)) 573 return false; 574 575 if (m_domainWasSetInDOM != other->m_domainWasSetInDOM) 576 return false; 577 578 if (m_domainWasSetInDOM && m_domain != other->m_domain) 579 return false; 580 581 return true; 582} 583 584bool SecurityOrigin::isSameSchemeHostPort(const SecurityOrigin* other) const 585{ 586 if (m_host != other->m_host) 587 return false; 588 589 if (m_protocol != other->m_protocol) 590 return false; 591 592 if (m_port != other->m_port) 593 return false; 594 595 if (isLocal() && !passesFileCheck(other)) 596 return false; 597 598 return true; 599} 600 601String SecurityOrigin::urlWithUniqueSecurityOrigin() 602{ 603 ASSERT(isMainThread()); 604 DEFINE_STATIC_LOCAL(const String, uniqueSecurityOriginURL, (ASCIILiteral("data:,"))); 605 return uniqueSecurityOriginURL; 606} 607 608} // namespace WebCore 609