diff options
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"/> |