diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index d5baaf060ee2..b9af2c205a04 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -354,6 +354,10 @@ pref("browser.dom.window.dump.enabled", false); // installable apps or wifi support. pref("security.fileuri.strict_origin_policy", false); +// Default Content Security Policy to apply to privileged and certified apps +pref("security.apps.privileged.CSP.default", "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'"); +pref("security.apps.certified.CSP.default", "default-src *; script-src 'self'; object-src 'none'; style-src 'self'"); + // Temporarily force-enable GL compositing. This is default-disabled // deep within the bowels of the widgetry system. Remove me when GL // compositing isn't default disabled in widget/android. diff --git a/build/automation.py.in b/build/automation.py.in index ee9ea247baba..1383dba7d341 100644 --- a/build/automation.py.in +++ b/build/automation.py.in @@ -29,6 +29,12 @@ _DEFAULT_HTTP_PORT = 8888 _DEFAULT_SSL_PORT = 4443 _DEFAULT_WEBSOCKET_PORT = 9988 +# from nsIPrincipal.idl +_APP_STATUS_NOT_INSTALLED = 0 +_APP_STATUS_INSTALLED = 1 +_APP_STATUS_PRIVILEGED = 2 +_APP_STATUS_CERTIFIED = 3 + #expand _DIST_BIN = __XPC_BIN_PATH__ #expand _IS_WIN32 = len("__WIN32__") != 0 #expand _IS_MAC = __IS_MAC__ != 0 @@ -297,7 +303,8 @@ class Automation(object): "receipt": null, "installTime": 132333986000, "manifestURL": "$manifestURL", -"localId": $localId +"localId": $localId, +"appStatus": $appStatus }""") manifestTemplate = Template("""{ @@ -517,31 +524,50 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t 'name': 'http_example_org', 'origin': 'http://example.org', 'manifestURL': 'http://example.org/manifest.webapp', - 'description': 'http://example.org App' + 'description': 'http://example.org App', + 'appStatus': _APP_STATUS_INSTALLED }, { 'name': 'https_example_com', 'origin': 'https://example.com', 'manifestURL': 'https://example.com/manifest.webapp', - 'description': 'https://example.com App' + 'description': 'https://example.com App', + 'appStatus': _APP_STATUS_INSTALLED }, { 'name': 'http_test1_example_org', 'origin': 'http://test1.example.org', 'manifestURL': 'http://test1.example.org/manifest.webapp', - 'description': 'http://test1.example.org App' + 'description': 'http://test1.example.org App', + 'appStatus': _APP_STATUS_INSTALLED }, { 'name': 'http_test1_example_org_8000', 'origin': 'http://test1.example.org:8000', 'manifestURL': 'http://test1.example.org:8000/manifest.webapp', - 'description': 'http://test1.example.org:8000 App' + 'description': 'http://test1.example.org:8000 App', + 'appStatus': _APP_STATUS_INSTALLED }, { 'name': 'http_sub1_test1_example_org', 'origin': 'http://sub1.test1.example.org', 'manifestURL': 'http://sub1.test1.example.org/manifest.webapp', - 'description': 'http://sub1.test1.example.org App' + 'description': 'http://sub1.test1.example.org App', + 'appStatus': _APP_STATUS_INSTALLED + }, + { + 'name': 'https_example_com_privileged', + 'origin': 'https://example.com', + 'manifestURL': 'https://example.com/manifest_priv.webapp', + 'description': 'https://example.com Privileged App', + 'appStatus': _APP_STATUS_PRIVILEGED + }, + { + 'name': 'https_example_com_certified', + 'origin': 'https://example.com', + 'manifestURL': 'https://example.com/manifest_cert.webapp', + 'description': 'https://example.com Certified App', + 'appStatus': _APP_STATUS_CERTIFIED }, ]; self.setupTestApps(profileDir, apps) diff --git a/caps/idl/nsIPrincipal.idl b/caps/idl/nsIPrincipal.idl index 5438fe518065..75ea8929010e 100644 --- a/caps/idl/nsIPrincipal.idl +++ b/caps/idl/nsIPrincipal.idl @@ -21,7 +21,7 @@ interface nsIContentSecurityPolicy; [ptr] native JSPrincipals(JSPrincipals); [ptr] native PrincipalArray(nsTArray >); -[scriptable, uuid(825ffce8-962d-11e1-aef3-8f2b6188709b)] +[scriptable, uuid(115d1081-373e-4837-8d12-d0f4874f3467)] interface nsIPrincipal : nsISerializable { /** @@ -273,6 +273,13 @@ interface nsIPrincipal : nsISerializable * Returns true iif the principal is inside a browser element. */ readonly attribute boolean isInBrowserElement; + + /** + * Returns true if this principal has an unknown appId. This shouldn't + * generally be used. We only expose it due to not providing the correct + * appId everywhere where we construct principals. + */ + readonly attribute boolean unknownAppId; }; /** diff --git a/caps/include/nsPrincipal.h b/caps/include/nsPrincipal.h index 3b5d138bfea6..b2320c6df2c5 100644 --- a/caps/include/nsPrincipal.h +++ b/caps/include/nsPrincipal.h @@ -131,6 +131,7 @@ public: NS_IMETHOD GetAppStatus(uint16_t* aAppStatus); NS_IMETHOD GetAppId(uint32_t* aAppStatus); NS_IMETHOD GetIsInBrowserElement(bool* aIsInBrowserElement); + NS_IMETHOD GetUnknownAppId(bool* aUnknownAppId); #ifdef DEBUG virtual void dumpImpl(); #endif @@ -227,6 +228,7 @@ public: NS_IMETHOD GetAppStatus(uint16_t* aAppStatus); NS_IMETHOD GetAppId(uint32_t* aAppStatus); NS_IMETHOD GetIsInBrowserElement(bool* aIsInBrowserElement); + NS_IMETHOD GetUnknownAppId(bool* aUnknownAppId); #ifdef DEBUG virtual void dumpImpl(); #endif diff --git a/caps/src/nsNullPrincipal.cpp b/caps/src/nsNullPrincipal.cpp index 4a0a03edacfd..969e1d4ea08b 100644 --- a/caps/src/nsNullPrincipal.cpp +++ b/caps/src/nsNullPrincipal.cpp @@ -360,6 +360,13 @@ nsNullPrincipal::GetIsInBrowserElement(bool* aIsInBrowserElement) return NS_OK; } +NS_IMETHODIMP +nsNullPrincipal::GetUnknownAppId(bool* aUnknownAppId) +{ + *aUnknownAppId = false; + return NS_OK; +} + /** * nsISerializable implementation */ diff --git a/caps/src/nsPrincipal.cpp b/caps/src/nsPrincipal.cpp index f6919ccc566e..65d174b56d66 100644 --- a/caps/src/nsPrincipal.cpp +++ b/caps/src/nsPrincipal.cpp @@ -1111,6 +1111,13 @@ nsPrincipal::GetIsInBrowserElement(bool* aIsInBrowserElement) return NS_OK; } +NS_IMETHODIMP +nsPrincipal::GetUnknownAppId(bool* aUnknownAppId) +{ + *aUnknownAppId = mAppId == nsIScriptSecurityManager::UNKNOWN_APP_ID; + return NS_OK; +} + NS_IMETHODIMP nsPrincipal::Read(nsIObjectInputStream* aStream) { @@ -1535,6 +1542,13 @@ nsExpandedPrincipal::GetIsInBrowserElement(bool* aIsInBrowserElement) return NS_ERROR_NOT_AVAILABLE; } +NS_IMETHODIMP +nsExpandedPrincipal::GetUnknownAppId(bool* aUnknownAppId) +{ + *aUnknownAppId = false; + return NS_OK; +} + void nsExpandedPrincipal::GetScriptLocation(nsACString& aStr) { diff --git a/caps/src/nsSystemPrincipal.cpp b/caps/src/nsSystemPrincipal.cpp index 5a58e080fcd3..4730193304a5 100644 --- a/caps/src/nsSystemPrincipal.cpp +++ b/caps/src/nsSystemPrincipal.cpp @@ -265,6 +265,13 @@ nsSystemPrincipal::GetIsInBrowserElement(bool* aIsInBrowserElement) return NS_OK; } +NS_IMETHODIMP +nsSystemPrincipal::GetUnknownAppId(bool* aUnknownAppId) +{ + *aUnknownAppId = false; + return NS_OK; +} + ////////////////////////////////////////// // Methods implementing nsISerializable // ////////////////////////////////////////// diff --git a/content/base/src/nsDocument.cpp b/content/base/src/nsDocument.cpp index 6017693577c6..793ed66ad3ce 100644 --- a/content/base/src/nsDocument.cpp +++ b/content/base/src/nsDocument.cpp @@ -2394,9 +2394,6 @@ nsDocument::StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, mMayStartLayout = false; mHaveInputEncoding = true; - nsCOMPtr csp; - nsresult rv = InitCSP(aChannel, getter_AddRefs(csp)); - NS_ENSURE_SUCCESS(rv, rv); if (aReset) { Reset(aChannel, aLoadGroup); @@ -2426,30 +2423,27 @@ nsDocument::StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, NS_ENSURE_SUCCESS(rv, rv); } - if (csp) { - // Copy into principal - nsIPrincipal* principal = GetPrincipal(); - principal->SetCsp(csp); -#ifdef PR_LOGGING - PR_LOG(gCspPRLog, PR_LOG_DEBUG, - ("Inserted CSP into principal %p", principal)); -#endif - } + nsresult rv = InitCSP(aChannel); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult -nsDocument::InitCSP(nsIChannel* aChannel, nsIContentSecurityPolicy **aCSP) +nsDocument::InitCSP(nsIChannel* aChannel) { - *aCSP = nullptr; - if (CSPService::sCSPEnabled) { - nsAutoCString tCspHeaderValue, tCspROHeaderValue; - nsCOMPtr httpChannel = do_QueryInterface(aChannel); - if (!httpChannel) { - // no CSP for non http channels - return NS_OK; - } + nsCOMPtr csp; + if (!CSPService::sCSPEnabled) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP is disabled, skipping CSP init for document %p", this)); +#endif + return NS_OK; + } + + nsAutoCString tCspHeaderValue, tCspROHeaderValue; + nsCOMPtr httpChannel = do_QueryInterface(aChannel); + if (httpChannel) { httpChannel->GetResponseHeader( NS_LITERAL_CSTRING("x-content-security-policy"), tCspHeaderValue); @@ -2457,45 +2451,115 @@ nsDocument::InitCSP(nsIChannel* aChannel, nsIContentSecurityPolicy **aCSP) httpChannel->GetResponseHeader( NS_LITERAL_CSTRING("x-content-security-policy-report-only"), tCspROHeaderValue); - NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); - NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); - - if (cspHeaderValue.IsEmpty() && cspROHeaderValue.IsEmpty()) { - // no CSP header present, stop processing - return NS_OK; - } + } + NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); + NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); + // ----- Figure out if we need to apply an app default CSP + bool applyAppDefaultCSP = false; + nsIPrincipal* principal = NodePrincipal(); + PRUint16 appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED; + bool unknownAppId; + if (NS_SUCCEEDED(principal->GetUnknownAppId(&unknownAppId)) && + !unknownAppId && + NS_SUCCEEDED(principal->GetAppStatus(&appStatus))) { + applyAppDefaultCSP = ( appStatus == nsIPrincipal::APP_STATUS_PRIVILEGED || + appStatus == nsIPrincipal::APP_STATUS_CERTIFIED); + } #ifdef PR_LOGGING - PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP header specified for document %p", this)); + else + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Failed to get app status from principal")); #endif - nsresult rv; - nsCOMPtr csp; - csp = do_CreateInstance("@mozilla.org/contentsecuritypolicy;1", &rv); - - if (NS_FAILED(rv)) { + // If there's no CSP to apply go ahead and return early + if (!applyAppDefaultCSP && + cspHeaderValue.IsEmpty() && + cspROHeaderValue.IsEmpty()) { #ifdef PR_LOGGING - PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Failed to create CSP object: %x", rv)); -#endif - return rv; - } - - // Store the request context for violation reports - csp->ScanRequestData(httpChannel); - - // Start parsing the policy nsCOMPtr chanURI; aChannel->GetURI(getter_AddRefs(chanURI)); + nsAutoCString aspec; + chanURI->GetAsciiSpec(aspec); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("no CSP for document, %s, %s", + aspec.get(), + applyAppDefaultCSP ? "is app" : "not an app")); +#endif + return NS_OK; + } #ifdef PR_LOGGING - PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP Loaded")); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Document is an app or CSP header specified %p", this)); #endif - // ReportOnly mode is enabled *only* if there are no regular-strength CSP - // headers present. If there are, then we ignore the ReportOnly mode and - // toss a warning into the error console, proceeding with enforcing the - // regular-strength CSP. - if (cspHeaderValue.IsEmpty()) { + nsresult rv; + csp = do_CreateInstance("@mozilla.org/contentsecuritypolicy;1", &rv); + + if (NS_FAILED(rv)) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Failed to create CSP object: %x", rv)); +#endif + return rv; + } + + // used as a "self" identifier for the CSP. + nsCOMPtr chanURI; + aChannel->GetURI(getter_AddRefs(chanURI)); + + // Store the request context for violation reports + csp->ScanRequestData(httpChannel); + + // ----- process the app default policy, if necessary + if (applyAppDefaultCSP) { + nsAdoptingString appCSP; + if (appStatus == nsIPrincipal::APP_STATUS_PRIVILEGED) { + appCSP = Preferences::GetString("security.apps.privileged.CSP.default"); + NS_ASSERTION(appCSP, "App, but no default CSP in security.apps.privileged.CSP.default"); + } else if (appStatus == nsIPrincipal::APP_STATUS_CERTIFIED) { + appCSP = Preferences::GetString("security.apps.certified.CSP.default"); + NS_ASSERTION(appCSP, "App, but no default CSP in security.apps.certified.CSP.default"); + } + + if (appCSP) + csp->RefinePolicy(appCSP, chanURI); + } + + // ----- if there's a full-strength CSP header, apply it. + if (!cspHeaderValue.IsEmpty()) { + // Need to tokenize the header value since multiple headers could be + // concatenated into one comma-separated list of policies. + // See RFC2616 section 4.2 (last paragraph) + nsCharSeparatedTokenizer tokenizer(cspHeaderValue, ','); + while (tokenizer.hasMoreTokens()) { + const nsSubstring& policy = tokenizer.nextToken(); + csp->RefinePolicy(policy, chanURI); +#ifdef PR_LOGGING + { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP refined with policy: \"%s\"", + NS_ConvertUTF16toUTF8(policy).get())); + } +#endif + } + } + + // ----- if there's a report-only CSP header, apply it + if (!cspROHeaderValue.IsEmpty()) { + // post a warning and skip report-only CSP when both read only and regular + // CSP policies are present since CSP only allows one policy and it can't + // be partially report-only. + if (applyAppDefaultCSP || !cspHeaderValue.IsEmpty()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "CSP", this, + nsContentUtils::eDOM_PROPERTIES, + "ReportOnlyCSPIgnored"); +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("Skipped report-only CSP init for document %p because another, enforced policy is set", this)); +#endif + } else { + // we can apply the report-only policy because there's no other CSP + // applied. csp->SetReportOnlyMode(true); // Need to tokenize the header value since multiple headers could be @@ -2508,58 +2572,43 @@ nsDocument::InitCSP(nsIChannel* aChannel, nsIContentSecurityPolicy **aCSP) #ifdef PR_LOGGING { PR_LOG(gCspPRLog, PR_LOG_DEBUG, - ("CSP (report only) refined with policy: \"%s\"", + ("CSP (report-only) refined with policy: \"%s\"", NS_ConvertUTF16toUTF8(policy).get())); } -#endif - } - } else { - //XXX(sstamm): maybe we should post a warning when both read only and regular - // CSP headers are present. - - // Need to tokenize the header value since multiple headers could be - // concatenated into one comma-separated list of policies. - // See RFC2616 section 4.2 (last paragraph) - nsCharSeparatedTokenizer tokenizer(cspHeaderValue, ','); - while (tokenizer.hasMoreTokens()) { - const nsSubstring& policy = tokenizer.nextToken(); - csp->RefinePolicy(policy, chanURI); -#ifdef PR_LOGGING - { - PR_LOG(gCspPRLog, PR_LOG_DEBUG, - ("CSP refined with policy: \"%s\"", - NS_ConvertUTF16toUTF8(policy).get())); - } #endif } } - - // Check for frame-ancestor violation - nsCOMPtr docShell = do_QueryReferent(mDocumentContainer); - if (docShell) { - bool safeAncestry = false; - - // PermitsAncestry sends violation reports when necessary - rv = csp->PermitsAncestry(docShell, &safeAncestry); - NS_ENSURE_SUCCESS(rv, rv); - - if (!safeAncestry) { -#ifdef PR_LOGGING - PR_LOG(gCspPRLog, PR_LOG_DEBUG, - ("CSP doesn't like frame's ancestry, not loading.")); -#endif - // stop! ERROR page! - aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION); - } - } - csp.forget(aCSP); } + + // ----- Enforce frame-ancestor policy on any applied policies + nsCOMPtr docShell = do_QueryReferent(mDocumentContainer); + if (docShell) { + bool safeAncestry = false; + + // PermitsAncestry sends violation reports when necessary + rv = csp->PermitsAncestry(docShell, &safeAncestry); + NS_ENSURE_SUCCESS(rv, rv); + + if (!safeAncestry) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP doesn't like frame's ancestry, not loading.")); +#endif + // stop! ERROR page! + aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION); + } + } + + if (csp) { + // Copy into principal + nsIPrincipal* principal = GetPrincipal(); + principal->SetCsp(csp); #ifdef PR_LOGGING - else { //CSP was not enabled! PR_LOG(gCspPRLog, PR_LOG_DEBUG, - ("CSP is disabled, skipping CSP init for document %p", this)); - } + ("Inserted CSP into principal %p", principal)); #endif + } + return NS_OK; } diff --git a/content/base/src/nsDocument.h b/content/base/src/nsDocument.h index 13d6cd57a203..972c4c39e589 100644 --- a/content/base/src/nsDocument.h +++ b/content/base/src/nsDocument.h @@ -1274,7 +1274,7 @@ private: void DoUnblockOnload(); nsresult CheckFrameOptions(); - nsresult InitCSP(nsIChannel* aChannel, nsIContentSecurityPolicy **aCSP); + nsresult InitCSP(nsIChannel* aChannel); // Sets aElement to be the pending pointer lock element. Once this document's // node principal's URI is granted the "fullscreen" permission, the pointer diff --git a/content/base/test/Makefile.in b/content/base/test/Makefile.in index 5219af96b8f0..c930ac84878e 100644 --- a/content/base/test/Makefile.in +++ b/content/base/test/Makefile.in @@ -367,6 +367,8 @@ MOCHITEST_FILES_B = \ file_CSP_evalscript_main.html \ file_CSP_evalscript_main.html^headers^ \ file_CSP_evalscript_main.js \ + file_csp_bug768029.html \ + file_csp_bug768029.sjs \ test_bug540854.html \ bug540854.sjs \ test_bug548463.html \ diff --git a/content/base/test/chrome/Makefile.in b/content/base/test/chrome/Makefile.in index a74f9addcf31..94cef2f52f36 100644 --- a/content/base/test/chrome/Makefile.in +++ b/content/base/test/chrome/Makefile.in @@ -46,6 +46,7 @@ MOCHITEST_CHROME_FILES = \ test_bug682305.html \ test_bug780199.xul \ test_bug780529.xul \ + test_csp_bug768029.html \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/content/base/test/chrome/test_csp_bug768029.html b/content/base/test/chrome/test_csp_bug768029.html new file mode 100644 index 000000000000..350d45e2b055 --- /dev/null +++ b/content/base/test/chrome/test_csp_bug768029.html @@ -0,0 +1,221 @@ + + + + + + Test for CSP on trusted/certified apps -- bug 768029 + + + + +Mozilla Bug 768029 +

