Revision: 3625
Author: metaweta
Date: Fri Jul 31 16:33:01 2009
Log: Restrict numeric whitelisting to nonnegative integers.
http://codereview.appspot.com/96200

* Changed CajitaRewriter.java and Rule.java to whitelist expressions of
  the form @o...@s & -1 <<< 1] instead of @o...@s]

* Changed cajita.js to check that index >= 0 when allowing numeric reads

* Added tests to CajitaRewriterTest and domita_test_untrusted.html to
  check that neither Rhino nor the browser return anything but undefined
  on reading a negative index.

* Updated structural tests in CajitaTest.java

[email protected]

http://code.google.com/p/google-caja/source/detail?r=3625

Modified:
 /trunk/src/com/google/caja/cajita.js
 /trunk/src/com/google/caja/parser/quasiliteral/CajitaRewriter.java
 /trunk/src/com/google/caja/parser/quasiliteral/Rule.java
 /trunk/tests/com/google/caja/parser/quasiliteral/CajitaRewriterTest.java
 /trunk/tests/com/google/caja/plugin/domita_test_untrusted.html

=======================================
--- /trunk/src/com/google/caja/cajita.js        Tue Jul 21 15:19:23 2009
+++ /trunk/src/com/google/caja/cajita.js        Fri Jul 31 16:33:01 2009
@@ -1374,7 +1374,7 @@
    * property of x, then canReadPub(y, 'foo') must be true.
    */
   function canReadPub(obj, name) {
-    if (typeof name === 'number') { return name in obj; }
+    if (typeof name === 'number' && name >= 0) { return name in obj; }
     name = String(name);
     if (obj === null) { return false; }
     if (obj === void 0) { return false; }
@@ -1388,7 +1388,7 @@
   }

   function hasOwnPropertyOf(obj, name) {
-    if (typeof name === 'number') { return hasOwnProp(obj, name); }
+ if (typeof name === 'number' && name >= 0) { return hasOwnProp(obj, name); }
     name = String(name);
     if (obj && obj[name + '_canRead___'] === obj) { return true; }
     return canReadPub(obj, name) && myOriginalHOP.call(obj, name);
@@ -1416,13 +1416,17 @@
    * If it can't then <tt>readPub</tt> returns <tt>undefined</tt> instead.
    */
   function readPub(obj, name) {
-    if (typeof name === 'number') {
+    if (typeof name === 'number' && name >= 0) {
       if (typeof obj === 'string') {
         // In partial anticipation of ES3.1.
         // TODO(erights): Once ES3.1 settles, revisit this and
         // correctly implement the agreed semantics.
-        // Mike Samuel suggests also making it conditional on
+        // Mike Samuel suggested also making it conditional on
         //  (+name) === (name & 0x7fffffff)
+        // but then realized that it violates the requirement
+        // that the string form be the canonical form of the
+        // number. So 'foo'['00'] would be treated the same
+        // as 'foo'['0'] which is incorrect.
         return obj.charAt(name);
       } else {
         return obj[name];
@@ -1466,7 +1470,7 @@
         return pumpkin;
       }
     }
-    if (typeof name === 'number') {
+    if (typeof name === 'number' && name >= 0) {
       if (myOriginalHOP.call(obj, name)) { return obj[name]; }
       return pumpkin;
     }
@@ -1764,6 +1768,7 @@
     // the check is expensive in this position.
 //  val = asFirstClass(val);
     if (typeof name === 'number' &&
+        name >= 0 &&
         // See issue 875
         obj instanceof Array &&
         obj.FROZEN___ !== obj) {
=======================================
--- /trunk/src/com/google/caja/parser/quasiliteral/CajitaRewriter.java Fri Jul 17 17:55:20 2009 +++ /trunk/src/com/google/caja/parser/quasiliteral/CajitaRewriter.java Fri Jul 31 16:33:01 2009
@@ -1048,14 +1048,15 @@
       @Override
       @RuleDescription(
           name="readNumPublic",
-          synopsis="Recognize that numeric indexing is inherently safe.",
+          synopsis="Recognize that array indexing is inherently safe.",
reason="When the developer knows that their index expression is" + - " numeric, they can indicate this with the unary plus operator" + - " -- which coerces to a number. Since numeric properties are" + - " necessarily readable, we can pass these through directly to" +
-              " JavaScript.",
-          matches="@o...@s]",
-          substitutes="@o...@s]")
+              " an array index, they can indicate this with the" +
+              " 'absolute value operator', really an expression which" +
+              " coerces to a nonnegative 32-bit integer. Since these" +
+              " properties are necessarily readable, we can pass them " +
+              " through directly to JavaScript.",
+          matches="@o...@s&(-1>>>1)]",
+          substitutes="@o...@s&(-1>>>1)]")
       public ParseTreeNode fire(
           ParseTreeNode node, Scope scope, MessageQueue mq) {
         return transform(node, scope, mq);
@@ -1066,9 +1067,9 @@
       @Override
       @RuleDescription(
           name="readNumWithConstantIndex",
-          synopsis="Recognize that numeric indexing is inherently safe.",
- reason="Numeric properties are always readable, we can pass these"
-              + " through directly to JavaScript.",
+          synopsis="Recognize that array indexing is inherently safe.",
+          reason="Nonnegative integer properties are always readable;" +
+              " we can pass these through directly to JavaScript.",
           matches="@o...@numliteral]",
           substitutes="@o...@numliteral]")
       public ParseTreeNode fire(
@@ -1077,9 +1078,14 @@
         if (bindings != null) {
           ParseTreeNode index = bindings.get("numLiteral");
           if (index instanceof NumberLiteral) {
-            return substV(
-                "o", expand(bindings.get("o"), scope, mq),
-                "numLiteral", expand(index, scope, mq));
+            double indexValue =
+                ((NumberLiteral) index).getValue().doubleValue();
+            if (indexValue >= 0 &&
+                indexValue == Math.floor(indexValue)) {
+              return substV(
+                  "o", expand(bindings.get("o"), scope, mq),
+                  "numLiteral", expand(index, scope, mq));
+            }
           }
         }
         return NONE;
@@ -1484,7 +1490,7 @@
               ops.getUncajoledLValue(), aNode.children().get(1));
           Operation assignment = ops.makeAssignment(rhs);
           return commas(newCommaOperation(ops.getTemporaries()),
-                       (Expression) expand(assignment, scope, mq));
+                        (Expression) expand(assignment, scope, mq));
         }
         return NONE;
       }
=======================================
--- /trunk/src/com/google/caja/parser/quasiliteral/Rule.java Wed May 20 11:46:54 2009 +++ /trunk/src/com/google/caja/parser/quasiliteral/Rule.java Fri Jul 31 16:33:01 2009
@@ -595,8 +595,10 @@
ParseTreeNode rightExpanded = rewriter.expand(uncajoledKey, scope, mq);
       Identifier tmpVar = scope.declareStartOfScopeTempVariable();
       key = new Reference(tmpVar);
-      if (isToNumberOp(rightExpanded)) {
- key = Operation.create(key.getFilePosition(), Operator.TO_NUMBER, key);
+      if (QuasiBuilder.match("@s&(-1>>>1)", rightExpanded)) {
+        // TODO(metaweta): Figure out a way to leave key alone and
+        // protect propertyAccess from rewriting instead.
+ key = (Expression) QuasiBuilder.substV("@key&(-1>>>1)", "key", key);
       }
       temporaries.add((Expression) QuasiBuilder.substV(
           "@tmpVar = @right;",
@@ -646,11 +648,6 @@
     if (!(e instanceof Reference)) { return false; }
return ReservedNames.IMPORTS.equals(((Reference) e).getIdentifierName());
   }
-
-  private static boolean isToNumberOp(ParseTreeNode n) {
-    return n instanceof Operation
-        && Operator.TO_NUMBER == ((Operation) n).getOperator();
-  }

   /**
    * The operands in a read/assign operation.
=======================================
--- /trunk/tests/com/google/caja/parser/quasiliteral/CajitaRewriterTest.java Thu Jul 23 09:16:30 2009 +++ /trunk/tests/com/google/caja/parser/quasiliteral/CajitaRewriterTest.java Fri Jul 31 16:33:01 2009
@@ -935,44 +935,44 @@
   public final void testReadNumPublic() throws Exception {
     checkSucceeds(
         "var p, q, x;" +
-        "p = q[+x];",
+        "p = q[x&(-1>>>1)];",
         "var p, q, x;" +
-        "p = q[+x];");
+        "p = q[x&(-1>>>1)];");
     // Make sure that setPub is used on assignment to test frozenness.
     checkSucceeds(
         "var p, q, x;" +
-        "q[+x] = p;",
+        "q[x&(-1>>>1)] = p;",
         "var p, q, x;" +
-        "___.setPub(q, +x, p);");
+        "___.setPub(q, x&(-1>>>1), p);");
     checkSucceeds(
         "var p, q;" +
-        "p[+q] += 2;",
+        "p[q&(-1>>>1)] += 2;",
         "var x0___;" +
         "var x1___;" +
         "var p, q;" +
         "x0___ = p," +
-        "x1___ = +q," +
-        "___.setPub(x0___, +x1___, x0___[+x1___] + 2);");
+        "x1___ = q&(-1>>>1)," +
+        "___.setPub(x0___, x1___&(-1>>>1), x0___[x1___&(-1>>>1)] + 2);");
     checkSucceeds(
         "var p, q;" +
-        "p[+q]--;",
+        "p[q&(-1>>>1)]--;",
         "var x0___;" +
         "var x1___;" +
         "var x2___;" +
         "var p, q;" +
         "x0___ = p," +
-        "x1___ = +q," +
-        "x2___ = +x0___[+x1___]," +
-        "___.setPub(x0___, +x1___, x2___ - 1), x2___;");
+        "x1___ = q&(-1>>>1)," +
+        "x2___ = +x0___[x1___&(-1>>>1)]," +
+        "___.setPub(x0___, x1___&(-1>>>1), x2___ - 1), x2___;");
     checkSucceeds(
         "var p, q;" +
-        "--p[+q];",
+        "--p[q&(-1>>>1)];",
         "var x0___;" +
         "var x1___;" +
         "var p, q;" +
         "x0___ = p," +
-        "x1___ = +q," +
-        "___.setPub(x0___, +x1___, x0___[+x1___] - 1);");
+        "x1___ = q&(-1>>>1)," +
+        "___.setPub(x0___, x1___&(-1>>>1), x0___[x1___&(-1>>>1)] - 1);");
   }

   public final void testNumWithConstantIndex() throws Exception {
=======================================
--- /trunk/tests/com/google/caja/plugin/domita_test_untrusted.html Thu Jul 30 19:16:24 2009 +++ /trunk/tests/com/google/caja/plugin/domita_test_untrusted.html Fri Jul 31 16:33:01 2009
@@ -360,6 +360,8 @@
   </table>
 </div>

+<p class="testcontainer" id="test-negative-indices" />
+
 <p class="testcontainer" id="test-document-body-appendChild"
>I should be the last element until something is appended to document.body</p>

@@ -2481,6 +2483,24 @@
   pass('test-row-cell');
 });

+jsunitRegister('testNegativeIndices',
+               function testNegativeIndices() {
+  // Under Firefox, there are many undocumented properties
+  // with negative indices; see
+ // http://www.thespanner.co.uk/2009/07/14/hidden-firefox-properties-revisited/ + // http://webreflection.blogspot.com/2009/06/javascript-arguments-weridness.html
+  var i;
+  function f(a, b, c) {}
+  (function () {
+    for (i = -1; i > -10; --i) {
+      assertEquals(f[i], void 0);
+      assertEquals((new RegExp('.'))[i], void 0);
+      assertEquals(arguments[i], void 0);
+    }
+  })();
+  pass('test-negative-indices');
+});
+
 jsunitRegister('testDocumentBodyAppendChild',
                function testDocumentBodyAppendChild() {
   var notWhitespaceRe = new RegExp('\\S');

Reply via email to