Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Separate browser policy functions into two packages.

* browser-policy uses browser-policy-framing and browser-policy-content, both of
  which set default policies when they are used. This way you get a default
  policy when you add a browser policy package, but you can pick and choose
  different packages if you only want to think about one of them.
* The two packages use different namespaces: BrowserPolicy.framing and
  BrowserPolicy.content, which meant some functions got renamed (e.g. not using
  "framing" or "content in the function name when it's already in the
  namespace).
  • Loading branch information...
commit b5286b941a77a1bdd57abb1ea01385eeaa62f7ea 1 parent d5159ae
Emily Stark estark37 authored
124 docs/client/packages/browser-policy.html
View
@@ -6,34 +6,38 @@
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
+When you add `browser-policy` to your app, you get default configurations for
+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.
+`browser-policy` also provides functions for you to configure these policies if
+the defaults are not suitable.
-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.
+If you only want to use Content-Security-Policy or X-Frame-Options but not both,
+you can add the individual packages `browser-policy-content` or
+`browser-policy-framing` instead of `browser-policy`.
+
+For most apps, we recommend that you take the following steps:
+
+* Add `browser-policy` to your app to enable a starter policy. 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. Users'
+browsers will only let your app be framed by web pages on the same origin as
+your app.
+* You can use the functions described below to customize the policies. If your
+app does not need any inline Javascript such as inline `<script>` tags, we
+recommend that you modify the policy by calling
+`BrowserPolicy.content.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.
Meteor determines the browser policy when the server starts up, so you should
call `BrowserPolicy` functions in top-level application code or in
@@ -41,14 +45,14 @@
#### Frame options
-You can use the following functions to specify which websites are allowed to
-frame your app. By default, any website is allowed.
+By default, only web pages on the same origin as your app are allowed to frame
+your app. You can use the following functions to modify this policy.
<dl class="callbacks">
-{{#dtdd "BrowserPolicy.disallowFraming()"}}
+{{#dtdd "BrowserPolicy.framing.disallow()"}}
Your app will never render inside a frame or iframe.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.restrictFramingToOrigin(origin)"}}
+{{#dtdd "BrowserPolicy.framing.restrictToOrigin(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
@@ -60,47 +64,46 @@
{{/warning}}
{{/dtdd}}
-{{#dtdd "BrowserPolicy.restrictFramingToSameOrigin()"}}
-Your app will only render inside frames loaded by webpages on the same origin as
-your app.
+{{#dtdd "BrowserPolicy.framing.allowAll()"}}
+This unsets the X-Frame-Options header, so that your app can be framed by
+any webpage.
{{/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.
+content can be loaded on your site.
You can use the following functions to adjust policies on where Javascript and
CSS can be run:
<dl class="callbacks">
-{{#dtdd "BrowserPolicy.allowInlineScripts()"}}
+{{#dtdd "BrowserPolicy.content.allowInlineScripts()"}}
Allows inline `<script>` tags, `javascript:` URLs, and inline event handlers.
+The default policy already allows inline scripts.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.disallowInlineScripts()"}}
+{{#dtdd "BrowserPolicy.content.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()"}}
+{{#dtdd "BrowserPolicy.content.allowEval()"}}
Allows the creation of Javascript code from strings using function such as `eval()`.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.disallowEval()"}}
-Disallows eval and related functions.
+{{#dtdd "BrowserPolicy.content.disallowEval()"}}
+Disallows eval and related functions. The default policy already disallows eval.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.allowInlineStyles()"}}
-Allows inline style tags and style attributes.
+{{#dtdd "BrowserPolicy.content.allowInlineStyles()"}}
+Allows inline style tags and style attributes. The default policy already allows
+inline styles.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.disallowInlineStyles()"}}
+{{#dtdd "BrowserPolicy.content.disallowInlineStyles()"}}
Disallows inline CSS.
{{/dtdd}}
</dl>
@@ -110,7 +113,7 @@
script, object, image, media, font, and connect.
<dl class="callbacks">
-{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;Origin(origin)"}}
+{{#dtdd "BrowserPolicy.content.allow&lt;ContentType&gt;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
@@ -119,34 +122,41 @@
origins to specify a whitelist of allowed origins.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;DataUrl()"}}
+{{#dtdd "BrowserPolicy.content.allow&lt;ContentType&gt;DataUrl()"}}
Allows this type of content to be loaded from a `data:` URL.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;SameOrigin()"}}
+{{#dtdd "BrowserPolicy.content.allow&lt;ContentType&gt;SameOrigin()"}}
Allows this type of content to be loaded from the same origin as your app.
{{/dtdd}}
-{{#dtdd "BrowserPolicy.disallow&lt;ContentType&gt;()"}}
+{{#dtdd "BrowserPolicy.content.disallow&lt;ContentType&gt;()"}}
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()`.
+You can also set policies for all these types of content at once, using these
+functions:
+
+* `BrowserPolicy.content.allowSameOriginForAll()`,
+* `BrowserPolicy.content.allowDataUrlForAll()`,
+* `BrowserPolicy.content.allowOriginForAll(origin)`
+* `BrowserPolicy.content.disallowAll()`
+
+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.content.allowOriginForAll("https://foo.com")` followed by
+`BrowserPolicy.content.disallowObject()`.
-Other examples of using the `BrowserPolicy` API:
+Other examples of using the `BrowserPolicy.content` API:
-* `BrowserPolicy.disallowObject()` causes the browser to disallow all
-`<object>` tags.
-* `BrowserPolicy.allowImageOrigin("https://example.com")`
+* `BrowserPolicy.content.disallowFont()` causes the browser to disallow all
+`<font>` tags.
+* `BrowserPolicy.content.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
+* `BrowserPolicy.content.allowConnectOrigin("https://example.com")` allows XMLHttpRequest
and WebSocket connections to `https://example.com`.
1  packages/browser-policy-common/.gitignore
View
@@ -0,0 +1 @@
+.build*
27 packages/browser-policy-common/browser-policy-common.js
View
@@ -0,0 +1,27 @@
+BrowserPolicy = {};
+
+var inTest = false;
+
+BrowserPolicy._runningTest = function () {
+ return inTest;
+};
+
+BrowserPolicy._setRunningTest = function () {
+ inTest = true;
+};
+
+WebApp.connectHandlers.use(function (req, res, next) {
+ // Never set headers inside tests because they could break other tests.
+ if (BrowserPolicy._runningTest())
+ return next();
+
+ var xFrameOptions = BrowserPolicy.framing &&
+ BrowserPolicy.framing._constructXFrameOptions();
+ var csp = BrowserPolicy.content &&
+ BrowserPolicy.content._constructCsp();
+ if (xFrameOptions)
+ res.setHeader("X-Frame-Options", xFrameOptions);
+ if (csp)
+ res.setHeader("Content-Security-Policy", csp);
+ next();
+});
10 packages/browser-policy-common/package.js
View
@@ -0,0 +1,10 @@
+Package.describe({
+ summary: "Common code for browser-policy packages",
+ internal: true
+});
+
+Package.on_use(function (api) {
+ api.use('webapp', 'server');
+ api.add_files('browser-policy-common.js', 'server');
+ api.export('BrowserPolicy', 'server');
+});
1  packages/browser-policy-content/.gitignore
View
@@ -0,0 +1 @@
+.build*
219 packages/browser-policy-content/browser-policy-content.js
View
@@ -0,0 +1,219 @@
+// By adding this package, you get 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.content.disallowInlineScripts() if they are
+// not using any inline script tags and are willing to accept an extra round
+// trip on page load.
+//
+// BrowserPolicy.content 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()
+//
+
+var cspSrcs;
+// CSP keywords have to be single-quoted.
+var unsafeInline = "'unsafe-inline'";
+var unsafeEval = "'unsafe-eval'";
+var selfKeyword = "'self'";
+var noneKeyword = "'none'";
+
+BrowserPolicy.content = {};
+
+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) {
+ cspSrcs = cspSrcs || {};
+ if (! _.has(cspSrcs, directive))
+ cspSrcs[directive] = _.clone(cspSrcs["default-src"]);
+};
+
+var setDefaultPolicy = function () {
+ // 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).
+ BrowserPolicy.content.setPolicy("default-src 'self'; " +
+ "script-src 'self' 'unsafe-inline'; " +
+ "connect-src *; " +
+ "img-src data: 'self'; " +
+ "style-src 'self' 'unsafe-inline';");
+};
+
+_.extend(BrowserPolicy.content, {
+ // Exported for tests and browser-policy-common.
+ _constructCsp: function () {
+ if (! cspSrcs || _.isEmpty(cspSrcs))
+ return null;
+
+ 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;
+ },
+ _reset: function () {
+ setDefaultPolicy();
+ },
+
+ setPolicy: function (csp) {
+ parseCsp(csp);
+ },
+
+ _keywordAllowed: function (directive, keyword) {
+ return (cspSrcs[directive] &&
+ _.indexOf(cspSrcs[directive], keyword) !== -1);
+ },
+
+ // Used by webapp to determine whether we need an extra round trip for
+ // __meteor_runtime_config__. If we're in a test run, we should always return
+ // true, since CSP headers are never sent on tests -- unless the
+ // _calledFromTests flag is set, in which case a test is testing what
+ // inlineScriptsAllowed() would return if we weren't in a test. Wphew.
+ // XXX maybe this test interface could be cleaned up
+ inlineScriptsAllowed: function (_calledFromTests) {
+ if (BrowserPolicy._runningTest() && ! _calledFromTests)
+ return true;
+
+ return BrowserPolicy.content._keywordAllowed("script-src",
+ unsafeInline);
+ },
+
+ // Helpers for creating content security policies
+
+ 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
+ allowSameOriginForAll: function () {
+ BrowserPolicy.content.allowOriginForAll(selfKeyword);
+ },
+ allowDataUrlForAll: function () {
+ BrowserPolicy.content.allowOriginForAll("data:");
+ },
+ allowOriginForAll: function (origin) {
+ ensureDirective("default-src");
+ _.each(_.keys(cspSrcs), function (directive) {
+ cspSrcs[directive].push(origin);
+ });
+ },
+ disallowAll: function () {
+ 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.content[allowMethodName] = function (src) {
+ ensureDirective(directive);
+ cspSrcs[directive].push(src);
+ };
+ BrowserPolicy.content[disallowMethodName] = function () {
+ cspSrcs[directive] = [];
+ };
+ BrowserPolicy.content[allowDataMethodName] = function () {
+ ensureDirective(directive);
+ cspSrcs[directive].push("data:");
+ };
+ BrowserPolicy.content[allowSelfMethodName] = function () {
+ ensureDirective(directive);
+ cspSrcs[directive].push(selfKeyword);
+ };
+ });
+
+
+setDefaultPolicy();
9 packages/browser-policy-content/package.js
View
@@ -0,0 +1,9 @@
+Package.describe({
+ summary: "Configure content security policies"
+});
+
+Package.on_use(function (api) {
+ api.imply(["browser-policy-common"], "server");
+ api.add_files("browser-policy-content.js", "server");
+ api.use(["underscore", "browser-policy-common"], "server");
+});
1  packages/browser-policy-framing/.gitignore
View
@@ -0,0 +1 @@
+.build*
39 packages/browser-policy-framing/browser-policy-framing.js
View
@@ -0,0 +1,39 @@
+// By adding this package, you get a default policy where only web pages on the
+// same origin as your app can frame your app.
+//
+// For controlling which origins can frame this app,
+// BrowserPolicy.framing.disallow()
+// BrowserPolicy.framing.restrictToOrigin(origin)
+// BrowserPolicy.framing.allowByAnyOrigin()
+
+var defaultXFrameOptions = "SAMEORIGIN";
+var xFrameOptions = defaultXFrameOptions;
+
+BrowserPolicy.framing = {};
+
+_.extend(BrowserPolicy.framing, {
+ // Exported for tests and browser-policy-common.
+ _constructXFrameOptions: function () {
+ return xFrameOptions;
+ },
+ _reset: function () {
+ xFrameOptions = defaultXFrameOptions;
+ },
+
+ disallow: function () {
+ xFrameOptions = "DENY";
+ },
+ // ALLOW-FROM not supported in Chrome or Safari.
+ restrictToOrigin: 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;
+ },
+ allowAll: function () {
+ xFrameOptions = null;
+ }
+});
9 packages/browser-policy-framing/package.js
View
@@ -0,0 +1,9 @@
+Package.describe({
+ summary: "Restrict which websites can frame your app"
+});
+
+Package.on_use(function (api) {
+ api.imply(["browser-policy-common"], "server");
+ api.use(["underscore", "browser-policy-common"], "server");
+ api.add_files("browser-policy-framing.js", "server");
+});
112 packages/browser-policy/browser-policy-test.js
View
@@ -1,3 +1,5 @@
+BrowserPolicy._setRunningTest();
+
var cspsEqual = function (csp1, csp2) {
var cspToObj = function (csp) {
csp = csp.substring(0, csp.length - 1);
@@ -15,109 +17,111 @@ var cspsEqual = function (csp1, csp2) {
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
};
+// It's important to call _reset() at the beginnning of these tests; otherwise
+// the headers left over at the end of the last test run will be used.
+
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 */);
+ BrowserPolicy.content._reset();
// Default policy
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
- test.isTrue(BrowserPolicy.inlineScriptsAllowed(true /* tests-only flag */));
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), defaultCsp));
+ test.isTrue(BrowserPolicy.content.inlineScriptsAllowed(true /* tests-only flag */));
// Redundant whitelisting (inline scripts already allowed in default policy)
- BrowserPolicy.allowInlineScripts();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
+ BrowserPolicy.content.allowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), defaultCsp));
// Disallow inline scripts
- BrowserPolicy.disallowInlineScripts();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.disallowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'self'; script-src 'self'; " +
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
- test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
+ test.isFalse(BrowserPolicy.content.inlineScriptsAllowed(true));
// Allow eval
- BrowserPolicy.allowEval();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
+ BrowserPolicy.content.allowEval();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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'; " +
+ BrowserPolicy.content.disallowInlineStyles();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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(),
+ BrowserPolicy.content.allowDataUrlForAll();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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));
+ BrowserPolicy.content.disallowAll();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'none';"));
+ test.isFalse(BrowserPolicy.content.inlineScriptsAllowed(true));
// Put inline scripts back in
- BrowserPolicy.allowInlineScripts();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.allowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'none'; script-src 'unsafe-inline';"));
- test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
+ test.isTrue(BrowserPolicy.content.inlineScriptsAllowed(true));
// Add 'self' to all content types
- BrowserPolicy.allowAllContentSameOrigin();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.allowSameOriginForAll();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
- test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
+ test.isTrue(BrowserPolicy.content.inlineScriptsAllowed(true));
// Disallow all content except same-origin scripts
- BrowserPolicy.disallowAllContent();
- BrowserPolicy.allowScriptSameOrigin();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.disallowAll();
+ BrowserPolicy.content.allowScriptSameOrigin();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'none'; script-src 'self';"));
- test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
+ test.isFalse(BrowserPolicy.content.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(),
+ BrowserPolicy.content.disallowAll();
+ BrowserPolicy.content.allowSameOriginForAll();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'self';"));
+ BrowserPolicy.content.disallowScript();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'self'; script-src 'none';"));
- BrowserPolicy.allowInlineScripts();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.allowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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(),
+ BrowserPolicy.content.disallowAll();
+ BrowserPolicy.content.allowSameOriginForAll();
+ BrowserPolicy.content.allowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
- BrowserPolicy.disallowInlineScripts();
- test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
+ BrowserPolicy.content.disallowInlineScripts();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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(),
+ BrowserPolicy.content.disallowAll();
+ BrowserPolicy.content.allowSameOriginForAll();
+ BrowserPolicy.content.disallowObject();
+ test.isTrue(cspsEqual(BrowserPolicy.content._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");
+ BrowserPolicy.framing._reset();
+ test.equal(BrowserPolicy.framing._constructXFrameOptions(), "SAMEORIGIN");
+ BrowserPolicy.framing.disallow();
+ test.equal(BrowserPolicy.framing._constructXFrameOptions(), "DENY");
+ BrowserPolicy.framing.allowAll();
+ test.equal(BrowserPolicy.framing._constructXFrameOptions(), null);
+ BrowserPolicy.framing.restrictToOrigin("foo.com");
+ test.equal(BrowserPolicy.framing._constructXFrameOptions(), "ALLOW-FROM foo.com");
test.throws(function () {
- BrowserPolicy.allowFramingByOrigin("bar.com");
+ BrowserPolicy.framing.restrictToOrigin("bar.com");
});
- BrowserPolicy.allowFramingByAnyOrigin();
- test.isFalse(BrowserPolicy._constructXFrameOptions());
});
280 packages/browser-policy/browser-policy.js
View
@@ -1,280 +0,0 @@
-// 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.restrictFramingToOrigin(origin)
-// BrowserPolicy.restrictFramingToSameOrigin()
-
-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;
- },
-
- restrictFramingToSameOrigin: function () {
- xFrameOptions = "SAMEORIGIN";
- },
- disallowFraming: function () {
- xFrameOptions = "DENY";
- },
- // ALLOW-FROM not supported in Chrome or Safari.
- restrictFramingToOrigin: 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;
- },
-
- // _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);
- };
- });
7 packages/browser-policy/package.js
View
@@ -3,12 +3,11 @@ Package.describe({
});
Package.on_use(function (api) {
- api.use(["underscore", "webapp"], "server");
- api.add_files("browser-policy.js", "server");
- api.export("BrowserPolicy", "server");
+ api.use(['browser-policy-content', 'browser-policy-framing'], 'server');
+ api.imply(['browser-policy-common'], 'server');
});
Package.on_test(function (api) {
- api.use(["tinytest", "browser-policy", "ejson"]);
+ api.use(["tinytest", "browser-policy", "ejson"], "server");
api.add_files("browser-policy-test.js", "server");
});
12 packages/webapp/webapp_server.js
View
@@ -231,8 +231,10 @@ var runWebAppServer = function () {
return;
}
- if (Package["browser-policy"] &&
- ! Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed() &&
+ var browserPolicyPackage = Package["browser-policy-common"];
+ if (browserPolicyPackage &&
+ browserPolicyPackage.BrowserPolicy.content &&
+ ! browserPolicyPackage.BrowserPolicy.content.inlineScriptsAllowed() &&
pathname === "/meteor_runtime_config.js") {
res.writeHead(200, { 'Content-type': 'application/javascript' });
res.write("__meteor_runtime_config__ = " +
@@ -401,8 +403,10 @@ var runWebAppServer = function () {
// 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()) {
+ var browserPolicyPackage = Package["browser-policy-common"];
+ if (! browserPolicyPackage ||
+ ! browserPolicyPackage.BrowserPolicy.content ||
+ browserPolicyPackage.BrowserPolicy.content.inlineScriptsAllowed()) {
boilerplateHtml = boilerplateHtml.replace(
/##RUNTIME_CONFIG##/,
"<script type='text/javascript'>__meteor_runtime_config__ = " +
Please sign in to comment.
Something went wrong with that request. Please try again.