+
+ +
+
+
+
+ + diff --git a/content/base/test/file_csp_bug768029.html b/content/base/test/file_csp_bug768029.html new file mode 100644 index 000000000000..7d86e0cf9554 --- /dev/null +++ b/content/base/test/file_csp_bug768029.html @@ -0,0 +1,25 @@ + + + + + + This is an app for testing + + + + + + + + + + + + Test for CSP applied to (simulated) app. + + + diff --git a/content/base/test/file_csp_bug768029.sjs b/content/base/test/file_csp_bug768029.sjs new file mode 100644 index 000000000000..9ae353055ef9 --- /dev/null +++ b/content/base/test/file_csp_bug768029.sjs @@ -0,0 +1,29 @@ +function handleRequest(request, response) { + + var query = {}; + + request.queryString.split('&').forEach(function(val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + response.setHeader("Cache-Control", "no-cache", false); + + if ("type" in query) { + switch (query.type) { + case "script": + response.setHeader("Content-Type", "application/javascript"); + response.write("\n\ndocument.write('
script loaded\\n
');\n\n"); + return; + case "style": + response.setHeader("Content-Type", "text/css"); + response.write("\n\n.cspfoo { color:red; }\n\n"); + return; + case "img": + response.setHeader("Content-Type", "image/png"); + return; + } + } + + response.setHeader("Content-Type", "text/plain"); + response.write("ohnoes!"); +} diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties index e969bc580a9e..b2c1245bda5d 100644 --- a/dom/locales/en-US/chrome/dom/dom.properties +++ b/dom/locales/en-US/chrome/dom/dom.properties @@ -105,6 +105,7 @@ FocusedWindowedPluginWhileFullScreen=Exited full-screen because windowed plugin HTMLMultipartXHRWarning=HTML parsing in XMLHttpRequest is not supported for multipart responses. HTMLSyncXHRWarning=HTML parsing in XMLHttpRequest is not supported in the synchronous mode. InvalidRedirectChannelWarning=Unable to redirect to %S because the channel doesn't implement nsIWritablePropertyBag2. +ReportOnlyCSPIgnored=Report-only CSP policy will be ignored because there are other non-report-only CSP policies applied. ResponseTypeSyncXHRWarning=Use of XMLHttpRequest's responseType attribute is no longer supported in the synchronous mode in window context. WithCredentialsSyncXHRWarning=Use of XMLHttpRequest's withCredentials attribute is no longer supported in the synchronous mode in window context. TimeoutSyncXHRWarning=Use of XMLHttpRequest's timeout attribute is not supported in the synchronous mode in window context.