diff --git a/caps/idl/moz.build b/caps/idl/moz.build index e888f2f760b45..bf392583227cd 100644 --- a/caps/idl/moz.build +++ b/caps/idl/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. XPIDL_SOURCES += [ + 'nsIDomainPolicy.idl', 'nsIPrincipal.idl', 'nsIScriptSecurityManager.idl', 'nsISecurityCheckedComponent.idl', diff --git a/caps/idl/nsIDomainPolicy.idl b/caps/idl/nsIDomainPolicy.idl new file mode 100644 index 0000000000000..e77c45681cacc --- /dev/null +++ b/caps/idl/nsIDomainPolicy.idl @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIDomainSet; + +/* + * When a domain policy is instantiated by invoking activateDomainPolicy() on + * nsIScriptSecurityManager, these domain sets are consulted when each new + * global is created (they have no effect on already-created globals). + * If javascript is globally enabled with |javascript.enabled|, the blacklists + * are consulted. If globally disabled, the whitelists are consulted. Lookups + * on blacklist and whitelist happen with contains(), and lookups on + * superBlacklist and superWhitelist happen with containsSuperDomain(). + * + * When deactivate() is invoked, the domain sets are emptied, and the + * nsIDomainPolicy ceases to have any effect on the system. + */ +[scriptable, builtinclass, uuid(27b10f54-f34b-42b7-8594-4348d3ad7953)] +interface nsIDomainPolicy : nsISupports +{ + readonly attribute nsIDomainSet blacklist; + readonly attribute nsIDomainSet superBlacklist; + readonly attribute nsIDomainSet whitelist; + readonly attribute nsIDomainSet superWhitelist; + + void deactivate(); +}; + +[scriptable, builtinclass, uuid(946a01ff-6525-4007-a2c2-447ebe1875d3)] +interface nsIDomainSet : nsISupports +{ + /* + * Add a domain to the set. No-op if it already exists. + */ + void add(in nsIURI aDomain); + + /* + * Remove a domain from the set. No-op if it doesn't exist. + */ + void remove(in nsIURI aDomain); + + /* + * Remove all entries from the set. + */ + void clear(); + + /* + * Returns true if a given domain is in the set. + */ + bool contains(in nsIURI aDomain); + + /* + * Returns true if a given domain is a subdomain of one of the entries in + * the set. + */ + bool containsSuperDomain(in nsIURI aDomain); +}; diff --git a/caps/idl/nsIScriptSecurityManager.idl b/caps/idl/nsIScriptSecurityManager.idl index 0349adc85a02f..7dfa1ab7f535e 100644 --- a/caps/idl/nsIScriptSecurityManager.idl +++ b/caps/idl/nsIScriptSecurityManager.idl @@ -9,8 +9,9 @@ interface nsIURI; interface nsIChannel; interface nsIDocShell; +interface nsIDomainPolicy; -[scriptable, uuid(36619e6e-6ed3-4fd5-9a16-763f3614e5ed)] +[scriptable, uuid(2911ae60-1b5f-47e6-941e-1bb7b53a167d)] interface nsIScriptSecurityManager : nsIXPCSecurityManager { ///////////////// Security Checks ////////////////// @@ -228,6 +229,29 @@ interface nsIScriptSecurityManager : nsIXPCSecurityManager * inMozBrowser has to be true if the app is inside a mozbrowser iframe. */ AUTF8String getJarPrefix(in unsigned long appId, in boolean inMozBrowser); + + /** + * Per-domain controls to enable and disable script. This system is designed + * to be used by at most one consumer, and enforces this with its semantics. + * + * Initially, domainPolicyActive is false. When activateDomainPolicy() is + * invoked, domainPolicyActive becomes true, and subsequent calls to + * activateDomainPolicy() will fail until deactivate() is invoked on the + * nsIDomainPolicy returned from activateDomainPolicy(). At this point, + * domainPolicyActive becomes false again, and a new consumer may acquire + * control of the system by invoking activateDomainPolicy(). + */ + nsIDomainPolicy activateDomainPolicy(); + readonly attribute boolean domainPolicyActive; + + /** + * Query mechanism for the above policy. + * + * If domainPolicyEnabled is false, this simply returns the current value + * of javascript.enabled. Otherwise, it returns the same value, but taking + * the various blacklist/whitelist exceptions into account. + */ + bool policyAllowsScript(in nsIURI aDomain); }; %{C++ diff --git a/caps/include/DomainPolicy.h b/caps/include/DomainPolicy.h new file mode 100644 index 0000000000000..857f85e9e6f5b --- /dev/null +++ b/caps/include/DomainPolicy.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DomainPolicy_h__ +#define DomainPolicy_h__ + +#include "nsIDomainPolicy.h" +#include "nsTHashtable.h" +#include "nsURIHashKey.h" + +namespace mozilla { + +// The name "DomainPolicy" conflicts with some of the old-style policy machinery +// in nsScriptSecurityManager.cpp, which needs to #include this file. So we +// temporarily use a sub-namespace until that machinery goes away in bug 913734. +namespace hotness { + +class DomainPolicy : public nsIDomainPolicy +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMAINPOLICY + DomainPolicy(); + virtual ~DomainPolicy(); + +private: + nsCOMPtr mBlacklist; + nsCOMPtr mSuperBlacklist; + nsCOMPtr mWhitelist; + nsCOMPtr mSuperWhitelist; +}; + +class DomainSet : public nsIDomainSet +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMAINSET + + DomainSet() {} + virtual ~DomainSet() {} + +protected: + nsTHashtable mHashTable; +}; + +} /* namespace hotness */ +} /* namespace mozilla */ + +#endif /* DomainPolicy_h__ */ diff --git a/caps/include/nsScriptSecurityManager.h b/caps/include/nsScriptSecurityManager.h index 11c61ea23b841..5adf5c844fa46 100644 --- a/caps/include/nsScriptSecurityManager.h +++ b/caps/include/nsScriptSecurityManager.h @@ -362,6 +362,8 @@ class nsScriptSecurityManager : public nsIScriptSecurityManager, AppAttributesEqual(nsIPrincipal* aFirst, nsIPrincipal* aSecond); + void DeactivateDomainPolicy(); + private: // GetScriptSecurityManager is the only call that can make one @@ -487,6 +489,10 @@ class nsScriptSecurityManager : public nsIScriptSecurityManager, bool mIsJavaScriptEnabled; bool mPolicyPrefsChanged; + // This machinery controls new-style domain policies. The old-style + // policy machinery will be removed soon. + nsCOMPtr mDomainPolicy; + static bool sStrictFileOriginPolicy; static nsIIOService *sIOService; diff --git a/caps/src/DomainPolicy.cpp b/caps/src/DomainPolicy.cpp new file mode 100644 index 0000000000000..be2c752230e48 --- /dev/null +++ b/caps/src/DomainPolicy.cpp @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=4 et sw=4 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DomainPolicy.h" +#include "nsScriptSecurityManager.h" + +namespace mozilla { +namespace hotness { + +NS_IMPL_ISUPPORTS1(DomainPolicy, nsIDomainPolicy) + +DomainPolicy::DomainPolicy() : mBlacklist(new DomainSet()) + , mSuperBlacklist(new DomainSet()) + , mWhitelist(new DomainSet()) + , mSuperWhitelist(new DomainSet()) +{} + +DomainPolicy::~DomainPolicy() +{ + // The SSM holds a strong ref to the DomainPolicy until Deactivate() is + // invoked, so we should never hit the destructor until that happens. + MOZ_ASSERT(!mBlacklist && !mSuperBlacklist && + !mWhitelist && !mSuperWhitelist); +} + + +NS_IMETHODIMP +DomainPolicy::GetBlacklist(nsIDomainSet** aSet) +{ + nsCOMPtr set = mBlacklist; + set.forget(aSet); + return NS_OK; +} + +NS_IMETHODIMP +DomainPolicy::GetSuperBlacklist(nsIDomainSet** aSet) +{ + nsCOMPtr set = mSuperBlacklist; + set.forget(aSet); + return NS_OK; +} + +NS_IMETHODIMP +DomainPolicy::GetWhitelist(nsIDomainSet** aSet) +{ + nsCOMPtr set = mWhitelist; + set.forget(aSet); + return NS_OK; +} + +NS_IMETHODIMP +DomainPolicy::GetSuperWhitelist(nsIDomainSet** aSet) +{ + nsCOMPtr set = mSuperWhitelist; + set.forget(aSet); + return NS_OK; +} + +NS_IMETHODIMP +DomainPolicy::Deactivate() +{ + // Clear the hashtables first to free up memory, since script might + // hold the doomed sets alive indefinitely. + mBlacklist->Clear(); + mSuperBlacklist->Clear(); + mWhitelist->Clear(); + mSuperWhitelist->Clear(); + + // Null them out. + mBlacklist = nullptr; + mSuperBlacklist = nullptr; + mWhitelist = nullptr; + mSuperWhitelist = nullptr; + + // Inform the SSM. + nsScriptSecurityManager::GetScriptSecurityManager()->DeactivateDomainPolicy(); + return NS_OK; +} + +static already_AddRefed +GetCanonicalClone(nsIURI* aURI) +{ + nsCOMPtr clone; + nsresult rv = aURI->Clone(getter_AddRefs(clone)); + NS_ENSURE_SUCCESS(rv, nullptr); + rv = clone->SetUserPass(EmptyCString()); + NS_ENSURE_SUCCESS(rv, nullptr); + rv = clone->SetPath(EmptyCString()); + NS_ENSURE_SUCCESS(rv, nullptr); + return clone.forget(); +} + +NS_IMPL_ISUPPORTS1(DomainSet, nsIDomainSet) + +NS_IMETHODIMP +DomainSet::Add(nsIURI* aDomain) +{ + nsCOMPtr clone = GetCanonicalClone(aDomain); + NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE); + mHashTable.PutEntry(clone); + return NS_OK; +} + +NS_IMETHODIMP +DomainSet::Remove(nsIURI* aDomain) +{ + nsCOMPtr clone = GetCanonicalClone(aDomain); + NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE); + mHashTable.RemoveEntry(clone); + return NS_OK; +} + +NS_IMETHODIMP +DomainSet::Clear() +{ + mHashTable.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +DomainSet::Contains(nsIURI* aDomain, bool* aContains) +{ + *aContains = false; + nsCOMPtr clone = GetCanonicalClone(aDomain); + NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE); + *aContains = mHashTable.Contains(clone); + return NS_OK; +} + +NS_IMETHODIMP +DomainSet::ContainsSuperDomain(nsIURI* aDomain, bool* aContains) +{ + *aContains = false; + nsCOMPtr clone = GetCanonicalClone(aDomain); + NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE); + nsAutoCString domain; + nsresult rv = clone->GetHost(domain); + NS_ENSURE_SUCCESS(rv, rv); + while (true) { + // Check the current domain. + if (mHashTable.Contains(clone)) { + *aContains = true; + return NS_OK; + } + + // Chop off everything before the first dot, or break if there are no + // dots left. + int32_t index = domain.Find("."); + if (index == kNotFound) + break; + domain.Assign(Substring(domain, index + 1)); + rv = clone->SetHost(domain); + NS_ENSURE_SUCCESS(rv, rv); + } + + // No match. + return NS_OK; + +} + +} /* namespace hotness */ +} /* namespace mozilla */ diff --git a/caps/src/moz.build b/caps/src/moz.build index 5b17bf6cae48d..24d22f73f477b 100644 --- a/caps/src/moz.build +++ b/caps/src/moz.build @@ -7,6 +7,7 @@ MODULE = 'caps' SOURCES += [ + 'DomainPolicy.cpp', 'nsJSPrincipals.cpp', 'nsNullPrincipal.cpp', 'nsNullPrincipalURI.cpp', diff --git a/caps/src/nsScriptSecurityManager.cpp b/caps/src/nsScriptSecurityManager.cpp index a10a87da1efd3..41f67cc6050ed 100644 --- a/caps/src/nsScriptSecurityManager.cpp +++ b/caps/src/nsScriptSecurityManager.cpp @@ -21,6 +21,7 @@ #include "nsSystemPrincipal.h" #include "nsPrincipal.h" #include "nsNullPrincipal.h" +#include "DomainPolicy.h" #include "nsXPIDLString.h" #include "nsCRT.h" #include "nsCRTGlue.h" @@ -1615,11 +1616,6 @@ nsScriptSecurityManager::ScriptAllowed(JSObject *aGlobal) return true; } - // If JS is disabled system-wide, we disallow. - if (!mIsJavaScriptEnabled) { - return false; - } - // Check for a per-site policy. static const char jsPrefGroupName[] = "javascript"; ClassInfoData nameData(nullptr, jsPrefGroupName); @@ -2156,6 +2152,9 @@ nsScriptSecurityManager::~nsScriptSecurityManager(void) if(mDefaultPolicy) mDefaultPolicy->Drop(); delete mCapabilities; + if (mDomainPolicy) + mDomainPolicy->Deactivate(); + MOZ_ASSERT(!mDomainPolicy); } void @@ -2581,3 +2580,74 @@ nsScriptSecurityManager::GetJarPrefix(uint32_t aAppId, mozilla::GetJarPrefix(aAppId, aInMozBrowser, aJarPrefix); return NS_OK; } + +NS_IMETHODIMP +nsScriptSecurityManager::GetDomainPolicyActive(bool *aRv) +{ + *aRv = !!mDomainPolicy; + return NS_OK; +} + +NS_IMETHODIMP +nsScriptSecurityManager::ActivateDomainPolicy(nsIDomainPolicy** aRv) +{ + // We only allow one domain policy at a time. The holder of the previous + // policy must explicitly deactivate it first. + if (mDomainPolicy) { + return NS_ERROR_SERVICE_NOT_AVAILABLE; + } + + mDomainPolicy = new mozilla::hotness::DomainPolicy(); + nsCOMPtr ptr = mDomainPolicy; + ptr.forget(aRv); + return NS_OK; +} + +// Intentionally non-scriptable. Script must have a reference to the +// nsIDomainPolicy to deactivate it. +void +nsScriptSecurityManager::DeactivateDomainPolicy() +{ + mDomainPolicy = nullptr; +} + +NS_IMETHODIMP +nsScriptSecurityManager::PolicyAllowsScript(nsIURI* aURI, bool *aRv) +{ + nsresult rv; + + // Compute our rule. If we don't have any domain policy set up that might + // provide exceptions to this rule, we're done. + *aRv = mIsJavaScriptEnabled; + if (!mDomainPolicy) { + return NS_OK; + } + + // We have a domain policy. Grab the appropriate set of exceptions to the + // rule (either the blacklist or the whitelist, depending on whether script + // is enabled or disabled by default). + nsCOMPtr exceptions; + nsCOMPtr superExceptions; + if (*aRv) { + mDomainPolicy->GetBlacklist(getter_AddRefs(exceptions)); + mDomainPolicy->GetSuperBlacklist(getter_AddRefs(superExceptions)); + } else { + mDomainPolicy->GetWhitelist(getter_AddRefs(exceptions)); + mDomainPolicy->GetSuperWhitelist(getter_AddRefs(superExceptions)); + } + + bool contains; + rv = exceptions->Contains(aURI, &contains); + NS_ENSURE_SUCCESS(rv, rv); + if (contains) { + *aRv = !*aRv; + return NS_OK; + } + rv = superExceptions->ContainsSuperDomain(aURI, &contains); + NS_ENSURE_SUCCESS(rv, rv); + if (contains) { + *aRv = !*aRv; + } + + return NS_OK; +} diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp index 8b902afffd1e8..b25b7a73ae471 100644 --- a/js/xpconnect/src/XPCJSRuntime.cpp +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -10,6 +10,7 @@ #include "xpcprivate.h" #include "xpcpublic.h" +#include "XPCWrapper.h" #include "XPCJSMemoryReporter.h" #include "WrapperFactory.h" #include "dom_quickstubs.h" @@ -448,15 +449,34 @@ PrincipalImmuneToScriptPolicy(nsIPrincipal* aPrincipal) Scriptability::Scriptability(JSCompartment *c) : mScriptBlocks(0) , mDocShellAllowsScript(true) + , mScriptBlockedByPolicy(false) { nsIPrincipal *prin = nsJSPrincipals::get(JS_GetCompartmentPrincipals(c)); mImmuneToScriptPolicy = PrincipalImmuneToScriptPolicy(prin); + + // If we're not immune, we should have a real principal with a codebase URI. + // Check the URI against the new-style domain policy. + if (!mImmuneToScriptPolicy) { + nsIScriptSecurityManager* ssm = XPCWrapper::GetSecurityManager(); + nsCOMPtr codebase; + nsresult rv = prin->GetURI(getter_AddRefs(codebase)); + bool policyAllows; + if (NS_SUCCEEDED(rv) && codebase && + NS_SUCCEEDED(ssm->PolicyAllowsScript(codebase, &policyAllows))) + { + mScriptBlockedByPolicy = !policyAllows; + } else { + // Something went wrong - be safe and block script. + mScriptBlockedByPolicy = true; + } + } } bool Scriptability::Allowed() { - return mDocShellAllowsScript && mScriptBlocks == 0; + return mDocShellAllowsScript && !mScriptBlockedByPolicy && + mScriptBlocks == 0; } bool diff --git a/js/xpconnect/src/xpcpublic.h b/js/xpconnect/src/xpcpublic.h index d1065ff7cd662..f631994d7c73c 100644 --- a/js/xpconnect/src/xpcpublic.h +++ b/js/xpconnect/src/xpcpublic.h @@ -61,6 +61,10 @@ class Scriptability { // Whether this scope is immune to user-defined or addon-defined script // policy. bool mImmuneToScriptPolicy; + + // Whether the new-style domain policy when this compartment was created + // forbids script execution. + bool mScriptBlockedByPolicy; }; JSObject *