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