Permalink
Browse files

Automated g4 rollback

*** Reason for rollback ***

Moved SanitizedContent itself (but not its subclasses) to Closure too
so that typechecking works.

*** Original change description ***

Automated g4 rollback

*** Reason for rollback ***

Broke Closure. 

*** Original change description ***

goog.soy to check that templates are either returning string or
SanitizedHtml and not other types.

Add an @define for projects to force Strict mode.

Move soydata.SanitizedContentKind definitions into Closure's Soy
directory goog.soy can depend on it.  Have soydata.SanitizedContentKind

... description truncated by g4 rollback ...

R=kai
DELTA=687  (482 added, 140 deleted, 65 changed)


Revision created by MOE tool push_codebase.
MOE_MIGRATION=5780


git-svn-id: http://closure-library.googlecode.com/svn/trunk@2280 0b95b8e8-c90f-11de-9d4f-f947ee5921c8
  • Loading branch information...
1 parent 33354d3 commit f49c45e5442f1020b801fe832926d8d9a5a14091 gboyer@google.com committed Nov 8, 2012
Showing with 305 additions and 8 deletions.
  1. +3 −2 closure/goog/deps.js
  2. +118 −0 closure/goog/soy/data.js
  3. +59 −6 closure/goog/soy/soy.js
  4. +78 −0 closure/goog/soy/soy_test.html
  5. +47 −0 closure/goog/soy/soy_test.js
