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 "Document.h" 34#include "FileSystem.h" 35#include "KURL.h" 36#include "OriginAccessEntry.h" 37#include "SchemeRegistry.h" 38#include <wtf/StdLibExtras.h> 39 40namespace WebCore { 41 42static SecurityOrigin::LocalLoadPolicy localLoadPolicy = SecurityOrigin::AllowLocalLoadsForLocalOnly; 43const int MaxAllowedPort = 65535; 44 45typedef Vector<OriginAccessEntry> OriginAccessWhiteList; 46typedef HashMap<String, OriginAccessWhiteList*> OriginAccessMap; 47 48static OriginAccessMap& originAccessMap() 49{ 50 DEFINE_STATIC_LOCAL(OriginAccessMap, originAccessMap, ()); 51 return originAccessMap; 52} 53 54static bool schemeRequiresAuthority(const String& scheme) 55{ 56 DEFINE_STATIC_LOCAL(URLSchemesMap, schemes, ()); 57 58 if (schemes.isEmpty()) { 59 schemes.add("http"); 60 schemes.add("https"); 61 schemes.add("ftp"); 62 } 63 64 return schemes.contains(scheme); 65} 66 67 68SecurityOrigin::SecurityOrigin(const KURL& url, SandboxFlags sandboxFlags) 69 : m_sandboxFlags(sandboxFlags) 70 , m_protocol(url.protocol().isNull() ? "" : url.protocol().lower()) 71 , m_host(url.host().isNull() ? "" : url.host().lower()) 72 , m_port(url.port()) 73 , m_isUnique(isSandboxed(SandboxOrigin) || SchemeRegistry::shouldTreatURLSchemeAsNoAccess(m_protocol)) 74 , m_universalAccess(false) 75 , m_domainWasSetInDOM(false) 76 , m_enforceFilePathSeparation(false) 77{ 78 // These protocols do not create security origins; the owner frame provides the origin 79 if (m_protocol == "about" || m_protocol == "javascript") 80 m_protocol = ""; 81 82#if ENABLE(BLOB) || ENABLE(FILE_SYSTEM) 83 bool isBlobOrFileSystemProtocol = false; 84#if ENABLE(BLOB) 85 if (m_protocol == BlobURL::blobProtocol()) 86 isBlobOrFileSystemProtocol = true; 87#endif 88#if ENABLE(FILE_SYSTEM) 89 if (m_protocol == "filesystem") 90 isBlobOrFileSystemProtocol = true; 91#endif 92 if (isBlobOrFileSystemProtocol) { 93 KURL originURL(ParsedURLString, url.path()); 94 if (originURL.isValid()) { 95 m_protocol = originURL.protocol().lower(); 96 m_host = originURL.host().lower(); 97 m_port = originURL.port(); 98 } else 99 m_isUnique = true; 100 } 101#endif 102 103 // For edge case URLs that were probably misparsed, make sure that the origin is unique. 104 if (schemeRequiresAuthority(m_protocol) && m_host.isEmpty()) 105 m_isUnique = true; 106 if (m_protocol.isEmpty()) 107 m_isUnique = true; 108 109 // document.domain starts as m_host, but can be set by the DOM. 110 m_domain = m_host; 111 112 // By default, only local SecurityOrigins can load local resources. 113 m_canLoadLocalResources = isLocal(); 114 if (m_canLoadLocalResources) { 115 // Directories should never be readable. 116 // Note that we do not do this check for blob or filesystem url because its origin is file:/// when it is created from local file urls. 117#if ENABLE(BLOB) || ENABLE(FILE_SYSTEM) 118 bool doDirectoryCheck = !isBlobOrFileSystemProtocol; 119#else 120 bool doDirectoryCheck = true; 121#endif 122 if (doDirectoryCheck && (!url.hasPath() || url.path().endsWith("/"))) 123 m_isUnique = true; 124 // Store the path in case we are doing per-file origin checking. 125 m_filePath = url.path(); 126 } 127 128 if (isDefaultPortForProtocol(m_port, m_protocol)) 129 m_port = 0; 130} 131 132SecurityOrigin::SecurityOrigin(const SecurityOrigin* other) 133 : m_sandboxFlags(other->m_sandboxFlags) 134 , m_protocol(other->m_protocol.threadsafeCopy()) 135 , m_host(other->m_host.threadsafeCopy()) 136 , m_encodedHost(other->m_encodedHost.threadsafeCopy()) 137 , m_domain(other->m_domain.threadsafeCopy()) 138 , m_filePath(other->m_filePath.threadsafeCopy()) 139 , m_port(other->m_port) 140 , m_isUnique(other->m_isUnique) 141 , m_universalAccess(other->m_universalAccess) 142 , m_domainWasSetInDOM(other->m_domainWasSetInDOM) 143 , m_canLoadLocalResources(other->m_canLoadLocalResources) 144 , m_enforceFilePathSeparation(other->m_enforceFilePathSeparation) 145{ 146} 147 148bool SecurityOrigin::isEmpty() const 149{ 150 return m_protocol.isEmpty(); 151} 152 153PassRefPtr<SecurityOrigin> SecurityOrigin::create(const KURL& url, SandboxFlags sandboxFlags) 154{ 155 if (!url.isValid()) 156 return adoptRef(new SecurityOrigin(KURL(), sandboxFlags)); 157 return adoptRef(new SecurityOrigin(url, sandboxFlags)); 158} 159 160PassRefPtr<SecurityOrigin> SecurityOrigin::createEmpty() 161{ 162 return create(KURL()); 163} 164 165PassRefPtr<SecurityOrigin> SecurityOrigin::threadsafeCopy() 166{ 167 return adoptRef(new SecurityOrigin(this)); 168} 169 170void SecurityOrigin::setDomainFromDOM(const String& newDomain) 171{ 172 m_domainWasSetInDOM = true; 173 m_domain = newDomain.lower(); 174} 175 176static HashSet<String>& schemesForbiddenFromDomainRelaxation() 177{ 178 DEFINE_STATIC_LOCAL(HashSet<String>, schemes, ()); 179 return schemes; 180} 181 182void SecurityOrigin::setDomainRelaxationForbiddenForURLScheme(bool forbidden, const String& scheme) 183{ 184 if (scheme.isEmpty()) 185 return; 186 187 if (forbidden) 188 schemesForbiddenFromDomainRelaxation().add(scheme); 189 else 190 schemesForbiddenFromDomainRelaxation().remove(scheme); 191} 192 193bool SecurityOrigin::isDomainRelaxationForbiddenForURLScheme(const String& scheme) 194{ 195 if (scheme.isEmpty()) 196 return false; 197 198 return schemesForbiddenFromDomainRelaxation().contains(scheme); 199} 200 201bool SecurityOrigin::canAccess(const SecurityOrigin* other) const 202{ 203 if (m_universalAccess) 204 return true; 205 206 if (this == other) 207 return true; 208 209 if (isUnique() || other->isUnique()) 210 return false; 211 212 // Here are two cases where we should permit access: 213 // 214 // 1) Neither document has set document.domain. In this case, we insist 215 // that the scheme, host, and port of the URLs match. 216 // 217 // 2) Both documents have set document.domain. In this case, we insist 218 // that the documents have set document.domain to the same value and 219 // that the scheme of the URLs match. 220 // 221 // This matches the behavior of Firefox 2 and Internet Explorer 6. 222 // 223 // Internet Explorer 7 and Opera 9 are more strict in that they require 224 // the port numbers to match when both pages have document.domain set. 225 // 226 // FIXME: Evaluate whether we can tighten this policy to require matched 227 // port numbers. 228 // 229 // Opera 9 allows access when only one page has set document.domain, but 230 // this is a security vulnerability. 231 232 bool canAccess = false; 233 if (m_protocol == other->m_protocol) { 234 if (!m_domainWasSetInDOM && !other->m_domainWasSetInDOM) { 235 if (m_host == other->m_host && m_port == other->m_port) 236 canAccess = true; 237 } else if (m_domainWasSetInDOM && other->m_domainWasSetInDOM) { 238 if (m_domain == other->m_domain) 239 canAccess = true; 240 } 241 } 242 243 if (canAccess && isLocal()) 244 canAccess = passesFileCheck(other); 245 246 return canAccess; 247} 248 249bool SecurityOrigin::passesFileCheck(const SecurityOrigin* other) const 250{ 251 ASSERT(isLocal() && other->isLocal()); 252 253 if (!m_enforceFilePathSeparation && !other->m_enforceFilePathSeparation) 254 return true; 255 256 return (m_filePath == other->m_filePath); 257} 258 259bool SecurityOrigin::canRequest(const KURL& url) const 260{ 261 if (m_universalAccess) 262 return true; 263 264 if (isUnique()) 265 return false; 266 267 RefPtr<SecurityOrigin> targetOrigin = SecurityOrigin::create(url); 268 269 if (targetOrigin->isUnique()) 270 return false; 271 272 // We call isSameSchemeHostPort here instead of canAccess because we want 273 // to ignore document.domain effects. 274 if (isSameSchemeHostPort(targetOrigin.get())) 275 return true; 276 277 if (isAccessWhiteListed(targetOrigin.get())) 278 return true; 279 280 return false; 281} 282 283bool SecurityOrigin::taintsCanvas(const KURL& url) const 284{ 285 if (canRequest(url)) 286 return false; 287 288 // This function exists because we treat data URLs as having a unique origin, 289 // contrary to the current (9/19/2009) draft of the HTML5 specification. 290 // We still want to let folks paint data URLs onto untainted canvases, so 291 // we special case data URLs below. If we change to match HTML5 w.r.t. 292 // data URL security, then we can remove this function in favor of 293 // !canRequest. 294 if (url.protocolIsData()) 295 return false; 296 297 return true; 298} 299 300bool SecurityOrigin::canReceiveDragData(const SecurityOrigin* dragInitiator) const 301{ 302 if (this == dragInitiator) 303 return true; 304 305 // FIXME: Currently we treat data URLs as having a unique origin, contrary to the 306 // current (9/19/2009) draft of the HTML5 specification. We still want to allow 307 // drop across data URLs, so we special case data URLs below. If we change to 308 // match HTML5 w.r.t. data URL security, then we can remove this check. 309 if (m_protocol == "data") 310 return true; 311 312 return canAccess(dragInitiator); 313} 314 315bool SecurityOrigin::isAccessWhiteListed(const SecurityOrigin* targetOrigin) const 316{ 317 if (OriginAccessWhiteList* list = originAccessMap().get(toString())) { 318 for (size_t i = 0; i < list->size(); ++i) { 319 if (list->at(i).matchesOrigin(*targetOrigin)) 320 return true; 321 } 322 } 323 return false; 324} 325 326bool SecurityOrigin::isAccessToURLWhiteListed(const KURL& url) const 327{ 328 RefPtr<SecurityOrigin> targetOrigin = SecurityOrigin::create(url); 329 return isAccessWhiteListed(targetOrigin.get()); 330} 331 332bool SecurityOrigin::canDisplay(const KURL& url) const 333{ 334 String protocol = url.protocol().lower(); 335 336 if (SchemeRegistry::canDisplayOnlyIfCanRequest(protocol)) 337 return canRequest(url); 338 339 if (SchemeRegistry::shouldTreatURLSchemeAsDisplayIsolated(protocol)) 340 return m_protocol == protocol || isAccessToURLWhiteListed(url); 341 342 if (restrictAccessToLocal() && SchemeRegistry::shouldTreatURLSchemeAsLocal(protocol)) 343 return canLoadLocalResources() || isAccessToURLWhiteListed(url); 344 345 return true; 346} 347 348void SecurityOrigin::grantLoadLocalResources() 349{ 350 // This function exists only to support backwards compatibility with older 351 // versions of WebKit. Granting privileges to some, but not all, documents 352 // in a SecurityOrigin is a security hazard because the documents without 353 // the privilege can obtain the privilege by injecting script into the 354 // documents that have been granted the privilege. 355 ASSERT(allowSubstituteDataAccessToLocal()); 356 m_canLoadLocalResources = true; 357} 358 359void SecurityOrigin::grantUniversalAccess() 360{ 361 m_universalAccess = true; 362} 363 364void SecurityOrigin::enforceFilePathSeparation() 365{ 366 ASSERT(isLocal()); 367 m_enforceFilePathSeparation = true; 368} 369 370bool SecurityOrigin::isLocal() const 371{ 372 return SchemeRegistry::shouldTreatURLSchemeAsLocal(m_protocol); 373} 374 375bool SecurityOrigin::isSecureTransitionTo(const KURL& url) const 376{ 377 // New window created by the application 378 if (isEmpty()) 379 return true; 380 381 RefPtr<SecurityOrigin> other = SecurityOrigin::create(url); 382 return canAccess(other.get()); 383} 384 385String SecurityOrigin::toString() const 386{ 387 if (isEmpty()) 388 return "null"; 389 390 if (isUnique()) 391 return "null"; 392 393 if (m_protocol == "file") { 394 if (m_enforceFilePathSeparation) 395 return "null"; 396 return "file://"; 397 } 398 399 Vector<UChar> result; 400 result.reserveInitialCapacity(m_protocol.length() + m_host.length() + 10); 401 append(result, m_protocol); 402 append(result, "://"); 403 append(result, m_host); 404 405 if (m_port) { 406 append(result, ":"); 407 append(result, String::number(m_port)); 408 } 409 410 return String::adopt(result); 411} 412 413PassRefPtr<SecurityOrigin> SecurityOrigin::createFromString(const String& originString) 414{ 415 return SecurityOrigin::create(KURL(KURL(), originString)); 416} 417 418static const char SeparatorCharacter = '_'; 419 420PassRefPtr<SecurityOrigin> SecurityOrigin::createFromDatabaseIdentifier(const String& databaseIdentifier) 421{ 422 // Make sure there's a first separator 423 size_t separator1 = databaseIdentifier.find(SeparatorCharacter); 424 if (separator1 == notFound) 425 return create(KURL()); 426 427 // Make sure there's a second separator 428 size_t separator2 = databaseIdentifier.reverseFind(SeparatorCharacter); 429 if (separator2 == notFound) 430 return create(KURL()); 431 432 // Ensure there were at least 2 separator characters. Some hostnames on intranets have 433 // underscores in them, so we'll assume that any additional underscores are part of the host. 434 if (separator1 == separator2) 435 return create(KURL()); 436 437 // Make sure the port section is a valid port number or doesn't exist 438 bool portOkay; 439 int port = databaseIdentifier.right(databaseIdentifier.length() - separator2 - 1).toInt(&portOkay); 440 bool portAbsent = (separator2 == databaseIdentifier.length() - 1); 441 if (!(portOkay || portAbsent)) 442 return create(KURL()); 443 444 if (port < 0 || port > MaxAllowedPort) 445 return create(KURL()); 446 447 // Split out the 3 sections of data 448 String protocol = databaseIdentifier.substring(0, separator1); 449 String host = databaseIdentifier.substring(separator1 + 1, separator2 - separator1 - 1); 450 451 host = decodeURLEscapeSequences(host); 452 return create(KURL(KURL(), protocol + "://" + host + ":" + String::number(port))); 453} 454 455PassRefPtr<SecurityOrigin> SecurityOrigin::create(const String& protocol, const String& host, int port) 456{ 457 if (port < 0 || port > MaxAllowedPort) 458 create(KURL()); 459 String decodedHost = decodeURLEscapeSequences(host); 460 return create(KURL(KURL(), protocol + "://" + host + ":" + String::number(port))); 461} 462 463String SecurityOrigin::databaseIdentifier() const 464{ 465 String separatorString(&SeparatorCharacter, 1); 466 467 if (m_encodedHost.isEmpty()) 468 m_encodedHost = encodeForFileName(m_host); 469 470 return m_protocol + separatorString + m_encodedHost + separatorString + String::number(m_port); 471} 472 473bool SecurityOrigin::equal(const SecurityOrigin* other) const 474{ 475 if (other == this) 476 return true; 477 478 if (!isSameSchemeHostPort(other)) 479 return false; 480 481 if (m_domainWasSetInDOM != other->m_domainWasSetInDOM) 482 return false; 483 484 if (m_domainWasSetInDOM && m_domain != other->m_domain) 485 return false; 486 487 return true; 488} 489 490bool SecurityOrigin::isSameSchemeHostPort(const SecurityOrigin* other) const 491{ 492 if (m_host != other->m_host) 493 return false; 494 495 if (m_protocol != other->m_protocol) 496 return false; 497 498 if (m_port != other->m_port) 499 return false; 500 501 if (isLocal() && !passesFileCheck(other)) 502 return false; 503 504 return true; 505} 506 507bool SecurityOrigin::shouldHideReferrer(const KURL& url, const String& referrer) 508{ 509 bool referrerIsSecureURL = protocolIs(referrer, "https"); 510 bool referrerIsWebURL = referrerIsSecureURL || protocolIs(referrer, "http"); 511 512 if (!referrerIsWebURL) 513 return true; 514 515 if (!referrerIsSecureURL) 516 return false; 517 518 bool URLIsSecureURL = url.protocolIs("https"); 519 520 return !URLIsSecureURL; 521} 522 523void SecurityOrigin::setLocalLoadPolicy(LocalLoadPolicy policy) 524{ 525 localLoadPolicy = policy; 526} 527 528bool SecurityOrigin::restrictAccessToLocal() 529{ 530 return localLoadPolicy != SecurityOrigin::AllowLocalLoadsForAll; 531} 532 533bool SecurityOrigin::allowSubstituteDataAccessToLocal() 534{ 535 return localLoadPolicy != SecurityOrigin::AllowLocalLoadsForLocalOnly; 536} 537 538void SecurityOrigin::addOriginAccessWhitelistEntry(const SecurityOrigin& sourceOrigin, const String& destinationProtocol, const String& destinationDomains, bool allowDestinationSubdomains) 539{ 540 ASSERT(isMainThread()); 541 ASSERT(!sourceOrigin.isEmpty()); 542 if (sourceOrigin.isEmpty()) 543 return; 544 545 String sourceString = sourceOrigin.toString(); 546 pair<OriginAccessMap::iterator, bool> result = originAccessMap().add(sourceString, 0); 547 if (result.second) 548 result.first->second = new OriginAccessWhiteList; 549 550 OriginAccessWhiteList* list = result.first->second; 551 list->append(OriginAccessEntry(destinationProtocol, destinationDomains, allowDestinationSubdomains ? OriginAccessEntry::AllowSubdomains : OriginAccessEntry::DisallowSubdomains)); 552} 553 554void SecurityOrigin::removeOriginAccessWhitelistEntry(const SecurityOrigin& sourceOrigin, const String& destinationProtocol, const String& destinationDomains, bool allowDestinationSubdomains) 555{ 556 ASSERT(isMainThread()); 557 ASSERT(!sourceOrigin.isEmpty()); 558 if (sourceOrigin.isEmpty()) 559 return; 560 561 String sourceString = sourceOrigin.toString(); 562 OriginAccessMap& map = originAccessMap(); 563 OriginAccessMap::iterator it = map.find(sourceString); 564 if (it == map.end()) 565 return; 566 567 OriginAccessWhiteList* list = it->second; 568 size_t index = list->find(OriginAccessEntry(destinationProtocol, destinationDomains, allowDestinationSubdomains ? OriginAccessEntry::AllowSubdomains : OriginAccessEntry::DisallowSubdomains)); 569 if (index == notFound) 570 return; 571 572 list->remove(index); 573 574 if (!list->isEmpty()) 575 return; 576 577 map.remove(it); 578 delete list; 579} 580 581void SecurityOrigin::resetOriginAccessWhitelists() 582{ 583 ASSERT(isMainThread()); 584 OriginAccessMap& map = originAccessMap(); 585 deleteAllValues(map); 586 map.clear(); 587} 588 589} // namespace WebCore 590