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