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 "AddonContentPolicy.h"
8
9 #include "mozilla/dom/nsCSPContext.h"
10 #include "nsCOMPtr.h"
11 #include "nsContentPolicyUtils.h"
12 #include "nsContentTypeParser.h"
13 #include "nsContentUtils.h"
14 #include "nsIConsoleService.h"
15 #include "nsIContentSecurityPolicy.h"
16 #include "nsIContent.h"
17 #include "mozilla/dom/Document.h"
18 #include "nsIEffectiveTLDService.h"
19 #include "nsIScriptError.h"
20 #include "nsIStringBundle.h"
21 #include "nsIUUIDGenerator.h"
22 #include "nsIURI.h"
23 #include "nsNetCID.h"
24 #include "nsNetUtil.h"
25
26 using namespace mozilla;
27
28 /* Enforces content policies for WebExtension scopes. Currently:
29 *
30 * - Prevents loading scripts with a non-default JavaScript version.
31 * - Checks custom content security policies for sufficiently stringent
32 * script-src and object-src directives.
33 */
34
35 #define VERSIONED_JS_BLOCKED_MESSAGE \
36 u"Versioned JavaScript is a non-standard, deprecated extension, and is " \
37 u"not supported in WebExtension code. For alternatives, please see: " \
38 u"https://developer.mozilla.org/Add-ons/WebExtensions/Tips"
39
40 AddonContentPolicy::AddonContentPolicy() = default;
41
42 AddonContentPolicy::~AddonContentPolicy() = default;
43
NS_IMPL_ISUPPORTS(AddonContentPolicy,nsIContentPolicy,nsIAddonContentPolicy)44 NS_IMPL_ISUPPORTS(AddonContentPolicy, nsIContentPolicy, nsIAddonContentPolicy)
45
46 static nsresult GetWindowIDFromContext(nsISupports* aContext,
47 uint64_t* aResult) {
48 NS_ENSURE_TRUE(aContext, NS_ERROR_FAILURE);
49
50 nsCOMPtr<nsIContent> content = do_QueryInterface(aContext);
51 NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
52
53 nsCOMPtr<nsPIDOMWindowInner> window = content->OwnerDoc()->GetInnerWindow();
54 NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
55
56 *aResult = window->WindowID();
57 return NS_OK;
58 }
59
LogMessage(const nsAString & aMessage,const nsAString & aSourceName,const nsAString & aSourceSample,nsISupports * aContext)60 static nsresult LogMessage(const nsAString& aMessage,
61 const nsAString& aSourceName,
62 const nsAString& aSourceSample,
63 nsISupports* aContext) {
64 nsCOMPtr<nsIScriptError> error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
65 NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY);
66
67 uint64_t windowID = 0;
68 GetWindowIDFromContext(aContext, &windowID);
69
70 nsresult rv = error->InitWithSanitizedSource(
71 aMessage, aSourceName, aSourceSample, 0, 0, nsIScriptError::errorFlag,
72 "JavaScript", windowID);
73 NS_ENSURE_SUCCESS(rv, rv);
74
75 nsCOMPtr<nsIConsoleService> console =
76 do_GetService(NS_CONSOLESERVICE_CONTRACTID);
77 NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY);
78
79 console->LogMessage(error);
80 return NS_OK;
81 }
82
83 // Content policy enforcement:
84
85 NS_IMETHODIMP
ShouldLoad(nsIURI * aContentLocation,nsILoadInfo * aLoadInfo,const nsACString & aMimeTypeGuess,int16_t * aShouldLoad)86 AddonContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
87 const nsACString& aMimeTypeGuess,
88 int16_t* aShouldLoad) {
89 if (!aContentLocation || !aLoadInfo) {
90 NS_SetRequestBlockingReason(
91 aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
92 *aShouldLoad = REJECT_REQUEST;
93 return NS_ERROR_FAILURE;
94 }
95
96 uint32_t contentType = aLoadInfo->GetExternalContentPolicyType();
97
98 MOZ_ASSERT(contentType == nsContentUtils::InternalContentPolicyTypeToExternal(
99 contentType),
100 "We should only see external content policy types here.");
101
102 *aShouldLoad = nsIContentPolicy::ACCEPT;
103 nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal();
104 if (!loadingPrincipal) {
105 return NS_OK;
106 }
107
108 // Only apply this policy to requests from documents loaded from
109 // moz-extension URLs, or to resources being loaded from moz-extension URLs.
110 if (!(aContentLocation->SchemeIs("moz-extension") ||
111 loadingPrincipal->SchemeIs("moz-extension"))) {
112 return NS_OK;
113 }
114
115 if (contentType == nsIContentPolicy::TYPE_SCRIPT) {
116 NS_ConvertUTF8toUTF16 typeString(aMimeTypeGuess);
117 nsContentTypeParser mimeParser(typeString);
118
119 // Reject attempts to load JavaScript scripts with a non-default version.
120 nsAutoString mimeType, version;
121 if (NS_SUCCEEDED(mimeParser.GetType(mimeType)) &&
122 nsContentUtils::IsJavascriptMIMEType(mimeType) &&
123 NS_SUCCEEDED(mimeParser.GetParameter("version", version))) {
124 NS_SetRequestBlockingReason(
125 aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
126 *aShouldLoad = nsIContentPolicy::REJECT_REQUEST;
127
128 nsCString sourceName;
129 loadingPrincipal->GetExposableSpec(sourceName);
130 NS_ConvertUTF8toUTF16 nameString(sourceName);
131
132 nsCOMPtr<nsISupports> context = aLoadInfo->GetLoadingContext();
133 LogMessage(NS_LITERAL_STRING(VERSIONED_JS_BLOCKED_MESSAGE), nameString,
134 typeString, context);
135 return NS_OK;
136 }
137 }
138
139 return NS_OK;
140 }
141
142 NS_IMETHODIMP
ShouldProcess(nsIURI * aContentLocation,nsILoadInfo * aLoadInfo,const nsACString & aMimeTypeGuess,int16_t * aShouldProcess)143 AddonContentPolicy::ShouldProcess(nsIURI* aContentLocation,
144 nsILoadInfo* aLoadInfo,
145 const nsACString& aMimeTypeGuess,
146 int16_t* aShouldProcess) {
147 #ifdef DEBUG
148 uint32_t contentType = aLoadInfo->GetExternalContentPolicyType();
149 MOZ_ASSERT(contentType == nsContentUtils::InternalContentPolicyTypeToExternal(
150 contentType),
151 "We should only see external content policy types here.");
152 #endif
153
154 *aShouldProcess = nsIContentPolicy::ACCEPT;
155 return NS_OK;
156 }
157
158 // CSP Validation:
159
160 static const char* allowedSchemes[] = {"blob", "filesystem", nullptr};
161
162 static const char* allowedHostSchemes[] = {"https", "moz-extension", nullptr};
163
164 /**
165 * Validates a CSP directive to ensure that it is sufficiently stringent.
166 * In particular, ensures that:
167 *
168 * - No remote sources are allowed other than from https: schemes
169 *
170 * - No remote sources specify host wildcards for generic domains
171 * (*.blogspot.com, *.com, *)
172 *
173 * - All remote sources and local extension sources specify a host
174 *
175 * - No scheme sources are allowed other than blob:, filesystem:,
176 * moz-extension:, and https:
177 *
178 * - No keyword sources are allowed other than 'none', 'self', 'unsafe-eval',
179 * and hash sources.
180 */
181 class CSPValidator final : public nsCSPSrcVisitor {
182 public:
CSPValidator(nsAString & aURL,CSPDirective aDirective,bool aDirectiveRequired=true)183 CSPValidator(nsAString& aURL, CSPDirective aDirective,
184 bool aDirectiveRequired = true)
185 : mURL(aURL),
186 mDirective(CSP_CSPDirectiveToString(aDirective)),
187 mFoundSelf(false) {
188 // Start with the default error message for a missing directive, since no
189 // visitors will be called if the directive isn't present.
190 mError.SetIsVoid(true);
191 if (aDirectiveRequired) {
192 FormatError("csp.error.missing-directive");
193 }
194 }
195
196 // Visitors
197
visitSchemeSrc(const nsCSPSchemeSrc & src)198 bool visitSchemeSrc(const nsCSPSchemeSrc& src) override {
199 nsAutoString scheme;
200 src.getScheme(scheme);
201
202 if (SchemeInList(scheme, allowedHostSchemes)) {
203 FormatError("csp.error.missing-host", scheme);
204 return false;
205 }
206 if (!SchemeInList(scheme, allowedSchemes)) {
207 FormatError("csp.error.illegal-protocol", scheme);
208 return false;
209 }
210 return true;
211 };
212
visitHostSrc(const nsCSPHostSrc & src)213 bool visitHostSrc(const nsCSPHostSrc& src) override {
214 nsAutoString scheme, host;
215
216 src.getScheme(scheme);
217 src.getHost(host);
218
219 if (scheme.LowerCaseEqualsLiteral("https")) {
220 if (!HostIsAllowed(host)) {
221 FormatError("csp.error.illegal-host-wildcard", scheme);
222 return false;
223 }
224 } else if (scheme.LowerCaseEqualsLiteral("moz-extension")) {
225 // The CSP parser silently converts 'self' keywords to the origin
226 // URL, so we need to reconstruct the URL to see if it was present.
227 if (!mFoundSelf) {
228 nsAutoString url(u"moz-extension://");
229 url.Append(host);
230
231 mFoundSelf = url.Equals(mURL);
232 }
233
234 if (host.IsEmpty() || host.EqualsLiteral("*")) {
235 FormatError("csp.error.missing-host", scheme);
236 return false;
237 }
238 } else if (!SchemeInList(scheme, allowedSchemes)) {
239 FormatError("csp.error.illegal-protocol", scheme);
240 return false;
241 }
242
243 return true;
244 };
245
visitKeywordSrc(const nsCSPKeywordSrc & src)246 bool visitKeywordSrc(const nsCSPKeywordSrc& src) override {
247 switch (src.getKeyword()) {
248 case CSP_NONE:
249 case CSP_SELF:
250 case CSP_UNSAFE_EVAL:
251 return true;
252
253 default:
254 FormatError(
255 "csp.error.illegal-keyword",
256 nsDependentString(CSP_EnumToUTF16Keyword(src.getKeyword())));
257 return false;
258 }
259 };
260
visitNonceSrc(const nsCSPNonceSrc & src)261 bool visitNonceSrc(const nsCSPNonceSrc& src) override {
262 FormatError("csp.error.illegal-keyword", NS_LITERAL_STRING("'nonce-*'"));
263 return false;
264 };
265
visitHashSrc(const nsCSPHashSrc & src)266 bool visitHashSrc(const nsCSPHashSrc& src) override { return true; };
267
268 // Accessors
269
GetError()270 inline nsAString& GetError() { return mError; };
271
FoundSelf()272 inline bool FoundSelf() { return mFoundSelf; };
273
274 // Formatters
275
276 template <typename... T>
FormatError(const char * aName,const T...aParams)277 inline void FormatError(const char* aName, const T... aParams) {
278 AutoTArray<nsString, sizeof...(aParams) + 1> params = {mDirective,
279 aParams...};
280 FormatErrorParams(aName, params);
281 };
282
283 private:
284 // Validators
285
HostIsAllowed(nsAString & host)286 bool HostIsAllowed(nsAString& host) {
287 if (host.First() == '*') {
288 if (host.EqualsLiteral("*") || host[1] != '.') {
289 return false;
290 }
291
292 host.Cut(0, 2);
293
294 nsCOMPtr<nsIEffectiveTLDService> tldService =
295 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
296
297 if (!tldService) {
298 return false;
299 }
300
301 NS_ConvertUTF16toUTF8 cHost(host);
302 nsAutoCString publicSuffix;
303
304 nsresult rv = tldService->GetPublicSuffixFromHost(cHost, publicSuffix);
305
306 return NS_SUCCEEDED(rv) && !cHost.Equals(publicSuffix);
307 }
308
309 return true;
310 };
311
SchemeInList(nsAString & scheme,const char ** schemes)312 bool SchemeInList(nsAString& scheme, const char** schemes) {
313 for (; *schemes; schemes++) {
314 if (scheme.LowerCaseEqualsASCII(*schemes)) {
315 return true;
316 }
317 }
318 return false;
319 };
320
321 // Formatters
322
GetStringBundle()323 already_AddRefed<nsIStringBundle> GetStringBundle() {
324 nsCOMPtr<nsIStringBundleService> sbs =
325 mozilla::services::GetStringBundleService();
326 NS_ENSURE_TRUE(sbs, nullptr);
327
328 nsCOMPtr<nsIStringBundle> stringBundle;
329 sbs->CreateBundle("chrome://global/locale/extensions.properties",
330 getter_AddRefs(stringBundle));
331
332 return stringBundle.forget();
333 };
334
FormatErrorParams(const char * aName,const nsTArray<nsString> & aParams)335 void FormatErrorParams(const char* aName, const nsTArray<nsString>& aParams) {
336 nsresult rv = NS_ERROR_FAILURE;
337
338 nsCOMPtr<nsIStringBundle> stringBundle = GetStringBundle();
339
340 if (stringBundle) {
341 rv = stringBundle->FormatStringFromName(aName, aParams, mError);
342 }
343
344 if (NS_WARN_IF(NS_FAILED(rv))) {
345 mError.AssignLiteral("An unexpected error occurred");
346 }
347 };
348
349 // Data members
350
351 nsAutoString mURL;
352 NS_ConvertASCIItoUTF16 mDirective;
353 nsString mError;
354
355 bool mFoundSelf;
356 };
357
358 /**
359 * Validates a custom content security policy string for use by an add-on.
360 * In particular, ensures that:
361 *
362 * - Both object-src and script-src directives are present, and meet
363 * the policies required by the CSPValidator class
364 *
365 * - The script-src directive includes the source 'self'
366 */
367 NS_IMETHODIMP
ValidateAddonCSP(const nsAString & aPolicyString,nsAString & aResult)368 AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
369 nsAString& aResult) {
370 nsresult rv;
371
372 // Validate against a randomly-generated extension origin.
373 // There is no add-on-specific behavior in the CSP code, beyond the ability
374 // for add-ons to specify a custom policy, but the parser requires a valid
375 // origin in order to operate correctly.
376 nsAutoString url(u"moz-extension://");
377 {
378 nsCOMPtr<nsIUUIDGenerator> uuidgen = services::GetUUIDGenerator();
379 NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
380
381 nsID id;
382 rv = uuidgen->GenerateUUIDInPlace(&id);
383 NS_ENSURE_SUCCESS(rv, rv);
384
385 char idString[NSID_LENGTH];
386 id.ToProvidedString(idString);
387
388 MOZ_RELEASE_ASSERT(idString[0] == '{' && idString[NSID_LENGTH - 2] == '}',
389 "UUID generator did not return a valid UUID");
390
391 url.AppendASCII(idString + 1, NSID_LENGTH - 3);
392 }
393
394 RefPtr<BasePrincipal> principal =
395 BasePrincipal::CreateContentPrincipal(NS_ConvertUTF16toUTF8(url));
396
397 nsCOMPtr<nsIURI> selfURI;
398 principal->GetURI(getter_AddRefs(selfURI));
399 RefPtr<nsCSPContext> csp = new nsCSPContext();
400 rv =
401 csp->SetRequestContextWithPrincipal(principal, selfURI, EmptyString(), 0);
402 NS_ENSURE_SUCCESS(rv, rv);
403 csp->AppendPolicy(aPolicyString, false, false);
404
405 const nsCSPPolicy* policy = csp->GetPolicy(0);
406 if (!policy) {
407 CSPValidator validator(url, nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE);
408 aResult.Assign(validator.GetError());
409 return NS_OK;
410 }
411
412 bool haveValidDefaultSrc = false;
413 {
414 CSPDirective directive = nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
415 CSPValidator validator(url, directive);
416
417 haveValidDefaultSrc = policy->visitDirectiveSrcs(directive, &validator);
418 }
419
420 aResult.SetIsVoid(true);
421 {
422 CSPDirective directive = nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE;
423 CSPValidator validator(url, directive, !haveValidDefaultSrc);
424
425 if (!policy->visitDirectiveSrcs(directive, &validator)) {
426 aResult.Assign(validator.GetError());
427 } else if (!validator.FoundSelf()) {
428 validator.FormatError("csp.error.missing-source",
429 NS_LITERAL_STRING("'self'"));
430 aResult.Assign(validator.GetError());
431 }
432 }
433
434 if (aResult.IsVoid()) {
435 CSPDirective directive = nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE;
436 CSPValidator validator(url, directive, !haveValidDefaultSrc);
437
438 if (!policy->visitDirectiveSrcs(directive, &validator)) {
439 aResult.Assign(validator.GetError());
440 }
441 }
442
443 return NS_OK;
444 }
445