Coverage Report

Created: 2018-09-25 14:53

/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
}