View

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
View
@@ -0,0 +1,118 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Soy data primitives.
+ *
+ * The goal is to encompass data types used by Soy, especially to mark content
+ * as known to be "safe".
+ *
+ * @author gboyer@google.com (Garrett Boyer)
+ */
+
+goog.provide('goog.soy.data');
+goog.provide('goog.soy.data.SanitizedContent');
+goog.provide('goog.soy.data.SanitizedContentKind');
+
+
+/**
+ * A type of textual content.
+ *
+ * This is an enum of type Object so that these values are unforgeable.
+ *
+ * @enum {!Object}
+ */
+goog.soy.data.SanitizedContentKind = {
+
+ /**
+ * A snippet of HTML that does not start or end inside a tag, comment, entity,
+ * or DOCTYPE; and that does not contain any executable code
+ * (JS, {@code <object>}s, etc.) from a different trust domain.
+ */
+ HTML: goog.DEBUG ? {sanitizedContentKindHtml: true} : {},
+
+ /**
+ * A sequence of code units that can appear between quotes (either kind) in a
+ * JS program without causing a parse error, and without causing any side
+ * effects.
+ * <p>
+ * The content should not contain unescaped quotes, newlines, or anything else
+ * that would cause parsing to fail or to cause a JS parser to finish the
+ * string its parsing inside the content.
+ * <p>
+ * The content must also not end inside an escape sequence ; no partial octal
+ * escape sequences or odd number of '{@code \}'s at the end.
+ */
+ JS_STR_CHARS: goog.DEBUG ? {sanitizedContentJsStrChars: true} : {},
+
+ /** A properly encoded portion of a URI. */
+ URI: goog.DEBUG ? {sanitizedContentUri: true} : {},
+
+ /** An attribute name and value such as {@code dir="ltr"}. */
+ HTML_ATTRIBUTE: goog.DEBUG ? {sanitizedContentHtmlAttribute: true} : {},
+
+ // TODO: Consider separating rules, declarations, and values into
+ // separate types, but for simplicity, we'll treat explicitly blessed
+ // SanitizedContent as allowed in all of these contexts.
+ /**
+ * A CSS3 declaration, property, value or group of semicolon separated
+ * declarations.
+ */
+ CSS: goog.DEBUG ? {sanitizedContentCss: true} : {},
+
+ /**
+ * Unsanitized plain-text content.
+ *
+ * This is effectively the "null" entry of this enum, and is sometimes used
+ * to explicitly mark content that should never be used unescaped. Since any
+ * string is safe to use as text, being of ContentKind.TEXT makes no
+ * guarantees about its safety in any other context such as HTML.
+ */
+ TEXT: goog.DEBUG ? {sanitizedContentKindText: true} : {}
+};
+
+
+
+/**
+ * A string-like object that carries a content-type.
+ *
+ * IMPORTANT! Do not create these directly, nor instantiate the subclasses.
+ * Instead, use a trusted, centrally reviewed library as endorsed by your team
+ * to generate these objects. Otherwise, you risk accidentally creating
+ * SanitizedContent that is attacker-controlled and gets evaluated unescaped in
+ * templates.
+ *
+ * @param {string} content The assumed-sanitized string. Be careful!
+ * @constructor
+ */
+goog.soy.data.SanitizedContent = function(content) {
+ /**
+ * The textual content.
+ * @type {string}
+ */
+ this.content = content;
+};
+
+
+/**
+ * The context in which this content is safe from XSS attacks.
+ * @type {goog.soy.data.SanitizedContentKind}
+ */
+goog.soy.data.SanitizedContent.prototype.contentKind;
+
+
+/** @override */
+goog.soy.data.SanitizedContent.prototype.toString = function() {
+ return this.content;
+};
View
@@ -18,11 +18,24 @@
goog.provide('goog.soy');
+goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
+goog.require('goog.soy.data');
+/**
+ * @define {boolean} Whether to require all Soy templates to be "strict html".
+ * Soy templates that use strict autoescaping forbid noAutoescape along with
+ * many dangerous directives, and return a runtime type SanitizedContent that
+ * marks them as safe.
+ *
+ * If this flag is enabled, Soy templates will fail to render if a template
+ * returns plain text -- indicating it is a non-strict template.
+ */
+goog.soy.REQUIRE_STRICT_AUTOESCAPE = false;
+
/**
* Renders a Soy template and then set the output string as
@@ -37,9 +50,9 @@ goog.require('goog.dom.TagName');
*/
goog.soy.renderElement = function(element, template, opt_templateData,
opt_injectedData) {
- element.innerHTML = template(
+ element.innerHTML = goog.soy.verifyTemplateOutputSafe_(template(
opt_templateData || goog.soy.defaultTemplateData_, undefined,
- opt_injectedData);
+ opt_injectedData));
};
@@ -60,9 +73,9 @@ goog.soy.renderElement = function(element, template, opt_templateData,
goog.soy.renderAsFragment = function(template, opt_templateData,
opt_injectedData, opt_domHelper) {
var dom = opt_domHelper || goog.dom.getDomHelper();
- return dom.htmlToDocumentFragment(
+ return dom.htmlToDocumentFragment(goog.soy.verifyTemplateOutputSafe_(
template(opt_templateData || goog.soy.defaultTemplateData_,
- undefined, opt_injectedData));
+ undefined, opt_injectedData)));
};
@@ -83,9 +96,9 @@ goog.soy.renderAsElement = function(template, opt_templateData,
opt_injectedData, opt_domHelper) {
var dom = opt_domHelper || goog.dom.getDomHelper();
var wrapper = dom.createElement(goog.dom.TagName.DIV);
- wrapper.innerHTML = template(
+ wrapper.innerHTML = goog.soy.verifyTemplateOutputSafe_(template(
opt_templateData || goog.soy.defaultTemplateData_,
- undefined, opt_injectedData);
+ undefined, opt_injectedData));
// If the template renders as a single element, return it.
if (wrapper.childNodes.length == 1) {
@@ -101,6 +114,46 @@ goog.soy.renderAsElement = function(template, opt_templateData,
/**
+ * Verifies that a template result is "safe" to insert as HTML.
+ *
+ * Note if the template is non-strict autoescape, the guarantees here are very
+ * weak. It is recommended applications switch to requiring strict
+ * autoescaping over time.
+ *
+ * @param {*} templateResult The template result.
+ * @return {string} The assumed-safe HTML output string.
+ * @private
+ */
+goog.soy.verifyTemplateOutputSafe_ = function(templateResult) {
+ // Allow strings as long as strict autoescaping is not mandated. Note we
+ // allow everything that isn't an object, because some non-escaping templates
+ // end up returning non-strings if their only print statement is a
+ // non-escaped argument, plus some unit tests spoof templates.
+ // TODO(gboyer): Track down and fix these cases.
+ if (!goog.soy.REQUIRE_STRICT_AUTOESCAPE && !goog.isObject(templateResult)) {
+ return String(templateResult);
+ }
+
+ // Allow SanitizedContent of kind HTML.
+ if (templateResult instanceof goog.soy.data.SanitizedContent) {
+ templateResult = /** @type {!goog.soy.data.SanitizedContent} */ (
+ templateResult);
+ var ContentKind = goog.soy.data.SanitizedContentKind;
+ if (templateResult.contentKind === ContentKind.HTML ||
+ templateResult.contentKind === ContentKind.HTML_ATTRIBUTE) {
+ return goog.asserts.assertString(templateResult.content);
+ }
+ }
+
+ goog.asserts.fail('Soy template output is unsafe for use as HTML: ' +
+ templateResult);
+
+ // In production, return a safe string, rather than failing hard.
+ return 'zSoyz';
+};
+
+
+/**
* Immutable object that is passed into templates that are rendered
* without any data.
* @type {Object}
@@ -15,15 +15,27 @@
<script>
goog.require('goog.dom');
goog.require('goog.dom.TagName');
+ goog.require('goog.functions');
goog.require('goog.soy');
+ goog.require('goog.soy.data');
goog.require('goog.soy.testHelper');
goog.require('goog.string');
+ goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
</script>
</head>
<body>
<script>
+var stubs;
+
+function setUp() {
+ stubs = new goog.testing.PropertyReplacer();
+}
+
+function tearDown() {
+ stubs.reset();
+}
function testRenderElement() {
var testDiv = goog.dom.createElement(goog.dom.TagName.DIV);
@@ -116,6 +128,72 @@
assertEquals('Hello', elementToInnerHtml(elem));
}
+/**
+ * Asserts that the function throws an error for unsafe templates.
+ * @param {Function} function Callback to test.
+ */
+function assertUnsafeTemplateOutputErrorThrown(func) {
+ stubs.set(goog.asserts, 'ENABLE_ASSERTS', true);
+ assertContains('Soy template output is unsafe for use as HTML',
+ assertThrows(func).message);
+ stubs.set(goog.asserts, 'ENABLE_ASSERTS', false);
+ assertEquals('zSoyz', func());
+}
+
+function testRejectUnsanitizedText() {
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ var div = goog.dom.createElement(goog.dom.TagName.DIV);
+ goog.soy.renderElement(div,
+ example.unsanitizedTextTemplate);
+ return div.innerHTML;
+ });
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ // This will render to a text node containing only zSoyz.
+ var fragment = goog.soy.renderAsFragment(example.unsanitizedTextTemplate);
+ return fragment.nodeValue;
+ });
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ return goog.soy.renderAsElement(example.unsanitizedTextTemplate).innerHTML;
+ });
+}
+
+function testRejectSanitizedCss() {
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ goog.soy.renderAsElement(example.sanitizedCssTemplate);
+ });
+}
+
+function testRejectSanitizedCss() {
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ return goog.soy.renderAsElement(
+ example.templateSpoofingSanitizedContentString).innerHTML;
+ });
+}
+
+function testRejectStringTemplatesWhenModeIsSet() {
+ stubs.set(goog.soy, 'REQUIRE_STRICT_AUTOESCAPE', true);
+ assertUnsafeTemplateOutputErrorThrown(function() {
+ return goog.soy.renderAsElement(example.noDataTemplate).innerHTML;
+ });
+}
+
+function testAcceptSanitizedHtml() {
+ assertEquals('Hello World', goog.dom.getTextContent(
+ goog.soy.renderAsElement(example.sanitizedHtmlTemplate)));
+}
+
+function testAcceptSanitizedHtmlAttribute() {
+ assertEquals('Hello World', goog.dom.getTextContent(
+ goog.soy.renderAsElement(example.sanitizedHtmlAttributeTemplate)));
+}
+
+function testAcceptNonObject() {
+ // Some templates, or things that spoof templates in unit tests, might return
+ // non-strings in unusual cases.
+ assertEquals('null', goog.dom.getTextContent(
+ goog.soy.renderAsElement(goog.functions.constant(null))));
+}
+
</script>
</body>
</html>
Oops, something went wrong.

0 comments on commit f49c45e

Please sign in to comment.