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