Source code

Revision control

Other Tools

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: " \
39
40
AddonContentPolicy::AddonContentPolicy() {}
41
42
AddonContentPolicy::~AddonContentPolicy() {}
43
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
60
static nsresult LogMessage(const nsAString& aMessage, nsIURI* aSourceURI,
61
const nsAString& aSourceSample,
62
nsISupports* aContext) {
63
nsCOMPtr<nsIScriptError> error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
64
NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY);
65
66
uint64_t windowID = 0;
67
GetWindowIDFromContext(aContext, &windowID);
68
69
nsresult rv = error->InitWithSourceURI(aMessage, aSourceURI, aSourceSample, 0,
70
0, nsIScriptError::errorFlag,
71
"JavaScript", windowID);
72
NS_ENSURE_SUCCESS(rv, rv);
73
74
nsCOMPtr<nsIConsoleService> console =
75
do_GetService(NS_CONSOLESERVICE_CONTRACTID);
76
NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY);
77
78
console->LogMessage(error);
79
return NS_OK;
80
}
81
82
// Content policy enforcement:
83
84
NS_IMETHODIMP
85
AddonContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
86
const nsACString& aMimeTypeGuess,
87
int16_t* aShouldLoad) {
88
if (!aContentLocation || !aLoadInfo) {
89
NS_SetRequestBlockingReason(
90
aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
91
*aShouldLoad = REJECT_REQUEST;
92
return NS_ERROR_FAILURE;
93
}
94
95
uint32_t contentType = aLoadInfo->GetExternalContentPolicyType();
96
nsCOMPtr<nsIURI> requestOrigin;
97
nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->LoadingPrincipal();
98
if (loadingPrincipal) {
99
loadingPrincipal->GetURI(getter_AddRefs(requestOrigin));
100
}
101
102
MOZ_ASSERT(contentType == nsContentUtils::InternalContentPolicyTypeToExternal(
103
contentType),
104
"We should only see external content policy types here.");
105
106
*aShouldLoad = nsIContentPolicy::ACCEPT;
107
108
if (!requestOrigin) {
109
return NS_OK;
110
}
111
112
// Only apply this policy to requests from documents loaded from
113
// moz-extension URLs, or to resources being loaded from moz-extension URLs.
114
if (!(aContentLocation->SchemeIs("moz-extension") ||
115
requestOrigin->SchemeIs("moz-extension"))) {
116
return NS_OK;
117
}
118
119
if (contentType == nsIContentPolicy::TYPE_SCRIPT) {
120
NS_ConvertUTF8toUTF16 typeString(aMimeTypeGuess);
121
nsContentTypeParser mimeParser(typeString);
122
123
// Reject attempts to load JavaScript scripts with a non-default version.
124
nsAutoString mimeType, version;
125
if (NS_SUCCEEDED(mimeParser.GetType(mimeType)) &&
126
nsContentUtils::IsJavascriptMIMEType(mimeType) &&
127
NS_SUCCEEDED(mimeParser.GetParameter("version", version))) {
128
NS_SetRequestBlockingReason(
129
aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
130
*aShouldLoad = nsIContentPolicy::REJECT_REQUEST;
131
132
nsCOMPtr<nsISupports> context = aLoadInfo->GetLoadingContext();
133
LogMessage(NS_LITERAL_STRING(VERSIONED_JS_BLOCKED_MESSAGE), requestOrigin,
134
typeString, context);
135
return NS_OK;
136
}
137
}
138
139
return NS_OK;
140
}
141
142
NS_IMETHODIMP
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:
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
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
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
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
261
bool visitNonceSrc(const nsCSPNonceSrc& src) override {
262
FormatError("csp.error.illegal-keyword", NS_LITERAL_STRING("'nonce-*'"));
263
return false;
264
};
265
266
bool visitHashSrc(const nsCSPHashSrc& src) override { return true; };
267
268
// Accessors
269
270
inline nsAString& GetError() { return mError; };
271
272
inline bool FoundSelf() { return mFoundSelf; };
273
274
// Formatters
275
276
template <typename... T>
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
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
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
323
already_AddRefed<nsIStringBundle> GetStringBundle() {
324
nsCOMPtr<nsIStringBundleService> sbs =
325
mozilla::services::GetStringBundleService();
326
NS_ENSURE_TRUE(sbs, nullptr);
327
328
nsCOMPtr<nsIStringBundle> stringBundle;
330
getter_AddRefs(stringBundle));
331
332
return stringBundle.forget();
333
};
334
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
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
}