mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'security-headers' into devel
This commit is contained in:
@@ -335,6 +335,7 @@ var toc = [
|
||||
"audit-argument-checks",
|
||||
"backbone",
|
||||
"bootstrap",
|
||||
"browser-policy",
|
||||
"coffeescript",
|
||||
"d3",
|
||||
"force-ssl",
|
||||
|
||||
@@ -22,6 +22,7 @@ and removed with:
|
||||
{{> pkg_audit_argument_checks}}
|
||||
{{> pkg_backbone}}
|
||||
{{> pkg_bootstrap}}
|
||||
{{> pkg_browser_policy}}
|
||||
{{> pkg_coffeescript}}
|
||||
{{> pkg_d3}}
|
||||
{{> pkg_force_ssl}}
|
||||
|
||||
156
docs/client/packages/browser-policy.html
Normal file
156
docs/client/packages/browser-policy.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<template name="pkg_browser_policy">
|
||||
{{#better_markdown}}
|
||||
## `browser-policy`
|
||||
|
||||
The `browser-policy` package lets you set security-related policies that will be
|
||||
enforced by newer browsers. These policies help you prevent and mitigate common
|
||||
attacks like cross-site scripting and clickjacking.
|
||||
|
||||
`browser-policy` lets you configure the HTTP headers X-Frame-Options and
|
||||
Content-Security-Policy. X-Frame-Options tells the browser which websites are
|
||||
allowed to frame your app. You should only let trusted websites frame your app,
|
||||
because malicious sites could harm your users
|
||||
with <a href="https://www.owasp.org/index.php/Clickjacking">clickjacking
|
||||
attacks</a>.
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Security/CSP/Introducing_Content_Security_Policy">Content-Security-Policy</a>
|
||||
tells the browser where your app can load content from, which encourages safe
|
||||
practices and mitigates the damage of a cross-site-scripting attack.
|
||||
|
||||
For most apps, we recommend that you take the following steps when using
|
||||
`browser-policy`:
|
||||
|
||||
* Call `BrowserPolicy.enableContentSecurityPolicy()` to enable a starter policy
|
||||
for your app. With this starter policy, your app's client code will be able to
|
||||
load content (images, scripts, fonts, etc.) only from its own origin, except
|
||||
that XMLHttpRequests and WebSocket connections can go to any origin. Further,
|
||||
your app's client code will not be able to use functions such as `eval()` that
|
||||
convert strings to code.
|
||||
* You can use the functions described below to customize the content
|
||||
security policy. If your app does not need any inline Javascript such as inline
|
||||
`<script>` tags, we recommend that you modify the policy by calling
|
||||
`BrowserPolicy.disallowInlineScripts()` in server code. This will result in one
|
||||
extra round trip when your app is loaded, but will help prevent cross-site
|
||||
scripting attacks by disabling all scripts except those loaded from a `script
|
||||
src` attribute.
|
||||
* If your app does not need to be framed by other websites, call
|
||||
`BrowserPolicy.allowFramingBySameOrigin()` to help prevent clickjacking attacks.
|
||||
|
||||
Meteor determines the browser policy when the server starts up, so you should
|
||||
call `BrowserPolicy` functions in top-level application code or in
|
||||
`Meteor.startup`.
|
||||
|
||||
#### Frame options
|
||||
|
||||
You can use the following functions to specify which websites are allowed to
|
||||
frame your app:
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.disallowFraming()"}}
|
||||
Your app will never render inside a frame or iframe.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingByOrigin(origin)"}}
|
||||
Your app will only render inside frames loaded by `origin`. You can only call
|
||||
this function once with a single origin, and cannot use wildcards or specify
|
||||
multiple origins that are allowed to frame your app. (This is a limitation of
|
||||
the X-Frame-Options header.) Example values of `origin` include
|
||||
"http://example.com" and "https://foo.example.com". Note that this value of the
|
||||
X-Frame-Options header is not yet supported in Chrome or Safari and will be
|
||||
ignored in those browsers.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingBySameOrigin()"}}
|
||||
Your app will only render inside frames loaded by webpages on the same origin as
|
||||
your app.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingByAnyOrigin()"}}
|
||||
Your app can be framed by any website.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
#### Content options
|
||||
|
||||
You can use the functions in this section to control how different types of
|
||||
content can be loaded on your site. In order to use any of these functions, you
|
||||
must first call `BrowserPolicy.enableContentSecurityPolicy()`, which enables the
|
||||
starter policy described above. This section covers additional functions that
|
||||
you can use to tighten or relax restrictions on what content your app can use.
|
||||
|
||||
You can use the following functions to adjust policies on where Javascript and
|
||||
CSS can be run:
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.allowInlineScripts()"}}
|
||||
Allows inline `<script>` tags, `javascript:` URLs, and inline event handlers.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowInlineScripts()"}}
|
||||
Disallows inline Javascript. Calling this function results in an extra
|
||||
round-trip on page load to retrieve Meteor runtime configuration that is usually
|
||||
part of an inline script tag.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowEval()"}}
|
||||
Allows the creation of Javascript code from strings using function such as `eval()`.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowEval()"}}
|
||||
Disallows eval and related functions.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowInlineStyles()"}}
|
||||
Allows inline style tags and style attributes.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowInlineStyles()"}}
|
||||
Disallows inline CSS.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
Finally, you can configure a whitelist of allowed requests that various types of
|
||||
content can make. The following functions are defined for the content types
|
||||
script, object, image, media, font, and connect.
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>Origin(origin)"}}
|
||||
Allows this type of content to be loaded from the given origin. `origin` is a
|
||||
string and can include an optional scheme (such as `http` or `https`), an
|
||||
optional wildcard at the beginning, and an optional port which can be a
|
||||
wildcard. Examples include `example.com`, `https://*.example.com`, and
|
||||
`example.com:*`. You can call these functions multiple times with different
|
||||
origins to specify a whitelist of allowed origins.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>DataUrl()"}}
|
||||
Allows this type of content to be loaded from a `data:` URL.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>SameOrigin()"}}
|
||||
Allows this type of content to be loaded from the same origin as your app.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallow<ContentType>()"}}
|
||||
Disallows this type of content on your app.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
These functions are also defined for the content type `AllContent`, which is a
|
||||
shorthand for calling one of the above functions once for each content type.
|
||||
For example, if you want to allow the origin `https://foo.com` for all types of
|
||||
content but you want to disable `<object>` tags, you can call
|
||||
`BrowserPolicy.allowAllContentOrigin("https://foo.com")` followed by
|
||||
`BrowserPolicy.disallowObject()`.
|
||||
|
||||
Other examples of using the `BrowserPolicy` API:
|
||||
|
||||
* `BrowserPolicy.disallowObject()` causes the browser to disallow all
|
||||
`<object>` tags.
|
||||
* `BrowserPolicy.allowImageOrigin("https://example.com")`
|
||||
allows images to have their `src` attributes point to images served from
|
||||
`https://example.com`.
|
||||
* `BrowserPolicy.allowConnectOrigin("https://example.com")` allows XMLHttpRequest
|
||||
and WebSocket connections to `https://example.com`.
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
1
packages/browser-policy/.gitignore
vendored
Normal file
1
packages/browser-policy/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
123
packages/browser-policy/browser-policy-test.js
Normal file
123
packages/browser-policy/browser-policy-test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
var cspsEqual = function (csp1, csp2) {
|
||||
var cspToObj = function (csp) {
|
||||
csp = csp.substring(0, csp.length - 1);
|
||||
var parts = _.map(csp.split("; "), function (part) {
|
||||
return part.split(" ");
|
||||
});
|
||||
var keys = _.map(parts, _.first);
|
||||
var values = _.map(parts, _.rest);
|
||||
_.each(values, function (value) {
|
||||
value.sort();
|
||||
});
|
||||
return _.object(keys, values);
|
||||
};
|
||||
|
||||
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
|
||||
};
|
||||
|
||||
Tinytest.add("browser-policy - csp", function (test) {
|
||||
var defaultCsp = "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"
|
||||
BrowserPolicy.enableContentSecurityPolicy(true /* enable for tests */);
|
||||
|
||||
// Default policy
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true /* tests-only flag */));
|
||||
|
||||
// Redundant whitelisting (inline scripts already allowed in default policy)
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
|
||||
|
||||
// Disallow inline scripts
|
||||
BrowserPolicy.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Allow eval
|
||||
BrowserPolicy.allowEval();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
|
||||
// Disallow inline styles
|
||||
BrowserPolicy.disallowInlineStyles();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self';"));
|
||||
|
||||
// Allow data: urls everywhere
|
||||
BrowserPolicy.allowAllContentDataUrl();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self' data:; script-src 'self' 'unsafe-eval' data:; " +
|
||||
"connect-src * data: 'self'; img-src data: 'self'; style-src 'self' data:;"));
|
||||
|
||||
// Disallow everything
|
||||
BrowserPolicy.disallowAllContent();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'none';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Put inline scripts back in
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'none'; script-src 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Add 'self' to all content types
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Disallow all content except same-origin scripts
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowScriptSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'none'; script-src 'self';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Starting with all content same origin, disallowScript() and then allow
|
||||
// inline scripts. Result should be that that only inline scripts can execute,
|
||||
// not same-origin scripts.
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self';"));
|
||||
BrowserPolicy.disallowScript();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'none';"));
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'unsafe-inline';"));
|
||||
|
||||
// Starting with all content same origin, allow inline scripts. (Should result
|
||||
// in both same origin and inline scripts allowed.)
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
BrowserPolicy.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self';"));
|
||||
|
||||
// Allow same origin for all content, then disallow object entirely.
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
BrowserPolicy.disallowObject();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; object-src 'none';"));
|
||||
});
|
||||
|
||||
Tinytest.add("browser-policy - x-frame-options", function (test) {
|
||||
BrowserPolicy._reset();
|
||||
BrowserPolicy.disallowFraming();
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "DENY");
|
||||
BrowserPolicy.allowFramingBySameOrigin();
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "SAMEORIGIN");
|
||||
BrowserPolicy.allowFramingByOrigin("foo.com");
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "ALLOW-FROM foo.com");
|
||||
test.throws(function () {
|
||||
BrowserPolicy.allowFramingByOrigin("bar.com");
|
||||
});
|
||||
BrowserPolicy.allowFramingByAnyOrigin();
|
||||
test.isFalse(BrowserPolicy._constructXFrameOptions());
|
||||
});
|
||||
284
packages/browser-policy/browser-policy.js
Normal file
284
packages/browser-policy/browser-policy.js
Normal file
@@ -0,0 +1,284 @@
|
||||
// To enable CSP, call BrowserPolicy.enableContentSecurityPolicy(). This enables
|
||||
// the following default policy:
|
||||
// No eval or other string-to-code, and content can only be loaded from the
|
||||
// same origin as the app (except for XHRs and websocket connections, which can
|
||||
// go to any origin).
|
||||
//
|
||||
// Apps should call BrowserPolicy.allowFramingBySameOrigin() to allow only
|
||||
// same-origin pages to frame their apps, if they don't explicitly want to be
|
||||
// framed by third-party sites.
|
||||
//
|
||||
// Apps should call BrowserPolicy.disallowInlineScripts() if they are not using
|
||||
// any inline script tags and are willing to accept an extra round trip on page
|
||||
// load.
|
||||
//
|
||||
// BrowserPolicy functions for tweaking CSP:
|
||||
// allowInlineScripts()
|
||||
// disallowInlineScripts(): adds extra round-trip to page load time
|
||||
// allowInlineStyles()
|
||||
// disallowInlineStyles()
|
||||
// allowEval()
|
||||
// disallowEval()
|
||||
//
|
||||
// For each type of content (script, object, image, media, font, connect,
|
||||
// style), there are the following functions:
|
||||
// allow<content type>Origin(origin): allows the type of content to be loaded
|
||||
// from the given origin
|
||||
// allow<content type>DataUrl(): allows the content to be loaded from data: URLs
|
||||
// allow<content type>SameOrigin(): allows the content to be loaded from the
|
||||
// same origin
|
||||
// disallow<content type>(): disallows this type of content all together (can't
|
||||
// be called for script)
|
||||
//
|
||||
// The following functions allow you to set rules for all types of content at
|
||||
// once:
|
||||
// allowAllContentOrigin(origin)
|
||||
// allowAllContentDataUrl()
|
||||
// allowAllContentSameOrigin()
|
||||
// disallowAllContent()
|
||||
//
|
||||
//
|
||||
// For controlling which origins can frame this app,
|
||||
// BrowserPolicy.disallowFraming()
|
||||
// BrowserPolicy.allowFramingByOrigin(origin)
|
||||
// BrowserPolicy.allowFramingBySameOrigin()
|
||||
// BrowserPolicy.allowFramingByAnyOrigin();
|
||||
|
||||
var xFrameOptions;
|
||||
var cspSrcs;
|
||||
|
||||
// CSP keywords have to be single-quoted.
|
||||
var unsafeInline = "'unsafe-inline'";
|
||||
var unsafeEval = "'unsafe-eval'";
|
||||
var selfKeyword = "'self'";
|
||||
var noneKeyword = "'none'";
|
||||
|
||||
var cspEnabled = false;
|
||||
var cspEnabledForTests = false;
|
||||
|
||||
BrowserPolicy = {};
|
||||
|
||||
// Exported for tests.
|
||||
var constructXFrameOptions = BrowserPolicy._constructXFrameOptions =
|
||||
function () {
|
||||
return xFrameOptions;
|
||||
};
|
||||
|
||||
var constructCsp = BrowserPolicy._constructCsp = function () {
|
||||
cspSrcs = cspSrcs || {};
|
||||
|
||||
var header = _.map(cspSrcs, function (srcs, directive) {
|
||||
srcs = srcs || [];
|
||||
if (_.isEmpty(srcs))
|
||||
srcs = [noneKeyword];
|
||||
var directiveCsp = _.uniq(srcs).join(" ");
|
||||
return directive + " " + directiveCsp + ";";
|
||||
});
|
||||
|
||||
header = header.join(" ");
|
||||
return header;
|
||||
};
|
||||
|
||||
var parseCsp = function (csp) {
|
||||
var policies = csp.split("; ");
|
||||
cspSrcs = {};
|
||||
_.each(policies, function (policy) {
|
||||
if (policy[policy.length - 1] === ";")
|
||||
policy = policy.substring(0, policy.length - 1);
|
||||
var srcs = policy.split(" ");
|
||||
var directive = srcs[0];
|
||||
if (_.indexOf(srcs, noneKeyword) !== -1)
|
||||
cspSrcs[directive] = null;
|
||||
else
|
||||
cspSrcs[directive] = srcs.slice(1);
|
||||
});
|
||||
|
||||
if (cspSrcs["default-src"] === undefined)
|
||||
throw new Error("Content Security Policies used with " +
|
||||
"browser-policy must specify a default-src.");
|
||||
|
||||
// Copy default-src sources to other directives.
|
||||
_.each(cspSrcs, function (sources, directive) {
|
||||
cspSrcs[directive] = _.union(sources || [], cspSrcs["default-src"] || []);
|
||||
});
|
||||
};
|
||||
|
||||
var removeCspSrc = function (directive, src) {
|
||||
cspSrcs[directive] = _.without(cspSrcs[directive] || [], src);
|
||||
};
|
||||
|
||||
var ensureDirective = function (directive) {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs = cspSrcs || {};
|
||||
if (! _.has(cspSrcs, directive))
|
||||
cspSrcs[directive] = _.clone(cspSrcs["default-src"]);
|
||||
};
|
||||
|
||||
var throwIfNotEnabled = function () {
|
||||
if (! cspEnabled && ! cspEnabledForTests)
|
||||
throw new Error("Enable this function by calling "+
|
||||
"BrowserPolicy.enableContentSecurityPolicy().");
|
||||
};
|
||||
|
||||
WebApp.connectHandlers.use(function (req, res, next) {
|
||||
if (xFrameOptions)
|
||||
res.setHeader("X-Frame-Options", constructXFrameOptions());
|
||||
if (cspEnabled)
|
||||
res.setHeader("Content-Security-Policy", constructCsp());
|
||||
next();
|
||||
});
|
||||
|
||||
BrowserPolicy = _.extend(BrowserPolicy, {
|
||||
_reset: function () {
|
||||
xFrameOptions = null;
|
||||
cspSrcs = null;
|
||||
cspEnabled = false;
|
||||
},
|
||||
|
||||
allowFramingBySameOrigin: function () {
|
||||
xFrameOptions = "SAMEORIGIN";
|
||||
},
|
||||
disallowFraming: function () {
|
||||
xFrameOptions = "DENY";
|
||||
},
|
||||
// ALLOW-FROM not supported in Chrome or Safari.
|
||||
allowFramingByOrigin: function (origin) {
|
||||
// Trying to specify two allow-from throws to prevent users from
|
||||
// accidentally overwriting an allow-from origin when they think they are
|
||||
// adding multiple origins.
|
||||
if (xFrameOptions && xFrameOptions.indexOf("ALLOW-FROM") === 0)
|
||||
throw new Error("You can only specify one origin that is allowed to" +
|
||||
" frame this app.");
|
||||
xFrameOptions = "ALLOW-FROM " + origin;
|
||||
},
|
||||
allowFramingByAnyOrigin: function () {
|
||||
xFrameOptions = null;
|
||||
},
|
||||
|
||||
// _enableForTests means that you can call CSP functions, but the header won't
|
||||
// actually be sent.
|
||||
enableContentSecurityPolicy: function (_enableForTests) {
|
||||
// By default, unsafe inline scripts and styles are allowed, since we expect
|
||||
// many apps will use them for analytics, etc. Unsafe eval is disallowed, and
|
||||
// the only allowable content source is the same origin or data, except for
|
||||
// connect which allows anything (since meteor.com apps make websocket
|
||||
// connections to a lot of different origins).
|
||||
if (! _enableForTests)
|
||||
cspEnabled = true;
|
||||
else
|
||||
cspEnabledForTests = true;
|
||||
cspSrcs = {};
|
||||
BrowserPolicy.setContentSecurityPolicy("default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src *; " +
|
||||
"img-src data: 'self'; " +
|
||||
"style-src 'self' 'unsafe-inline';");
|
||||
},
|
||||
|
||||
setContentSecurityPolicy: function (csp) {
|
||||
throwIfNotEnabled();
|
||||
parseCsp(csp);
|
||||
},
|
||||
|
||||
// Helpers for creating content security policies
|
||||
|
||||
_keywordAllowed: function (directive, keyword, _calledFromTests) {
|
||||
// All keywords are allowed if csp is not enabled and we're not in a test
|
||||
// run. If csp is enabled or we're in a test run, then look in cspSrcs to
|
||||
// see if it's allowed.
|
||||
return (! cspEnabled && ! _calledFromTests) ||
|
||||
(cspSrcs[directive] &&
|
||||
_.indexOf(cspSrcs[directive], keyword) !== -1);
|
||||
},
|
||||
|
||||
// Used by webapp to determine whether we need an extra round trip for
|
||||
// __meteor_runtime_config__.
|
||||
// _calledFromTests is used to indicate that we should ignore cspEnabled and
|
||||
// instead look directly in cspSrcs to determine if the keyword is allowed.
|
||||
// XXX maybe this test interface could be cleaned up
|
||||
inlineScriptsAllowed: function (_calledFromTests) {
|
||||
return BrowserPolicy._keywordAllowed("script-src",
|
||||
unsafeInline, _calledFromTests);
|
||||
},
|
||||
|
||||
allowInlineScripts: function () {
|
||||
ensureDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeInline);
|
||||
},
|
||||
disallowInlineScripts: function () {
|
||||
ensureDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeInline);
|
||||
},
|
||||
allowEval: function () {
|
||||
ensureDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeEval);
|
||||
},
|
||||
disallowEval: function () {
|
||||
ensureDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeEval);
|
||||
},
|
||||
allowInlineStyles: function () {
|
||||
ensureDirective("style-src");
|
||||
cspSrcs["style-src"].push(unsafeInline);
|
||||
},
|
||||
disallowInlineStyles: function () {
|
||||
ensureDirective("style-src");
|
||||
removeCspSrc("style-src", unsafeInline);
|
||||
},
|
||||
|
||||
// Functions for setting defaults
|
||||
allowAllContentSameOrigin: function () {
|
||||
BrowserPolicy.allowAllContentOrigin(selfKeyword);
|
||||
},
|
||||
allowAllContentDataUrl: function () {
|
||||
BrowserPolicy.allowAllContentOrigin("data:");
|
||||
},
|
||||
allowAllContentOrigin: function (origin) {
|
||||
ensureDirective("default-src");
|
||||
_.each(_.keys(cspSrcs), function (directive) {
|
||||
cspSrcs[directive].push(origin);
|
||||
});
|
||||
},
|
||||
disallowAllContent: function () {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs = {
|
||||
"default-src": []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// allow<Resource>Origin, allow<Resource>Data, allow<Resource>self, and
|
||||
// disallow<Resource> methods for each type of resource.
|
||||
_.each(["script", "object", "img", "media",
|
||||
"font", "connect", "style"],
|
||||
function (resource) {
|
||||
var directive = resource + "-src";
|
||||
var methodResource;
|
||||
if (resource !== "img") {
|
||||
methodResource = resource.charAt(0).toUpperCase() +
|
||||
resource.slice(1);
|
||||
} else {
|
||||
methodResource = "Image";
|
||||
}
|
||||
var allowMethodName = "allow" + methodResource + "Origin";
|
||||
var disallowMethodName = "disallow" + methodResource;
|
||||
var allowDataMethodName = "allow" + methodResource + "DataUrl";
|
||||
var allowSelfMethodName = "allow" + methodResource + "SameOrigin";
|
||||
|
||||
BrowserPolicy[allowMethodName] = function (src) {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push(src);
|
||||
};
|
||||
BrowserPolicy[disallowMethodName] = function () {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs[directive] = [];
|
||||
};
|
||||
BrowserPolicy[allowDataMethodName] = function () {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push("data:");
|
||||
};
|
||||
BrowserPolicy[allowSelfMethodName] = function () {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push(selfKeyword);
|
||||
};
|
||||
});
|
||||
14
packages/browser-policy/package.js
Normal file
14
packages/browser-policy/package.js
Normal file
@@ -0,0 +1,14 @@
|
||||
Package.describe({
|
||||
summary: "Configure security policies enforced by the browser"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(["underscore", "webapp"], "server");
|
||||
api.add_files("browser-policy.js", "server");
|
||||
api.export("BrowserPolicy", "server");
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(["tinytest", "browser-policy", "ejson"]);
|
||||
api.add_files("browser-policy-test.js", "server");
|
||||
});
|
||||
@@ -12,6 +12,11 @@ Package.on_use(function (api) {
|
||||
api.use(['application-configuration'], {
|
||||
unordered: true
|
||||
});
|
||||
// At response serving time, webapp uses browser-policy if it is loaded. If
|
||||
// browser-policy is loaded, then it must be loaded after webapp
|
||||
// (browser-policy depends on webapp). So we don't explicitly depend in any
|
||||
// way on browser-policy here, but we use it when it is loaded, and it can be
|
||||
// loaded after webapp.
|
||||
api.export(['WebApp', 'main', 'WebAppInternals'], 'server');
|
||||
api.add_files('webapp_server.js', 'server');
|
||||
});
|
||||
|
||||
@@ -147,7 +147,6 @@ var appUrl = function (url) {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
var runWebAppServer = function () {
|
||||
// read the control for the client we'll be serving up
|
||||
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
||||
@@ -231,6 +230,17 @@ var runWebAppServer = function () {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Package["browser-policy"] &&
|
||||
! Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed() &&
|
||||
pathname === "/meteor_runtime_config.js") {
|
||||
res.writeHead(200, { 'Content-type': 'application/javascript' });
|
||||
res.write("__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.has(staticFiles, pathname)) {
|
||||
next();
|
||||
return;
|
||||
@@ -387,15 +397,25 @@ var runWebAppServer = function () {
|
||||
argv = optimist(argv).boolean('keepalive').argv;
|
||||
|
||||
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
|
||||
boilerplateHtml =
|
||||
fs.readFileSync(boilerplateHtmlPath, 'utf8')
|
||||
.replace(
|
||||
"// ##RUNTIME_CONFIG##",
|
||||
"__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";")
|
||||
.replace(
|
||||
/##ROOT_URL_PATH_PREFIX##/g,
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
boilerplateHtml = fs.readFileSync(boilerplateHtmlPath, 'utf8');
|
||||
|
||||
// Include __meteor_runtime_config__ in the app html, as an inline script if
|
||||
// it's not forbidden by CSP.
|
||||
if (! Package["browser-policy"] ||
|
||||
Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed()) {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript'>__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";</script>");
|
||||
} else {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript' src='##ROOT_URL_PATH_PREFIX##/meteor_runtime_config.js'></script>"
|
||||
);
|
||||
}
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##ROOT_URL_PATH_PREFIX##/g,
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
|
||||
// only start listening after all the startup code has run.
|
||||
var localPort = parseInt(process.env.PORT) || 0;
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
{{#each stylesheets}} <link rel="stylesheet" href="##ROOT_URL_PATH_PREFIX##{{this}}">
|
||||
{{/each}}
|
||||
|
||||
<script type="text/javascript">
|
||||
// ##RUNTIME_CONFIG##
|
||||
</script>
|
||||
##RUNTIME_CONFIG##
|
||||
|
||||
{{#each scripts}} <script type="text/javascript" src="##ROOT_URL_PATH_PREFIX##{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
Reference in New Issue
Block a user