Chromium Code Reviews| Index: components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java |
| 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 0000000000000000000000000000000000000000..32e94ca0d6930904e3e700f81499190dd3c08bb6 |
| --- /dev/null |
| +++ b/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java |
| @@ -0,0 +1,171 @@ |
| +// 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)); |
|
Robert Sesek
2015/07/07 21:54:16
Can this throw as well?
Bernhard Bauer
2015/07/08 11:54:29
It shouldn't, because if the value is not an integ
|
| + } |
| + 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); |
| + if (Character.isLowSurrogate(c)) return false; |
|
Robert Sesek
2015/07/07 21:54:16
Is this one-line if stylelistic? It seems terribly
Robert Sesek
2015/07/07 21:54:16
This code is security critical. Please add some do
Bernhard Bauer
2015/07/08 11:54:29
I know! It does look weird if you're used to C++ s
Bernhard Bauer
2015/07/08 11:54:29
Done. Also reordered the checks slightly so they'r
|
| + |
| + if (!Character.isHighSurrogate(c)) { |
| + if (!isUnicodeCharacter(c)) return false; |
| + |
| + continue; |
| + } |
| + |
| + char high = c; |
| + if (++i >= length) return false; |
| + |
| + char low = string.charAt(i); |
| + if (!Character.isLowSurrogate(low)) return false; |
| + |
| + int codePoint = Character.toCodePoint(high, low); |
| + 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); |
| +} |