summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbauerb <bauerb@chromium.org>2015-07-13 04:05:44 -0700
committerCommit bot <commit-bot@chromium.org>2015-07-13 11:06:17 +0000
commitb1704d0c90a6eb1e4d5eb1e83976e5964ee378b7 (patch)
tree3fc9c84049911645710aa2a9bb5f06e38ed84b5d
parentf8633deb9da835a863a5f77bdc1c52ee701d8ae2 (diff)
downloadchromium_src-b1704d0c90a6eb1e4d5eb1e83976e5964ee378b7.zip
chromium_src-b1704d0c90a6eb1e4d5eb1e83976e5964ee378b7.tar.gz
chromium_src-b1704d0c90a6eb1e4d5eb1e83976e5964ee378b7.tar.bz2
Add a JSON sanitizer.
On Android, the JSON sanitizer parses the untrusted JSON in Java and checks it for wellformedness before serializing it again. On desktop platforms it uses SafeJsonParser to parse the JSON out of process, then serializes it again. Also, on Android (where JsonSanitizer is not implemented in terms of the SafeJsonParser) the SafeJsonParser now uses the JsonSanitizer to sanitize its input before parsing it directly in process, instead of starting a new process, which is very costly on Android. BUG=501333 Review URL: https://codereview.chromium.org/1203083002 Cr-Commit-Position: refs/heads/master@{#338495}
-rw-r--r--chrome/android/BUILD.gn1
-rw-r--r--chrome/browser/android/chrome_jni_registrar.cc2
-rw-r--r--chrome/browser/supervised_user/supervised_user_whitelist_service_unittest.cc7
-rw-r--r--chrome/chrome.gyp3
-rw-r--r--chrome/test/base/chrome_test_suite.cc10
-rw-r--r--components/BUILD.gn5
-rw-r--r--components/components_tests.gyp7
-rw-r--r--components/safe_json.gypi52
-rw-r--r--components/safe_json/BUILD.gn38
-rw-r--r--components/safe_json/DEPS6
-rw-r--r--components/safe_json/android/BUILD.gn22
-rw-r--r--components/safe_json/android/component_jni_registrar.cc25
-rw-r--r--components/safe_json/android/component_jni_registrar.h19
-rw-r--r--components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java178
-rw-r--r--components/safe_json/json_sanitizer.cc89
-rw-r--r--components/safe_json/json_sanitizer.h52
-rw-r--r--components/safe_json/json_sanitizer_android.cc115
-rw-r--r--components/safe_json/json_sanitizer_unittest.cc184
-rw-r--r--components/safe_json/safe_json_parser.cc22
-rw-r--r--components/safe_json/safe_json_parser_android.cc56
-rw-r--r--components/safe_json/safe_json_parser_android.h41
-rw-r--r--components/test/DEPS1
-rw-r--r--components/test/run_all_unittests.cc8
-rw-r--r--tools/android/eclipse/.classpath1
24 files changed, 931 insertions, 13 deletions
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index b5265a6..8e29a4b 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -126,6 +126,7 @@ android_library("chrome_java") {
"//components/gcm_driver/android:gcm_driver_java",
"//components/invalidation/impl:java",
"//components/navigation_interception/android:navigation_interception_java",
+ "//components/safe_json/android:safe_json_java",
"//components/service_tab_launcher:service_tab_launcher_java",
"//components/policy/android:policy_java",
"//components/precache/android:precache_java",
diff --git a/chrome/browser/android/chrome_jni_registrar.cc b/chrome/browser/android/chrome_jni_registrar.cc
index 416214c..3c4b02b 100644
--- a/chrome/browser/android/chrome_jni_registrar.cc
+++ b/chrome/browser/android/chrome_jni_registrar.cc
@@ -132,6 +132,7 @@
#include "components/gcm_driver/android/component_jni_registrar.h"
#include "components/invalidation/impl/android/component_jni_registrar.h"
#include "components/navigation_interception/component_jni_registrar.h"
+#include "components/safe_json/android/component_jni_registrar.h"
#include "components/service_tab_launcher/component_jni_registrar.h"
#include "components/variations/android/component_jni_registrar.h"
#include "components/web_contents_delegate_android/component_jni_registrar.h"
@@ -153,6 +154,7 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = {
{"Invalidation", invalidation::android::RegisterInvalidationJni},
{"NavigationInterception",
navigation_interception::RegisterNavigationInterceptionJni},
+ {"SafeJson", safe_json::android::RegisterSafeJsonJni},
{"WebContentsDelegateAndroid",
web_contents_delegate_android::RegisterWebContentsDelegateAndroidJni},
// Register JNI for chrome classes.
diff --git a/chrome/browser/supervised_user/supervised_user_whitelist_service_unittest.cc b/chrome/browser/supervised_user/supervised_user_whitelist_service_unittest.cc
index 5a7ef97..1ae9984 100644
--- a/chrome/browser/supervised_user/supervised_user_whitelist_service_unittest.cc
+++ b/chrome/browser/supervised_user/supervised_user_whitelist_service_unittest.cc
@@ -21,13 +21,16 @@
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
-#include "components/safe_json/testing_json_parser.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "sync/api/sync_change.h"
#include "sync/api/sync_error_factory.h"
#include "sync/protocol/sync.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
+#if !defined(OS_ANDROID)
+#include "components/safe_json/testing_json_parser.h"
+#endif
+
namespace {
const char kClientId[] = "client-id";
@@ -152,7 +155,9 @@ class SupervisedUserWhitelistServiceTest : public testing::Test {
content::TestBrowserThreadBundle thread_bundle_;
TestingProfile profile_;
+#if !defined(OS_ANDROID)
safe_json::TestingJsonParser::ScopedFactoryOverride factory_override_;
+#endif
scoped_ptr<MockSupervisedUserWhitelistInstaller> installer_;
scoped_ptr<SupervisedUserWhitelistService> service_;
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 6d10f0d..f0bcc0a 100644
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -598,9 +598,10 @@
'../components/components.gyp:gcm_driver_java',
'../components/components.gyp:invalidation_java',
'../components/components.gyp:navigation_interception_java',
- '../components/components.gyp:service_tab_launcher_java',
'../components/components.gyp:policy_java',
'../components/components.gyp:precache_java',
+ '../components/components.gyp:safe_json_java',
+ '../components/components.gyp:service_tab_launcher_java',
'../components/components.gyp:variations_java',
'../components/components.gyp:web_contents_delegate_android_java',
'../content/content.gyp:content_java',
diff --git a/chrome/test/base/chrome_test_suite.cc b/chrome/test/base/chrome_test_suite.cc
index 933d6a0..6581d13 100644
--- a/chrome/test/base/chrome_test_suite.cc
+++ b/chrome/test/base/chrome_test_suite.cc
@@ -22,6 +22,11 @@
#include "extensions/common/constants.h"
#include "testing/gtest/include/gtest/gtest.h"
+#if defined(OS_ANDROID)
+#include "base/android/jni_android.h"
+#include "chrome/browser/android/chrome_jni_registrar.h"
+#endif
+
#if defined(OS_CHROMEOS)
#include "base/process/process_metrics.h"
#include "chromeos/chromeos_paths.h"
@@ -77,6 +82,11 @@ void ChromeTestSuite::Initialize() {
PathService::Override(base::DIR_MODULE, browser_dir_);
}
+#if defined(OS_ANDROID)
+ ASSERT_TRUE(chrome::android::RegisterBrowserJNI(
+ base::android::AttachCurrentThread()));
+#endif
+
#if !defined(OS_IOS)
// Disable external libraries load if we are under python process in
// ChromeOS. That means we are autotest and, if ASAN is used,
diff --git a/components/BUILD.gn b/components/BUILD.gn
index ae681a1..591d2fd 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -349,7 +349,10 @@ test("components_unittests") {
}
if (!is_ios) {
- deps += [ "//components/scheduler:unit_tests" ]
+ deps += [
+ "//components/safe_json:unit_tests",
+ "//components/scheduler:unit_tests",
+ ]
}
}
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index d06e4ce..dddb001 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -489,6 +489,9 @@
'rappor/rappor_utils_unittest.cc',
'rappor/sampler_unittest.cc',
],
+ 'safe_json_unittest_sources': [
+ 'safe_json/json_sanitizer_unittest.cc',
+ ],
'scheduler_unittest_sources': [
'scheduler/child/idle_helper_unittest.cc',
'scheduler/child/nestable_task_runner_for_test.cc',
@@ -900,6 +903,7 @@
'<@(navigation_interception_unittest_sources)',
'<@(network_hints_unittest_sources)',
'<@(power_unittest_sources)',
+ '<@(safe_json_unittest_sources)',
'<@(scheduler_unittest_sources)',
'<@(storage_monitor_unittest_sources)',
'<@(ui_unittest_sources)',
@@ -929,6 +933,8 @@
'components.gyp:password_manager_content_common',
'components.gyp:power',
'components.gyp:precache_content',
+ 'components.gyp:safe_json',
+ 'components.gyp:safe_json_test_support',
'components.gyp:sessions_content',
'components.gyp:storage_monitor',
'components.gyp:storage_monitor_test_support',
@@ -1061,6 +1067,7 @@
'dependencies': [
'components.gyp:cronet_static',
'components.gyp:data_reduction_proxy_content',
+ 'components.gyp:safe_json_java',
'../content/content.gyp:content_java',
'../testing/android/native_test.gyp:native_test_native_code',
],
diff --git a/components/safe_json.gypi b/components/safe_json.gypi
index dcf2824..bda8494 100644
--- a/components/safe_json.gypi
+++ b/components/safe_json.gypi
@@ -13,17 +13,37 @@
'../base/base.gyp:base',
'../components/components_strings.gyp:components_strings',
'../content/content.gyp:content_browser',
+ '../content/content.gyp:content_common',
'../ui/base/ui_base.gyp:ui_base',
],
'include_dirs': [
'..',
],
'sources': [
+ 'safe_json/android/component_jni_registrar.cc',
+ 'safe_json/android/component_jni_registrar.h',
+ 'safe_json/json_sanitizer.cc',
+ 'safe_json/json_sanitizer.h',
+ 'safe_json/json_sanitizer_android.cc',
'safe_json/safe_json_parser.cc',
'safe_json/safe_json_parser.h',
+ 'safe_json/safe_json_parser_android.cc',
+ 'safe_json/safe_json_parser_android.h',
'safe_json/safe_json_parser_impl.cc',
'safe_json/safe_json_parser_impl.h',
],
+ 'conditions': [
+ ['OS == "android"', {
+ 'dependencies': [
+ 'safe_json_jni_headers',
+ ],
+ 'sources!': [
+ 'safe_json/json_sanitizer.cc',
+ 'safe_json/safe_json_parser_impl.cc',
+ 'safe_json/safe_json_parser_impl.h',
+ ],
+ }],
+ ],
},
{
'target_name': 'safe_json_test_support',
@@ -45,7 +65,7 @@
'type': 'static_library',
'dependencies': [
'../base/base.gyp:base',
- '../content/content.gyp:content_browser',
+ '../content/content.gyp:content_utility',
'../ipc/ipc.gyp:ipc',
],
'include_dirs': [
@@ -59,4 +79,34 @@
],
},
],
+ 'conditions': [
+ ['OS=="android"', {
+ 'targets': [
+ {
+ # GN version: //components/safe_json/android:safe_json_java
+ 'target_name': 'safe_json_java',
+ 'type': 'none',
+ 'dependencies': [
+ '../base/base.gyp:base',
+ ],
+ 'variables': {
+ 'java_in_dir': 'safe_json/android/java',
+ },
+ 'includes': [ '../build/java.gypi' ],
+ },
+ {
+ # GN version: //components/safe_json:jni
+ 'target_name': 'safe_json_jni_headers',
+ 'type': 'none',
+ 'sources': [
+ 'safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java',
+ ],
+ 'variables': {
+ 'jni_gen_package': 'safe_json',
+ },
+ 'includes': [ '../build/jni_generator.gypi' ],
+ },
+ ],
+ }],
+ ]
}
diff --git a/components/safe_json/BUILD.gn b/components/safe_json/BUILD.gn
index a57efcc..2748f60 100644
--- a/components/safe_json/BUILD.gn
+++ b/components/safe_json/BUILD.gn
@@ -2,11 +2,22 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+if (is_android) {
+ import("//build/config/android/rules.gni")
+}
+
# GYP version: components/safe_json.gypi:safe_json
source_set("safe_json") {
sources = [
+ "android/component_jni_registrar.cc",
+ "android/component_jni_registrar.h",
+ "json_sanitizer.cc",
+ "json_sanitizer.h",
+ "json_sanitizer_android.cc",
"safe_json_parser.cc",
"safe_json_parser.h",
+ "safe_json_parser_android.cc",
+ "safe_json_parser_android.h",
"safe_json_parser_impl.cc",
"safe_json_parser_impl.h",
]
@@ -18,6 +29,30 @@ source_set("safe_json") {
"//content/public/browser",
"//ui/base",
]
+
+ if (is_android) {
+ sources -= [
+ "json_sanitizer.cc",
+ "safe_json_parser_impl.cc",
+ "safe_json_parser_impl.h",
+ ]
+ deps += [ "android:safe_json_jni_headers" ]
+ }
+}
+
+# GYP version: components/safe_json.gypi:safe_json_unittest_sources
+source_set("unit_tests") {
+ testonly = true
+ sources = [
+ "json_sanitizer_unittest.cc",
+ ]
+
+ deps = [
+ ":safe_json",
+ ":test_support",
+ "//base",
+ "//testing/gtest",
+ ]
}
# GYP version: components/safe_json.gypi:safe_json_test_support
@@ -45,7 +80,8 @@ source_set("safe_json_parser_message_filter") {
deps = [
"//base",
- "//content/public/browser",
+ "//content/public/common",
+ "//content/public/utility",
"//ipc",
]
}
diff --git a/components/safe_json/DEPS b/components/safe_json/DEPS
index 4e771fc..478fc1a 100644
--- a/components/safe_json/DEPS
+++ b/components/safe_json/DEPS
@@ -1,8 +1,10 @@
include_rules = [
"+base",
- "+content/public",
+ "+content/public/browser",
+ "+content/public/common",
+ "+content/public/utility",
"+grit", # For generated headers
- "+grit/components_strings.h", # For generated headers
"+ipc",
+ "+jni",
"+ui/base",
]
diff --git a/components/safe_json/android/BUILD.gn b/components/safe_json/android/BUILD.gn
new file mode 100644
index 0000000..bfb462d
--- /dev/null
+++ b/components/safe_json/android/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+_jni_sources =
+ [ "java/src/org/chromium/components/safejson/JsonSanitizer.java" ]
+
+# GYP: //components/safe_json.gypi:safe_json_jni_headers
+generate_jni("safe_json_jni_headers") {
+ sources = _jni_sources
+ jni_package = "safe_json"
+}
+
+# GYP: //components/safe_json.gypi:safe_json_java
+android_library("safe_json_java") {
+ deps = [
+ "//base:base_java",
+ ]
+ java_files = [] + _jni_sources
+}
diff --git a/components/safe_json/android/component_jni_registrar.cc b/components/safe_json/android/component_jni_registrar.cc
new file mode 100644
index 0000000..f1f937b
--- /dev/null
+++ b/components/safe_json/android/component_jni_registrar.cc
@@ -0,0 +1,25 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_json/android/component_jni_registrar.h"
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_registrar.h"
+#include "base/basictypes.h"
+#include "components/safe_json/json_sanitizer.h"
+
+namespace safe_json {
+namespace android {
+
+static base::android::RegistrationMethod kSafeJsonRegisteredMethods[] = {
+ {"JsonSanitizer", JsonSanitizer::Register},
+};
+
+bool RegisterSafeJsonJni(JNIEnv* env) {
+ return base::android::RegisterNativeMethods(
+ env, kSafeJsonRegisteredMethods, arraysize(kSafeJsonRegisteredMethods));
+}
+
+} // namespace android
+} // namespace safe_json
diff --git a/components/safe_json/android/component_jni_registrar.h b/components/safe_json/android/component_jni_registrar.h
new file mode 100644
index 0000000..8417783
--- /dev/null
+++ b/components/safe_json/android/component_jni_registrar.h
@@ -0,0 +1,19 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAFE_JSON_ANDROID_COMPONENT_JNI_REGISTRAR_H_
+#define COMPONENTS_SAFE_JSON_ANDROID_COMPONENT_JNI_REGISTRAR_H_
+
+#include <jni.h>
+
+namespace safe_json {
+namespace android {
+
+// Register all JNI bindings necessary for the safe_json component.
+bool RegisterSafeJsonJni(JNIEnv* env);
+
+} // namespace android
+} // namespace safe_json
+
+#endif // COMPONENTS_SAFE_JSON_ANDROID_COMPONENT_JNI_REGISTRAR_H_
diff --git a/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java b/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java
new file mode 100644
index 0000000..1fae2fd
--- /dev/null
+++ b/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java
@@ -0,0 +1,178 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.safejson;
+
+import android.util.JsonReader;
+import android.util.JsonToken;
+import android.util.JsonWriter;
+import android.util.MalformedJsonException;
+
+import org.chromium.base.CalledByNative;
+import org.chromium.base.JNINamespace;
+import org.chromium.base.StreamUtil;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+/**
+ * Sanitizes and normalizes a JSON string by parsing it, checking for wellformedness, and
+ * serializing it again. This class is meant to be used from native code.
+ */
+@JNINamespace("safe_json")
+public class JsonSanitizer {
+
+ // Disallow instantiating the class.
+ private JsonSanitizer() {
+ }
+
+ /**
+ * The maximum nesting depth to which the native JSON parser restricts input in order to avoid
+ * stack overflows.
+ */
+ private static final int MAX_NESTING_DEPTH = 100;
+
+ @CalledByNative
+ public static void sanitize(long nativePtr, String unsafeJson) {
+ JsonReader reader = new JsonReader(new StringReader(unsafeJson));
+ StringWriter stringWriter = new StringWriter(unsafeJson.length());
+ JsonWriter writer = new JsonWriter(stringWriter);
+ StackChecker stackChecker = new StackChecker();
+ try {
+ boolean end = false;
+ while (!end) {
+ JsonToken token = reader.peek();
+ switch (token) {
+ case BEGIN_ARRAY:
+ stackChecker.increaseAndCheck();
+ reader.beginArray();
+ writer.beginArray();
+ break;
+ case END_ARRAY:
+ stackChecker.decrease();
+ reader.endArray();
+ writer.endArray();
+ break;
+ case BEGIN_OBJECT:
+ stackChecker.increaseAndCheck();
+ reader.beginObject();
+ writer.beginObject();
+ break;
+ case END_OBJECT:
+ stackChecker.decrease();
+ reader.endObject();
+ writer.endObject();
+ break;
+ case NAME:
+ writer.name(sanitizeString(reader.nextName()));
+ break;
+ case STRING:
+ writer.value(sanitizeString(reader.nextString()));
+ break;
+ case NUMBER: {
+ // Read the value as a string, then try to parse it first as a long, then as
+ // a double.
+ String value = reader.nextString();
+ try {
+ writer.value(Long.parseLong(value));
+ } catch (NumberFormatException e) {
+ writer.value(Double.parseDouble(value));
+ }
+ break;
+ }
+ case BOOLEAN:
+ writer.value(reader.nextBoolean());
+ break;
+ case NULL:
+ reader.nextNull();
+ writer.nullValue();
+ break;
+ case END_DOCUMENT:
+ end = true;
+ break;
+ }
+ }
+ } catch (IOException | IllegalStateException e) {
+ nativeOnError(nativePtr, e.getMessage());
+ return;
+ } finally {
+ StreamUtil.closeQuietly(reader);
+ StreamUtil.closeQuietly(writer);
+ }
+ nativeOnSuccess(nativePtr, stringWriter.toString());
+ }
+
+ /**
+ * Helper class to check nesting depth of JSON expressions.
+ */
+ private static class StackChecker {
+ private int mStackDepth = 0;
+
+ public void increaseAndCheck() {
+ if (++mStackDepth >= MAX_NESTING_DEPTH) {
+ throw new IllegalStateException("Too much nesting");
+ }
+ }
+
+ public void decrease() {
+ mStackDepth--;
+ }
+ }
+
+ private static String sanitizeString(String string) throws MalformedJsonException {
+ if (!checkString(string)) {
+ throw new MalformedJsonException("Invalid escape sequence");
+ }
+ return string;
+ }
+
+ /**
+ * Checks whether a given String is well-formed UTF-16, i.e. all surrogates appear in high-low
+ * pairs and each code point is a valid character.
+ *
+ * @param string The string to check.
+ * @return Whether the given string is well-formed UTF-16.
+ */
+ private static boolean checkString(String string) {
+ int length = string.length();
+ for (int i = 0; i < length; i++) {
+ char c = string.charAt(i);
+ // Check that surrogates only appear in pairs of a high surrogate followed by a low
+ // surrogate.
+ // A lone low surrogate is not allowed.
+ if (Character.isLowSurrogate(c)) return false;
+
+ int codePoint;
+ if (Character.isHighSurrogate(c)) {
+ // A high surrogate has to be followed by a low surrogate.
+ char high = c;
+ if (++i >= length) return false;
+
+ char low = string.charAt(i);
+ if (!Character.isLowSurrogate(low)) return false;
+
+ // Decode the high-low pair into a code point.
+ codePoint = Character.toCodePoint(high, low);
+ } else {
+ // The code point is neither a low surrogate nor a high surrogate, so we just need
+ // to check that it's a valid character.
+ codePoint = c;
+ }
+
+ if (!isUnicodeCharacter(codePoint)) return false;
+ }
+ return true;
+ }
+
+ private static boolean isUnicodeCharacter(int codePoint) {
+ // See the native method base::IsValidCharacter().
+ return codePoint < 0xD800 || (codePoint >= 0xE000 && codePoint < 0xFDD0)
+ || (codePoint > 0xFDEF && codePoint <= 0x10FFFF && (codePoint & 0xFFFE) != 0xFFFE);
+ }
+
+ private static native void nativeOnSuccess(long id, String json);
+
+ private static native void nativeOnError(long id, String error);
+}
diff --git a/components/safe_json/json_sanitizer.cc b/components/safe_json/json_sanitizer.cc
new file mode 100644
index 0000000..e00043e
--- /dev/null
+++ b/components/safe_json/json_sanitizer.cc
@@ -0,0 +1,89 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_json/json_sanitizer.h"
+
+#if defined(OS_ANDROID)
+#error Build json_sanitizer_android.cc instead of this file on Android.
+#endif
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/json/json_writer.h"
+#include "base/memory/weak_ptr.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "components/safe_json/safe_json_parser.h"
+
+namespace safe_json {
+
+namespace {
+
+class OopJsonSanitizer : public JsonSanitizer {
+ public:
+ OopJsonSanitizer(const std::string& unsafe_json,
+ const StringCallback& success_callback,
+ const StringCallback& error_callback);
+
+ private:
+ friend struct base::DefaultDeleter<OopJsonSanitizer>;
+ ~OopJsonSanitizer() {}
+
+ void OnParseSuccess(scoped_ptr<base::Value> value);
+ void OnParseError(const std::string& error);
+
+ StringCallback success_callback_;
+ StringCallback error_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(OopJsonSanitizer);
+};
+
+OopJsonSanitizer::OopJsonSanitizer(const std::string& unsafe_json,
+ const StringCallback& success_callback,
+ const StringCallback& error_callback)
+ : success_callback_(success_callback), error_callback_(error_callback) {
+ SafeJsonParser::Parse(unsafe_json,
+ base::Bind(&OopJsonSanitizer::OnParseSuccess,
+ base::Unretained(this)),
+ base::Bind(&OopJsonSanitizer::OnParseError,
+ base::Unretained(this)));
+}
+
+void OopJsonSanitizer::OnParseSuccess(scoped_ptr<base::Value> value) {
+ // Self-destruct at the end of this method.
+ scoped_ptr<OopJsonSanitizer> deleter(this);
+
+ // A valid JSON document may only have a dictionary or list as its top-level
+ // type, but the JSON parser also accepts other types, so we filter them out.
+ base::Value::Type type = value->GetType();
+ if (type != base::Value::TYPE_DICTIONARY && type != base::Value::TYPE_LIST) {
+ error_callback_.Run("Invalid top-level type");
+ return;
+ }
+
+ std::string json;
+ if (!base::JSONWriter::Write(*value, &json)) {
+ error_callback_.Run("Encoding error");
+ return;
+ }
+
+ success_callback_.Run(json);
+}
+
+void OopJsonSanitizer::OnParseError(const std::string& error) {
+ error_callback_.Run("Parse error: " + error);
+ delete this;
+}
+
+} // namespace
+
+// static
+void JsonSanitizer::Sanitize(const std::string& unsafe_json,
+ const StringCallback& success_callback,
+ const StringCallback& error_callback) {
+ // OopJsonSanitizer destroys itself when it is finished.
+ new OopJsonSanitizer(unsafe_json, success_callback, error_callback);
+}
+
+} // namespace safe_json
diff --git a/components/safe_json/json_sanitizer.h b/components/safe_json/json_sanitizer.h
new file mode 100644
index 0000000..3f393d5
--- /dev/null
+++ b/components/safe_json/json_sanitizer.h
@@ -0,0 +1,52 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAFE_JSON_JSON_SANITIZER_H_
+#define COMPONENTS_SAFE_JSON_JSON_SANITIZER_H_
+
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/compiler_specific.h"
+#include "base/memory/scoped_ptr.h"
+
+#if defined(OS_ANDROID)
+#include <jni.h>
+#endif
+
+namespace safe_json {
+
+// Sanitizes and normalizes JSON by parsing it in a safe environment and
+// re-serializing it. Parsing the sanitized JSON should result in a value
+// identical to parsing the original JSON.
+// This allows parsing the sanitized JSON with the regular JSONParser while
+// reducing the risk versus parsing completely untrusted JSON. It also minifies
+// the resulting JSON, which might save some space.
+class JsonSanitizer {
+ public:
+ using StringCallback = base::Callback<void(const std::string&)>;
+
+ // Starts sanitizing the passed in unsafe JSON string. Either the passed
+ // |success_callback| or the |error_callback| will be called with the result
+ // of the sanitization or an error message, respectively, but not before the
+ // method returns.
+ static void Sanitize(const std::string& unsafe_json,
+ const StringCallback& success_callback,
+ const StringCallback& error_callback);
+
+#if defined(OS_ANDROID)
+ static bool Register(JNIEnv* env);
+#endif
+
+ protected:
+ JsonSanitizer() {}
+ ~JsonSanitizer() {}
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(JsonSanitizer);
+};
+
+} // namespace safe_json
+
+#endif // COMPONENTS_SAFE_JSON_JSON_SANITIZER_H_
diff --git a/components/safe_json/json_sanitizer_android.cc b/components/safe_json/json_sanitizer_android.cc
new file mode 100644
index 0000000..9ccad08
--- /dev/null
+++ b/components/safe_json/json_sanitizer_android.cc
@@ -0,0 +1,115 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_json/json_sanitizer.h"
+
+#include "base/android/jni_string.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/strings/string_util.h"
+#include "jni/JsonSanitizer_jni.h"
+
+namespace safe_json {
+
+namespace {
+
+// An implementation of JsonSanitizer that calls into Java. It deals with
+// malformed input (in particular malformed Unicode encodings) in the following
+// steps:
+// 1. The input string is checked for whether it is well-formed UTF-8. Malformed
+// UTF-8 is rejected.
+// 2. The UTF-8 string is converted in native code to a Java String, which is
+// encoded as UTF-16.
+// 2. The Java String is parsed as JSON in the memory-safe environment of the
+// Java VM and any string literals are unescaped.
+// 3. The string literals themselves are now untrusted, so they are checked in
+// Java for whether they are valid UTF-16.
+// 4. The parsed JSON with sanitized literals is encoded back into a Java
+// String and passed back to native code.
+// 5. The Java String is converted back to UTF-8 in native code.
+// This ensures that both invalid UTF-8 and invalid escaped UTF-16 will be
+// rejected.
+class JsonSanitizerAndroid : public JsonSanitizer {
+ public:
+ JsonSanitizerAndroid(const StringCallback& success_callback,
+ const StringCallback& error_callback);
+ ~JsonSanitizerAndroid() {}
+
+ void Sanitize(const std::string& unsafe_json);
+
+ void OnSuccess(const std::string& json);
+ void OnError(const std::string& error);
+
+ private:
+ StringCallback success_callback_;
+ StringCallback error_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(JsonSanitizerAndroid);
+};
+
+JsonSanitizerAndroid::JsonSanitizerAndroid(
+ const StringCallback& success_callback,
+ const StringCallback& error_callback)
+ : success_callback_(success_callback),
+ error_callback_(error_callback) {}
+
+void JsonSanitizerAndroid::Sanitize(const std::string& unsafe_json) {
+ // The JSON parser only accepts wellformed UTF-8.
+ if (!base::IsStringUTF8(unsafe_json)) {
+ OnError("Unsupported encoding");
+ return;
+ }
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ base::android::ScopedJavaLocalRef<jstring> unsafe_json_java =
+ base::android::ConvertUTF8ToJavaString(env, unsafe_json);
+
+ // This will synchronously call either OnSuccess() or OnError().
+ Java_JsonSanitizer_sanitize(env, reinterpret_cast<jlong>(this),
+ unsafe_json_java.obj());
+}
+
+void JsonSanitizerAndroid::OnSuccess(const std::string& json) {
+ base::MessageLoop::current()->PostTask(FROM_HERE,
+ base::Bind(success_callback_, json));
+}
+
+void JsonSanitizerAndroid::OnError(const std::string& error) {
+ base::MessageLoop::current()->PostTask(FROM_HERE,
+ base::Bind(error_callback_, error));
+}
+
+} // namespace
+
+void OnSuccess(JNIEnv* env, jclass clazz, jlong jsanitizer, jstring json) {
+ JsonSanitizerAndroid* sanitizer =
+ reinterpret_cast<JsonSanitizerAndroid*>(jsanitizer);
+ sanitizer->OnSuccess(base::android::ConvertJavaStringToUTF8(env, json));
+}
+
+void OnError(JNIEnv* env, jclass clazz, jlong jsanitizer, jstring error) {
+ JsonSanitizerAndroid* sanitizer =
+ reinterpret_cast<JsonSanitizerAndroid*>(jsanitizer);
+ sanitizer->OnError(base::android::ConvertJavaStringToUTF8(env, error));
+}
+
+// static
+void JsonSanitizer::Sanitize(const std::string& unsafe_json,
+ const StringCallback& success_callback,
+ const StringCallback& error_callback) {
+ // JsonSanitizerAndroid does all its work synchronously, but posts any
+ // callbacks to the current message loop. This means it can be destroyed at
+ // the end of this method.
+ JsonSanitizerAndroid sanitizer(success_callback, error_callback);
+ sanitizer.Sanitize(unsafe_json);
+}
+
+// static
+bool JsonSanitizer::Register(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
+
+} // namespace safe_json
diff --git a/components/safe_json/json_sanitizer_unittest.cc b/components/safe_json/json_sanitizer_unittest.cc
new file mode 100644
index 0000000..0a75ad3
--- /dev/null
+++ b/components/safe_json/json_sanitizer_unittest.cc
@@ -0,0 +1,184 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/bind.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/values.h"
+#include "components/safe_json/json_sanitizer.h"
+#include "components/safe_json/safe_json_parser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(OS_ANDROID)
+#include "components/safe_json/testing_json_parser.h"
+#endif
+
+namespace safe_json {
+
+class JsonSanitizerTest : public ::testing::Test {
+ public:
+ void TearDown() override {
+ // Flush any tasks from the message loop to avoid leaks.
+ base::RunLoop().RunUntilIdle();
+ }
+
+ protected:
+ void CheckSuccess(const std::string& json);
+ void CheckError(const std::string& json);
+
+ private:
+ enum class State {
+ // ERROR is a #define on Windows, so we prefix the values with STATE_.
+ STATE_IDLE,
+ STATE_SUCCESS,
+ STATE_ERROR,
+ };
+
+ void Sanitize(const std::string& json);
+
+ void OnSuccess(const std::string& json);
+ void OnError(const std::string& error);
+
+ base::MessageLoop message_loop_;
+
+#if !defined(OS_ANDROID)
+ safe_json::TestingJsonParser::ScopedFactoryOverride factory_override_;
+#endif
+
+ std::string result_;
+ std::string error_;
+ State state_;
+
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+void JsonSanitizerTest::CheckSuccess(const std::string& json) {
+ SCOPED_TRACE(json);
+ Sanitize(json);
+ scoped_ptr<base::Value> parsed = base::JSONReader::Read(json);
+ ASSERT_TRUE(parsed);
+ EXPECT_EQ(State::STATE_SUCCESS, state_) << "Error: " << error_;
+
+ // The JSON parser should accept the result.
+ int error_code;
+ std::string error;
+ scoped_ptr<base::Value> reparsed = base::JSONReader::ReadAndReturnError(
+ result_, base::JSON_PARSE_RFC, &error_code, &error);
+ EXPECT_TRUE(reparsed)
+ << "Invalid result: " << error;
+
+ // The parsed values should be equal.
+ EXPECT_TRUE(reparsed->Equals(parsed.get()));
+}
+
+void JsonSanitizerTest::CheckError(const std::string& json) {
+ SCOPED_TRACE(json);
+ Sanitize(json);
+ EXPECT_EQ(State::STATE_ERROR, state_) << "Result: " << result_;
+}
+
+void JsonSanitizerTest::Sanitize(const std::string& json) {
+ state_ = State::STATE_IDLE;
+ result_.clear();
+ error_.clear();
+ run_loop_.reset(new base::RunLoop);
+ JsonSanitizer::Sanitize(
+ json,
+ base::Bind(&JsonSanitizerTest::OnSuccess, base::Unretained(this)),
+ base::Bind(&JsonSanitizerTest::OnError, base::Unretained(this)));
+
+ // We should never get a result immediately.
+ EXPECT_EQ(State::STATE_IDLE, state_);
+ run_loop_->Run();
+}
+
+void JsonSanitizerTest::OnSuccess(const std::string& json) {
+ ASSERT_EQ(State::STATE_IDLE, state_);
+ state_ = State::STATE_SUCCESS;
+ result_ = json;
+ run_loop_->Quit();
+}
+
+void JsonSanitizerTest::OnError(const std::string& error) {
+ ASSERT_EQ(State::STATE_IDLE, state_);
+ state_ = State::STATE_ERROR;
+ error_ = error;
+ run_loop_->Quit();
+}
+
+TEST_F(JsonSanitizerTest, Json) {
+ // Valid JSON:
+ CheckSuccess("{\n \"foo\": \"bar\"\n}");
+ CheckSuccess("[true]");
+ CheckSuccess("[42]");
+ CheckSuccess("[3.14]");
+ CheckSuccess("[4.0]");
+ CheckSuccess("[null]");
+ CheckSuccess("[\"foo\", \"bar\"]");
+
+ // JSON syntax errors:
+ CheckError("");
+ CheckError("[");
+ CheckError("null");
+
+ // Unterminated array.
+ CheckError("[1,2,3,]");
+}
+
+TEST_F(JsonSanitizerTest, Nesting) {
+ // 99 nested arrays are fine.
+ std::string nested(99u, '[');
+ nested.append(99u, ']');
+ CheckSuccess(nested);
+
+ // 100 nested arrays is too much.
+ CheckError(std::string(100u, '[') + std::string(100u, ']'));
+}
+
+TEST_F(JsonSanitizerTest, Unicode) {
+ // Non-ASCII characters encoded either directly as UTF-8 or escaped as UTF-16:
+ CheckSuccess("[\"☃\"]");
+ CheckSuccess("[\"\\u2603\"]");
+ CheckSuccess("[\"😃\"]");
+ CheckSuccess("[\"\\ud83d\\ude03\"]");
+
+ // Malformed UTF-8:
+ // A continuation byte outside of a sequence.
+ CheckError("[\"\x80\"]");
+
+ // A start byte that is missing a continuation byte.
+ CheckError("[\"\xc0\"]");
+
+ // An invalid byte in UTF-8.
+ CheckError("[\"\xfe\"]");
+
+ // An overlong encoding (of the letter 'A').
+ CheckError("[\"\xc1\x81\"]");
+
+ // U+D83D, a code point reserved for (high) surrogates.
+ CheckError("[\"\xed\xa0\xbd\"]");
+
+ // U+4567890, a code point outside of the valid range for Unicode.
+ CheckError("[\"\xfc\x84\x95\xa7\xa2\x90\"]");
+
+ // Malformed escaped UTF-16:
+ // An unmatched high surrogate.
+ CheckError("[\"\\ud83d\"]");
+
+ // An unmatched low surrogate.
+ CheckError("[\"\\ude03\"]");
+
+ // A low surrogate followed by a high surrogate.
+ CheckError("[\"\\ude03\\ud83d\"]");
+
+ // Valid escaped UTF-16 that encodes non-characters:
+ CheckError("[\"\\ufdd0\"]");
+ CheckError("[\"\\ufffe\"]");
+ CheckError("[\"\\ud83f\\udffe\"]");
+}
+
+} // namespace safe_json
diff --git a/components/safe_json/safe_json_parser.cc b/components/safe_json/safe_json_parser.cc
index 8193d03..653800e 100644
--- a/components/safe_json/safe_json_parser.cc
+++ b/components/safe_json/safe_json_parser.cc
@@ -4,7 +4,11 @@
#include "components/safe_json/safe_json_parser.h"
+#if defined(OS_ANDROID)
+#include "components/safe_json/safe_json_parser_android.h"
+#else
#include "components/safe_json/safe_json_parser_impl.h"
+#endif
namespace safe_json {
@@ -12,6 +16,20 @@ namespace {
SafeJsonParser::Factory g_factory = nullptr;
+SafeJsonParser* Create(const std::string& unsafe_json,
+ const SafeJsonParser::SuccessCallback& success_callback,
+ const SafeJsonParser::ErrorCallback& error_callback) {
+ if (g_factory)
+ return g_factory(unsafe_json, success_callback, error_callback);
+
+#if defined(OS_ANDROID)
+ return new SafeJsonParserAndroid(unsafe_json, success_callback,
+ error_callback);
+#else
+ return new SafeJsonParserImpl(unsafe_json, success_callback, error_callback);
+#endif
+}
+
} // namespace
// static
@@ -24,9 +42,7 @@ void SafeJsonParser::Parse(const std::string& unsafe_json,
const SuccessCallback& success_callback,
const ErrorCallback& error_callback) {
SafeJsonParser* parser =
- g_factory ? g_factory(unsafe_json, success_callback, error_callback)
- : new SafeJsonParserImpl(unsafe_json, success_callback,
- error_callback);
+ Create(unsafe_json, success_callback, error_callback);
parser->Start();
}
diff --git a/components/safe_json/safe_json_parser_android.cc b/components/safe_json/safe_json_parser_android.cc
new file mode 100644
index 0000000..9266ec7
--- /dev/null
+++ b/components/safe_json/safe_json_parser_android.cc
@@ -0,0 +1,56 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_json/safe_json_parser_android.h"
+
+#include "base/bind.h"
+#include "base/json/json_reader.h"
+#include "base/values.h"
+#include "components/safe_json/json_sanitizer.h"
+
+namespace safe_json {
+
+SafeJsonParserAndroid::SafeJsonParserAndroid(
+ const std::string& unsafe_json,
+ const SuccessCallback& success_callback,
+ const ErrorCallback& error_callback)
+ : unsafe_json_(unsafe_json),
+ success_callback_(success_callback),
+ error_callback_(error_callback) {}
+
+SafeJsonParserAndroid::~SafeJsonParserAndroid() {}
+
+void SafeJsonParserAndroid::Start() {
+ JsonSanitizer::Sanitize(
+ unsafe_json_,
+ base::Bind(&SafeJsonParserAndroid::OnSanitizationSuccess,
+ base::Unretained(this)),
+ base::Bind(&SafeJsonParserAndroid::OnSanitizationError,
+ base::Unretained(this)));
+}
+
+void SafeJsonParserAndroid::OnSanitizationSuccess(
+ const std::string& sanitized_json) {
+ // Self-destruct at the end of this method.
+ scoped_ptr<SafeJsonParserAndroid> deleter(this);
+
+ int error_code;
+ std::string error;
+ scoped_ptr<base::Value> value = base::JSONReader::ReadAndReturnError(
+ sanitized_json, base::JSON_PARSE_RFC, &error_code, &error);
+
+ if (!value) {
+ error_callback_.Run(error);
+ return;
+ }
+
+ success_callback_.Run(value.Pass());
+}
+
+void SafeJsonParserAndroid::OnSanitizationError(const std::string& error) {
+ error_callback_.Run(error);
+ delete this;
+}
+
+} // namespace safe_json
diff --git a/components/safe_json/safe_json_parser_android.h b/components/safe_json/safe_json_parser_android.h
new file mode 100644
index 0000000..2058731
--- /dev/null
+++ b/components/safe_json/safe_json_parser_android.h
@@ -0,0 +1,41 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAFE_JSON_SAFE_JSON_PARSER_ANDROID_H_
+#define COMPONENTS_SAFE_JSON_SAFE_JSON_PARSER_ANDROID_H_
+
+#include "base/memory/scoped_ptr.h"
+#include "components/safe_json/safe_json_parser.h"
+
+namespace safe_json {
+
+class JsonSanitizer;
+
+class SafeJsonParserAndroid : public SafeJsonParser {
+ public:
+ SafeJsonParserAndroid(const std::string& unsafe_json,
+ const SuccessCallback& success_callback,
+ const ErrorCallback& error_callback);
+
+ private:
+ friend struct base::DefaultDeleter<SafeJsonParserAndroid>;
+
+ ~SafeJsonParserAndroid() override;
+
+ void OnSanitizationSuccess(const std::string& sanitized_json);
+ void OnSanitizationError(const std::string& error);
+
+ // SafeJsonParser implementation.
+ void Start() override;
+
+ const std::string unsafe_json_;
+ SuccessCallback success_callback_;
+ ErrorCallback error_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(SafeJsonParserAndroid);
+};
+
+} // namespace safe_json
+
+#endif // COMPONENTS_SAFE_JSON_SAFE_JSON_PARSER_ANDROID_H_
diff --git a/components/test/DEPS b/components/test/DEPS
index b734027..ad24217 100644
--- a/components/test/DEPS
+++ b/components/test/DEPS
@@ -2,6 +2,7 @@ include_rules = [
# To initialize the global data of content_settings.
"+components/content_settings/core/common",
"+components/invalidation/impl/android/component_jni_registrar.h",
+ "+components/safe_json/android/component_jni_registrar.h",
"+content/public/android/java/src/org/chromium/content/browser",
"+content/public/app/content_jni_onload.h",
"+content/public/app/content_main.h",
diff --git a/components/test/run_all_unittests.cc b/components/test/run_all_unittests.cc
index 0c2dd10..a24c284 100644
--- a/components/test/run_all_unittests.cc
+++ b/components/test/run_all_unittests.cc
@@ -23,6 +23,7 @@
#if defined(OS_ANDROID)
#include "base/android/jni_android.h"
#include "components/invalidation/impl/android/component_jni_registrar.h"
+#include "components/safe_json/android/component_jni_registrar.h"
#include "ui/base/android/ui_base_jni_registrar.h"
#include "ui/gfx/android/gfx_jni_registrar.h"
#endif
@@ -48,9 +49,10 @@ class ComponentsTestSuite : public base::TestSuite {
#if defined(OS_ANDROID)
// Register JNI bindings for android.
JNIEnv* env = base::android::AttachCurrentThread();
- gfx::android::RegisterJni(env);
- ui::android::RegisterJni(env);
- invalidation::android::RegisterInvalidationJni(env);
+ ASSERT_TRUE(gfx::android::RegisterJni(env));
+ ASSERT_TRUE(ui::android::RegisterJni(env));
+ ASSERT_TRUE(invalidation::android::RegisterInvalidationJni(env));
+ ASSERT_TRUE(safe_json::android::RegisterSafeJsonJni(env));
#endif
ui::RegisterPathProvider();
diff --git a/tools/android/eclipse/.classpath b/tools/android/eclipse/.classpath
index 3c2770a..4400f24 100644
--- a/tools/android/eclipse/.classpath
+++ b/tools/android/eclipse/.classpath
@@ -46,6 +46,7 @@ to the classpath for downstream development. See "additional_entries" below.
<classpathentry kind="src" path="components/policy/android/java/src"/>
<classpathentry kind="src" path="components/precache/android/java/src"/>
<classpathentry kind="src" path="components/precache/android/javatests/src"/>
+ <classpathentry kind="src" path="components/safe_json/android/java/src"/>
<classpathentry kind="src" path="components/service_tab_launcher/android/java/src"/>
<classpathentry kind="src" path="components/variations/android/java/src"/>
<classpathentry kind="src" path="components/web_contents_delegate_android/android/java/src"/>