Permalink
Browse files

debugged safe content

  • Loading branch information...
1 parent 4f165fc commit 5f3e59e4ba8bb80f8ba51f4dabe5704d02a9d6dd @mikesamuel committed Oct 25, 2011
@@ -134,6 +134,11 @@ private static boolean decodeEntityOnto(
= new ReplacementTable(REPLACEMENT_TABLE)
.add('&', null);
+ private static final ReplacementTable NORM_BASIC_REPLACEMENT_TABLE
+ = new ReplacementTable(NORM_REPLACEMENT_TABLE)
+ .add('\'', null)
+ .add('"', null);
+
/** escapeOnto escapes for inclusion in HTML text. */
static void escapeOnto(@Nullable Object o, Writer out) throws IOException {
String safe = ContentType.HTML.derefSafeContent(o);
@@ -180,8 +185,8 @@ static int filterNameOnto(@Nullable Object o, Writer out, int context)
throws IOException {
String safe = ContentType.HTMLAttr.derefSafeContent(o);
if (safe != null) {
- out.write(' ');
- out.write(safe);
+ if (Context.state(context) == Context.State.TagName) { out.write(' '); }
+ NORM_BASIC_REPLACEMENT_TABLE.escapeOnto(safe, out);
return context;
}
String s = ReplacementTable.toString(o);
@@ -156,29 +156,40 @@ static boolean isJSIdentPart(char c) {
static final ReplacementTable STR_REPLACEMENT_TABLE
= new ReplacementTable()
.add((char) 0, "\\0")
- .add('\t', "\\t")
- .add('\n', "\\n")
- .add('\u000b', "\\x0b") // "\v" == "v" on IE 6.
- .add('\f', "\\f")
- .add('\r', "\\r")
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
+ .add('`', "\\x60")
.add('"', "\\x22")
.add('&', "\\x26")
.add('\'', "\\x27")
+ // JS strings cannot contain embedded newlines. Escape all space chars.
+ // U+2028 and U+2029 handled below.
+ .add('\t', "\\t")
+ .add('\n', "\\n")
+ .add('\u000b', "\\x0b") // "\v" == "v" on IE 6.
+ .add('\f', "\\f")
+ .add('\r', "\\r")
+ // Prevent function calls even if they escape, and handle capturing
+ // groups when inherited by regex below.
+ .add('(', "\\(")
+ .add(')', "\\)")
+ // UTF-7 attack vector
.add('+', "\\x2b")
+ // Prevent embedded "</script"
.add('/', "\\/")
+ // Prevent embedded <!-- and -->
.add('<', "\\x3c")
.add('>', "\\x3e")
+ // Correctness.
.add('\\', "\\\\")
- .add('`', "\\x60")
+ // JavaScript specific newline chars.
.replaceNonAscii(new int[] { 0x2028, 0x2029 },
new String[] { "\\u2028", "\\u2029" });
/**
* STR_NORM_REPLACEMENT_TABLE is like STR_REPLACEMENT_TABLE but does not
* overencode existing escapes since this table has no entry for "\\".
*/
- private static final ReplacementTable STR_NORM_REPLACEMENT_TABLE
+ static final ReplacementTable STR_NORM_REPLACEMENT_TABLE
= new ReplacementTable(STR_REPLACEMENT_TABLE)
.add('\\', null);
@@ -193,19 +204,17 @@ protected void writeEmpty(Writer out) throws IOException {
out.write("(?:)");
}
}
+ .add('{', "\\{")
+ .add('|', "\\|")
+ .add('}', "\\}")
.add('$', "\\$")
- .add('(', "\\(")
- .add(')', "\\)")
.add('*', "\\*")
.add('-', "\\-")
.add('.', "\\.")
.add('?', "\\?")
.add('[', "\\[")
.add(']', "\\]")
- .add('^', "\\^")
- .add('{', "\\{")
- .add('|', "\\|")
- .add('}', "\\}");
+ .add('^', "\\^");
static void escapeStrOnto(@Nullable Object o, Writer out) throws IOException {
String safe = ContentType.JSStr.derefSafeContent(o);
@@ -291,7 +300,36 @@ void escape(@Nullable Object o, boolean protectBoundaries)
// merge into other tokens.
// Surrounding with parentheses might introduce call operators.
out.write(protectBoundaries ? " null " : "null");
- } else if (o instanceof JSONMarshaler) {
+ return;
+ }
+ if (o instanceof SafeContent) {
+ SafeContent ct = (SafeContent) o;
+ ContentType t = ct.getContentType();
+ switch (t) {
+ case JS:
+ if (protectBoundaries) { out.write(' '); }
+ out.write(ct.toString());
+ if (protectBoundaries) { out.write(' '); }
+ return;
+ case JSStr:
+ String s = ct.toString();
+ int trailingSlashes = 0;
+ for (int i = s.length(); --i >= 0; ++trailingSlashes) {
+ if (s.charAt(i) != '\\') { break; }
+ }
+ out.write('\'');
+ JS.STR_NORM_REPLACEMENT_TABLE.escapeOnto(s, out);
+ if ((trailingSlashes & 1) != 0) {
+ out.write('\\');
+ }
+ // If s ends with an incomplete escape sequence, complete it.
+ out.write('\'');
+ return;
+ default:
+ // Fall through to cases below.
+ }
+ }
+ if (o instanceof JSONMarshaler) {
String json = sanityCheckJSON(((JSONMarshaler) o).toJSON());
char ch0 = json.charAt(0); // sanityCheckJSON does not allow empty.
if (protectBoundaries && JS.isJSIdentPart(ch0)) { out.write(' '); }
@@ -29,11 +29,12 @@
* produce a valid hierarchical or opaque URL part.
*/
static void escapeOnto(boolean norm, Object o, Writer out)
- throws IOException {
+ throws IOException {
String s;
String safe = ContentType.URL.derefSafeContent(o);
if (safe != null) {
s = safe;
+ norm = true;
} else {
s = ReplacementTable.toString(o);
}
@@ -32,7 +32,8 @@
* results in the output
* <blockquote>
* {@code <b>I &lt;3 Ponies!</b>}
- * {@code <button onclick="foo({&#34;foo&#34;:&#34;\x22bar\x22&#34;:42})">}
+ * <code>&lt;button
+ * onclick="foo({&#34;foo&#34;:&#34;\x22bar\x22&#34;:42})"&gt;</code>
* </blockquote>
* The safe parts are treated as literal chunks of HTML/CSS/JS, and the unsafe
* parts are escaped to preserve security and least-surprise.
@@ -1167,7 +1167,7 @@ public final void testSafeWriter() throws Exception {
"bad dynamic attribute name 1",
// The value is interpreted consistent with the attribute name.
"<input {{\"onchange\"}}=\"{{\"doEvil()\"}}\">",
- "<input onchange=\"'doEvil()'\">"
+ "<input onchange=\"'doEvil\\(\\)'\">"
);
assertTemplateOutput(
"bad dynamic attribute name 2",
@@ -229,7 +229,7 @@ public final void testJSStrEscaper() throws Exception {
// From http://code.google.com/p/doctype/wiki/ArticleUtf7
assertEscapedStrChars(
"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
- "\\x2bADw-script\\x2bAD4-alert(1)\\x2bADw-\\/script\\x2bAD4-");
+ "\\x2bADw-script\\x2bAD4-alert\\(1\\)\\x2bADw-\\/script\\x2bAD4-");
// Invalid UTF-8 sequence
assertEscapedStrChars("foo\u00A0bar", "foo\u00A0bar");
}
@@ -294,7 +294,7 @@ public final void testEscapersOnLower7AndSelectHighCodepoints()
"jsStrEscaper",
"\\0\1\2\3\4\5\6\7\10\\t\\n\\x0b\\f\\r\16\17" +
"\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37" +
- " !\\x22#$%\\x26\\x27()*\\x2b,-.\\/" +
+ " !\\x22#$%\\x26\\x27\\(\\)*\\x2b,-.\\/" +
"0123456789:;\\x3c=\\x3e?" +
"@ABCDEFGHIJKLMNO" +
"PQRSTUVWXYZ[\\\\]^_" +
@@ -0,0 +1,201 @@
+// Copyright (C) 2011 Google Inc.
+//
+// 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.
+
+package com.google.autoesc;
+
+import java.io.StringWriter;
+import junit.framework.TestCase;
+
+public class SafeContentTest extends TestCase {
+
+ private static final Object[] INPUTS = {
+ "<b> \"foo%\" O'Reilly &bar;",
+ new SafeContentString("a[href =~ \"//example.com\"]#foo", ContentType.CSS),
+ new SafeContentString("Hello, <b>World</b> &amp;tc!", ContentType.HTML),
+ new SafeContentString("dir=\"ltr\" title=\"x<y\"", ContentType.HTMLAttr),
+ new SafeContentString("c && alert(\"Hello, World!\");", ContentType.JS),
+ new SafeContentString("Hello, World & O'Reilly\\x21", ContentType.JSStr),
+ new SafeContentString("greeting=H%69&addressee=(World)", ContentType.URL),
+ };
+
+ /** @param goldens correspond to INPUTS */
+ private void assertInterp(String tmpl, String... goldens) throws Exception {
+ assertEquals(INPUTS.length, goldens.length);
+ int prefixLen = tmpl.indexOf("{{.}}");
+ String prefix = tmpl.substring(0, prefixLen);
+ String suffix = tmpl.substring(prefixLen + 5);
+ for (int i = 0; i < INPUTS.length; ++i) {
+ Object input = INPUTS[i];
+ StringWriter buf = new StringWriter();
+ HTMLEscapingWriter w = new HTMLEscapingWriter(buf);
+ w.writeSafe(prefix);
+ w.write(input);
+ w.writeSafe(suffix);
+ w.close();
+ String actual = buf.toString();
+ actual = actual.substring(prefixLen, actual.length() - suffix.length());
+ String type = input.getClass().getSimpleName() +
+ (input instanceof SafeContent
+ ? " " + ((SafeContent) input).getContentType()
+ : "");
+ assertEquals("`" + tmpl + "` with " + type, goldens[i], actual);
+ }
+ }
+
+ public final void testSafeContentInterp() throws Exception {
+ // For each content sensitive escaper, see how it does on
+ // each of the typed strings above.
+ assertInterp("<style>{{.}} { color: blue }</style>",
+ "ZautoescZ",
+ // Allowed but not escaped.
+ "a[href =~ \"//example.com\"]#foo",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ");
+ assertInterp("<div style=\"{{.}}\">",
+ "ZautoescZ",
+ // Allowed and HTML escaped.
+ "a[href =~ &#34;//example.com&#34;]#foo",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ");
+ assertInterp("{{.}}",
+ "&lt;b&gt; &#34;foo%&#34; O&#39;Reilly &amp;bar;",
+ "a[href =~ &#34;//example.com&#34;]#foo",
+ // Not escaped.
+ "Hello, <b>World</b> &amp;tc!",
+ "dir=&#34;ltr&#34; title=&#34;x&lt;y&#34;",
+ "c &amp;&amp; alert(&#34;Hello, World!&#34;);",
+ "Hello, World &amp; O&#39;Reilly\\x21",
+ "greeting=H%69&amp;addressee=(World)");
+ assertInterp("<a{{.}}>",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ // Allowed and HTML escaped.
+ " dir=\"ltr\" title=\"x&lt;y\"",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ");
+ assertInterp("<a {{.}}>",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ",
+ // Allowed and HTML escaped.
+ "dir=\"ltr\" title=\"x&lt;y\"",
+ "ZautoescZ",
+ "ZautoescZ",
+ "ZautoescZ");
+ assertInterp("<a title={{.}}>",
+ "\"&lt;b&gt; &#34;foo%&#34; O'Reilly &amp;bar;\"",
+ "\"a[href =~ &#34;//example.com&#34;]#foo\"",
+ // Tags stripped, spaces escaped, entity not re-escaped.
+ "\"Hello, World &amp;tc!\"",
+ "\"dir=&#34;ltr&#34; title=&#34;x&lt;y&#34;\"",
+ "\"c &amp;&amp; alert(&#34;Hello, World!&#34;);\"",
+ "\"Hello, World &amp; O'Reilly\\x21\"",
+ "\"greeting=H%69&amp;addressee=(World)\"");
+ assertInterp("<a title='{{.}}'>",
+ "&lt;b&gt; \"foo%\" O&#39;Reilly &amp;bar;",
+ "a[href =~ \"//example.com\"]#foo",
+ // Tags stripped, entity not re-escaped.
+ "Hello, World &amp;tc!",
+ "dir=\"ltr\" title=\"x&lt;y\"",
+ "c &amp;&amp; alert(\"Hello, World!\");",
+ "Hello, World &amp; O&#39;Reilly\\x21",
+ "greeting=H%69&amp;addressee=(World)");
+ assertInterp("<textarea>{{.}}</textarea>",
+ "&lt;b&gt; &#34;foo%&#34; O&#39;Reilly &amp;bar;",
+ "a[href =~ &#34;//example.com&#34;]#foo",
+ // Angle brackets escaped to prevent injection of close tags, entity
+ // not re-escaped.
+ "Hello, &lt;b&gt;World&lt;/b&gt; &amp;tc!",
+ "dir=&#34;ltr&#34; title=&#34;x&lt;y&#34;",
+ "c &amp;&amp; alert(&#34;Hello, World!&#34;);",
+ "Hello, World &amp; O&#39;Reilly\\x21",
+ "greeting=H%69&amp;addressee=(World)");
+ assertInterp("<script>alert({{.}})</script>",
+ "'\\x3cb\\x3e \\x22foo%\\x22 O\\x27Reilly \\x26bar;'",
+ "'a[href =~ \\x22\\/\\/example.com\\x22]#foo'",
+ "'Hello, \\x3cb\\x3eWorld\\x3c\\/b\\x3e \\x26amp;tc!'",
+ "'dir=\\x22ltr\\x22 title=\\x22x\\x3cy\\x22'",
+ // Not escaped.
+ " c && alert(\"Hello, World!\"); ",
+ // Escape sequence not over-escaped.
+ "'Hello, World \\x26 O\\x27Reilly\\x21'",
+ "'greeting=H%69\\x26addressee=\\(World\\)'");
+ assertInterp("<button onclick=\"alert({{.}})\">",
+ "'\\x3cb\\x3e \\x22foo%\\x22 O\\x27Reilly \\x26bar;'",
+ "'a[href =~ \\x22\\/\\/example.com\\x22]#foo'",
+ "'Hello, \\x3cb\\x3eWorld\\x3c\\/b\\x3e \\x26amp;tc!'",
+ "'dir=\\x22ltr\\x22 title=\\x22x\\x3cy\\x22'",
+ // Not JS escaped but HTML escaped.
+ " c &amp;&amp; alert(&#34;Hello, World!&#34;); ",
+ // Escape sequence not over-escaped.
+ "'Hello, World \\x26 O\\x27Reilly\\x21'",
+ "'greeting=H%69\\x26addressee=\\(World\\)'");
+ assertInterp("<button onclick='alert({{.}})'>",
+ "&#39;\\x3cb\\x3e \\x22foo%\\x22 O\\x27Reilly \\x26bar;&#39;",
+ "&#39;a[href =~ \\x22\\/\\/example.com\\x22]#foo&#39;",
+ "&#39;Hello, \\x3cb\\x3eWorld\\x3c\\/b\\x3e \\x26amp;tc!&#39;",
+ "&#39;dir=\\x22ltr\\x22 title=\\x22x\\x3cy\\x22&#39;",
+ // Not JS escaped but HTML escaped.
+ " c &amp;&amp; alert(\"Hello, World!\"); ",
+ // Escape sequence not over-escaped.
+ "&#39;Hello, World \\x26 O\\x27Reilly\\x21&#39;",
+ "&#39;greeting=H%69\\x26addressee=\\(World\\)&#39;");
+ assertInterp("<script>alert(\"{{.}}\")</script>",
+ "\\x3cb\\x3e \\x22foo%\\x22 O\\x27Reilly \\x26bar;",
+ "a[href =~ \\x22\\/\\/example.com\\x22]#foo",
+ "Hello, \\x3cb\\x3eWorld\\x3c\\/b\\x3e \\x26amp;tc!",
+ "dir=\\x22ltr\\x22 title=\\x22x\\x3cy\\x22",
+ "c \\x26\\x26 alert\\(\\x22Hello, World!\\x22\\);",
+ // Escape sequence not over-escaped.
+ "Hello, World \\x26 O\\x27Reilly\\x21",
+ "greeting=H%69\\x26addressee=\\(World\\)");
+ assertInterp("<button onclick='alert(\"{{.}}\")'>",
+ "\\x3cb\\x3e \\x22foo%\\x22 O\\x27Reilly \\x26bar;",
+ "a[href =~ \\x22\\/\\/example.com\\x22]#foo",
+ "Hello, \\x3cb\\x3eWorld\\x3c\\/b\\x3e \\x26amp;tc!",
+ "dir=\\x22ltr\\x22 title=\\x22x\\x3cy\\x22",
+ "c \\x26\\x26 alert\\(\\x22Hello, World!\\x22\\);",
+ // Escape sequence not over-escaped.
+ "Hello, World \\x26 O\\x27Reilly\\x21",
+ "greeting=H%69\\x26addressee=\\(World\\)");
+ assertInterp("<a href=\"?q={{.}}\">",
+ "%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b",
+ "a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo",
+ "Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21",
+ "dir%3d%22ltr%22%20title%3d%22x%3cy%22",
+ "c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b",
+ "Hello%2c%20World%20%26%20O%27Reilly%5cx21",
+ // Quotes and parens are escaped but %69 is not over-escaped.
+ // HTML escaping is done.
+ "greeting=H%69&amp;addressee=%28World%29");
+ assertInterp("<style>body { background: url('?img={{.}}') }</style>",
+ "%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b",
+ "a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo",
+ "Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21",
+ "dir%3d%22ltr%22%20title%3d%22x%3cy%22",
+ "c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b",
+ "Hello%2c%20World%20%26%20O%27Reilly%5cx21",
+ // Quotes and parens are escaped but %69 is not over-escaped.
+ // HTML escaping is not done.
+ "greeting=H%69&addressee=%28World%29");
+ }
+}

0 comments on commit 5f3e59e

Please sign in to comment.