added JSON constraints to protect clients from malicious or buggy servers

git-svn-id: https://svn.apache.org/repos/asf/chemistry/opencmis/trunk@1751876 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/JSONConstraints.java b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/JSONConstraints.java
new file mode 100644
index 0000000..3b8fcfe
--- /dev/null
+++ b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/JSONConstraints.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.chemistry.opencmis.commons.impl;
+
+public class JSONConstraints {
+
+    private JSONConstraints() {
+    }
+
+    public static final int MAX_OBJECT_SIZE;
+    public static final int MAX_ARRAY_SIZE;
+    public static final int MAX_DEPTH;
+
+    public static final int MAX_OBJECT_SIZE_DEFAULT = 10000;
+    public static final int MAX_ARRAY_SIZE_DEFAULT = 1000000;
+    public static final int MAX_DEPTH_DEFAULT = 200;
+
+    public static final String MAX_OBJECT_SIZE_SYSTEM_PROPERTY = "org.apache.chemistry.opencmis.JSONConstraints.maxObjectSize";
+    public static final String MAX_ARRAY_SIZE_SYSTEM_PROPERTY = "org.apache.chemistry.opencmis.JSONConstraints.maxArraySize";
+    public static final String MAX_DEPTH_SYSTEM_PROPERTY = "org.apache.chemistry.opencmis.JSONConstraints.maxDepth";
+
+    static {
+        int maxObjectSize = MAX_OBJECT_SIZE_DEFAULT;
+        try {
+            String maxObjectSizeStr = System.getProperty(MAX_OBJECT_SIZE_SYSTEM_PROPERTY);
+            if (maxObjectSizeStr != null) {
+                maxObjectSize = Integer.parseInt(maxObjectSizeStr);
+
+                // check for sane values
+                if (maxObjectSize < 100) {
+                    maxObjectSize = MAX_OBJECT_SIZE_DEFAULT;
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        MAX_OBJECT_SIZE = maxObjectSize;
+
+        int maxArraySize = MAX_ARRAY_SIZE_DEFAULT;
+        try {
+            String maxArraySizeStr = System.getProperty(MAX_ARRAY_SIZE_SYSTEM_PROPERTY);
+            if (maxArraySizeStr != null) {
+                maxArraySize = Integer.parseInt(maxArraySizeStr);
+
+                // check for sane values
+                if (maxArraySize < 1000) {
+                    maxArraySize = MAX_ARRAY_SIZE_DEFAULT;
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        MAX_ARRAY_SIZE = maxArraySize;
+
+        int maxDepth = MAX_DEPTH_DEFAULT;
+        try {
+            String maxDepthStr = System.getProperty(MAX_DEPTH_SYSTEM_PROPERTY);
+            if (maxDepthStr != null) {
+                maxDepth = Integer.parseInt(maxDepthStr);
+
+                // check for sane values
+                if (maxDepth < 10) {
+                    maxDepth = MAX_DEPTH_DEFAULT;
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        MAX_DEPTH = maxDepth;
+    }
+}
diff --git a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParseException.java b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParseException.java
index 06e2087..1aa6d6c 100644
--- a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParseException.java
+++ b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParseException.java
@@ -21,10 +21,10 @@
 /**
  * ParseException explains why and where the error occurs in source JSON text.
  * 
- * (Taken from JSON.simple <http://code.google.com/p/json-simple/> and modified
- * for OpenCMIS.)
+ * (Taken from JSON.simple &lt;http://code.google.com/p/json-simple/&gt; and
+ * modified for OpenCMIS.)
  * 
- * @author FangYidong<fangyidong@yahoo.com.cn>
+ * @author FangYidong&lt;fangyidong@yahoo.com.cn&gt;
  */
 public class JSONParseException extends Exception {
     private static final long serialVersionUID = -7880698968187728548L;
@@ -33,6 +33,7 @@
     public static final int ERROR_UNEXPECTED_TOKEN = 1;
     public static final int ERROR_UNEXPECTED_EXCEPTION = 2;
     public static final int ERROR_STRING_TOO_LONG = 3;
+    public static final int ERROR_JSON_TOO_BIG = 4;
 
     private int errorType;
     private Object unexpectedObject;
@@ -73,6 +74,9 @@
         case ERROR_STRING_TOO_LONG:
             sb.append("String too long");
             break;
+        case ERROR_JSON_TOO_BIG:
+            sb.append("JSON too big");
+            break;
         default:
             sb.append("Unkown error at position ").append(position).append('.');
             break;
diff --git a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParser.java b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParser.java
index 3f382d2..d47f85c 100644
--- a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParser.java
+++ b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/JSONParser.java
@@ -26,16 +26,17 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.chemistry.opencmis.commons.impl.JSONConstraints;
 import org.apache.chemistry.opencmis.commons.impl.json.JSONArray;
 import org.apache.chemistry.opencmis.commons.impl.json.JSONObject;
 
 /**
  * Parser for JSON text. Please note that JSONParser is NOT thread-safe.
  * 
- * (Taken from JSON.simple <http://code.google.com/p/json-simple/> and modified
- * for OpenCMIS.)
+ * (Taken from JSON.simple &lt;http://code.google.com/p/json-simple/&gt; and
+ * modified for OpenCMIS.)
  * 
- * @author FangYidong<fangyidong@yahoo.com.cn>
+ * @author FangYidong&lt;fangyidong@yahoo.com.cn&gt;
  */
 public class JSONParser {
     public static final int S_INIT = 0;
@@ -202,6 +203,9 @@
                         statusStack.removeFirst();
                         String key = (String) valueStack.removeFirst();
                         Map<String, Object> parent = (Map<String, Object>) valueStack.getFirst();
+                        if (parent.size() + 1 > JSONConstraints.MAX_OBJECT_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         parent.put(key, token.value);
                         status = peekStatus(statusStack);
                         break;
@@ -209,6 +213,12 @@
                         statusStack.removeFirst();
                         key = (String) valueStack.removeFirst();
                         parent = (Map<String, Object>) valueStack.getFirst();
+                        if (parent.size() + 1 > JSONConstraints.MAX_OBJECT_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
+                        if (valueStack.size() + 1 > JSONConstraints.MAX_DEPTH) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         List<Object> newArray = createArrayContainer(containerFactory);
                         parent.put(key, newArray);
                         status = S_IN_ARRAY;
@@ -219,6 +229,12 @@
                         statusStack.removeFirst();
                         key = (String) valueStack.removeFirst();
                         parent = (Map<String, Object>) valueStack.getFirst();
+                        if (parent.size() + 1 > JSONConstraints.MAX_OBJECT_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
+                        if (valueStack.size() + 1 > JSONConstraints.MAX_DEPTH) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         Map<String, Object> newObject = createObjectContainer(containerFactory);
                         parent.put(key, newObject);
                         status = S_IN_OBJECT;
@@ -236,6 +252,9 @@
                         break;
                     case Yytoken.TYPE_VALUE:
                         List<Object> val = (List<Object>) valueStack.getFirst();
+                        if (val.size() + 1 > JSONConstraints.MAX_ARRAY_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         val.add(token.value);
                         break;
                     case Yytoken.TYPE_RIGHT_SQUARE:
@@ -249,6 +268,12 @@
                         break;
                     case Yytoken.TYPE_LEFT_BRACE:
                         val = (List<Object>) valueStack.getFirst();
+                        if (val.size() + 1 > JSONConstraints.MAX_ARRAY_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
+                        if (valueStack.size() + 1 > JSONConstraints.MAX_DEPTH) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         Map<String, Object> newObject = createObjectContainer(containerFactory);
                         val.add(newObject);
                         status = S_IN_OBJECT;
@@ -257,6 +282,12 @@
                         break;
                     case Yytoken.TYPE_LEFT_SQUARE:
                         val = (List<Object>) valueStack.getFirst();
+                        if (val.size() + 1 > JSONConstraints.MAX_ARRAY_SIZE) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
+                        if (valueStack.size() + 1 > JSONConstraints.MAX_DEPTH) {
+                            throw new JSONParseException(JSONParseException.ERROR_JSON_TOO_BIG);
+                        }
                         List<Object> newArray = createArrayContainer(containerFactory);
                         val.add(newArray);
                         status = S_IN_ARRAY;
diff --git a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/Yylex.java b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/Yylex.java
index 9d58d45..e5c35d9 100644
--- a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/Yylex.java
+++ b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/json/parser/Yylex.java
@@ -22,8 +22,8 @@
 import java.math.BigInteger;
 
 /**
- * (Taken from JSON.simple <http://code.google.com/p/json-simple/> and modified
- * for OpenCMIS.)
+ * (Taken from JSON.simple &lt;http://code.google.com/p/json-simple/&gt; and
+ * modified for OpenCMIS.)
  */
 class Yylex {
 
diff --git a/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/json/JSONConstraintsTest.java b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/json/JSONConstraintsTest.java
new file mode 100644
index 0000000..e84a1a9
--- /dev/null
+++ b/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/json/JSONConstraintsTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.chemistry.opencmis.commons.impl.json;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.chemistry.opencmis.commons.impl.JSONConstraints;
+import org.apache.chemistry.opencmis.commons.impl.json.parser.JSONParseException;
+import org.apache.chemistry.opencmis.commons.impl.json.parser.JSONParser;
+import org.junit.Test;
+
+public class JSONConstraintsTest {
+
+    private enum JSONElement {
+        VALUE, OBJECT, ARRAY
+    };
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testObjectSize() {
+        JSONParser parser = new JSONParser();
+
+        for (JSONElement jsonElement : JSONElement.values()) {
+            JSONObject jsonObject1 = createObject(JSONConstraints.MAX_OBJECT_SIZE, jsonElement);
+            try {
+                Object parsedObject = parser.parse(jsonObject1.toJSONString());
+                assertNotNull(parsedObject);
+                assertTrue(parsedObject instanceof Map);
+                assertEquals(JSONConstraints.MAX_OBJECT_SIZE, ((Map) parsedObject).size());
+            } catch (JSONParseException e) {
+                fail();
+            }
+
+            JSONObject jsonObject2 = createObject(JSONConstraints.MAX_OBJECT_SIZE + 1, jsonElement);
+            try {
+                parser.parse(jsonObject2.toJSONString());
+                fail();
+            } catch (JSONParseException e) {
+                assertEquals(JSONParseException.ERROR_JSON_TOO_BIG, e.getErrorType());
+            }
+        }
+    }
+
+    private JSONObject createObject(int size, JSONElement jsonElement) {
+        JSONObject result = new JSONObject();
+
+        for (int i = 0; i < size; i++) {
+            switch (jsonElement) {
+            case VALUE:
+                result.put("key" + i, "value" + i);
+                break;
+            case OBJECT:
+                result.put("key" + i, new JSONObject());
+                break;
+            case ARRAY:
+                result.put("key" + i, new JSONArray());
+                break;
+            default:
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testArraySize() {
+        JSONParser parser = new JSONParser();
+
+        for (JSONElement jsonElement : JSONElement.values()) {
+            JSONArray jsonArray1 = createArray(JSONConstraints.MAX_ARRAY_SIZE, jsonElement);
+            try {
+                Object parsedObject = parser.parse(jsonArray1.toJSONString());
+                assertNotNull(parsedObject);
+                assertTrue(parsedObject instanceof List);
+                assertEquals(JSONConstraints.MAX_ARRAY_SIZE, ((List) parsedObject).size());
+            } catch (JSONParseException e) {
+                fail();
+            }
+
+            JSONArray jsonArray2 = createArray(JSONConstraints.MAX_ARRAY_SIZE + 1, jsonElement);
+            try {
+                parser.parse(jsonArray2.toJSONString());
+                fail();
+            } catch (JSONParseException e) {
+                assertEquals(JSONParseException.ERROR_JSON_TOO_BIG, e.getErrorType());
+            }
+        }
+    }
+
+    private JSONArray createArray(int size, JSONElement jsonElement) {
+        JSONArray result = new JSONArray();
+
+        for (int i = 0; i < size; i++) {
+            switch (jsonElement) {
+            case VALUE:
+                result.add("value" + i);
+                break;
+            case OBJECT:
+                result.add(new JSONObject());
+                break;
+            case ARRAY:
+                result.add(new JSONArray());
+                break;
+            default:
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    @Test
+    public void testDepth() {
+        JSONParser parser = new JSONParser();
+
+        JSONObject jsonObject1 = createDeepObject(JSONConstraints.MAX_DEPTH);
+        try {
+            parser.parse(jsonObject1.toJSONString());
+        } catch (JSONParseException e) {
+            fail();
+        }
+
+        JSONObject jsonObject2 = createDeepObject(JSONConstraints.MAX_DEPTH + 1);
+        try {
+            parser.parse(jsonObject2.toJSONString());
+            fail();
+        } catch (JSONParseException e) {
+            assertEquals(JSONParseException.ERROR_JSON_TOO_BIG, e.getErrorType());
+        }
+    }
+
+    private JSONObject createDeepObject(int depth) {
+        JSONObject result = new JSONObject();
+
+        JSONObject lastObject = result;
+        for (int i = 1; i < depth; i++) {
+            JSONObject newObject = new JSONObject();
+            lastObject.put("obj", newObject);
+            lastObject = newObject;
+        }
+
+        return result;
+    }
+}