| Index: chrome/android/java/src/org/chromium/chrome/browser/UrlUtilities.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/UrlUtilities.java b/chrome/android/java/src/org/chromium/chrome/browser/UrlUtilities.java
|
| index 1eca2c96dce9aba68b4853176d8e0fab675a370f..f4f292244910f9740b91ec940008602909628aed 100644
|
| --- a/chrome/android/java/src/org/chromium/chrome/browser/UrlUtilities.java
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/UrlUtilities.java
|
| @@ -7,10 +7,15 @@ package org.chromium.chrome.browser;
|
| import android.text.TextUtils;
|
|
|
| import org.chromium.base.CollectionUtil;
|
| +import org.chromium.base.Log;
|
|
|
| +import java.io.UnsupportedEncodingException;
|
| import java.net.URI;
|
| import java.net.URISyntaxException;
|
| +import java.net.URLDecoder;
|
| import java.util.HashSet;
|
| +import java.util.regex.Matcher;
|
| +import java.util.regex.Pattern;
|
|
|
| /**
|
| * Utilities for working with URIs (and URLs). These methods may be used in security-sensitive
|
| @@ -18,6 +23,8 @@ import java.util.HashSet;
|
| * must be high.
|
| */
|
| public class UrlUtilities {
|
| + private static final String TAG = "UrlUtilities";
|
| +
|
| /**
|
| * URI schemes that ContentView can handle.
|
| */
|
| @@ -207,6 +214,156 @@ public class UrlUtilities {
|
| return nativeGetDomainAndRegistry(uri, includePrivateRegistries);
|
| }
|
|
|
| + // Patterns used in validateIntentUrl.
|
| + private static final Pattern DNS_HOSTNAME_PATTERN =
|
| + Pattern.compile("^[\\w\\.-]*$");
|
| + private static final Pattern JAVA_PACKAGE_NAME_PATTERN =
|
| + Pattern.compile("^[\\w\\.-]*$");
|
| + private static final Pattern ANDROID_COMPONENT_NAME_PATTERN =
|
| + Pattern.compile("^[\\w\\./-]*$");
|
| + private static final Pattern URL_SCHEME_PATTERN =
|
| + Pattern.compile("^[a-zA-Z]+$");
|
| +
|
| + /**
|
| + * @param url An Android intent:// URL to validate.
|
| + *
|
| + * @throws URISyntaxException if url is not a valid Android intent://
|
| + * URL, as specified at
|
| + * https://developer.chrome.com/multidevice/android/intents#syntax.
|
| + */
|
| + public static boolean validateIntentUrl(String url) {
|
| + if (url == null) {
|
| + Log.d(TAG, "url was null");
|
| + return false;
|
| + }
|
| +
|
| + URI parsed = null;
|
| + try {
|
| + parsed = new URI(url);
|
| + } catch (URISyntaxException e) {
|
| + Log.d(TAG, e.toString());
|
| + return false;
|
| + }
|
| +
|
| + if (!parsed.getScheme().equals("intent")) {
|
| + Log.d(TAG, "scheme was not 'intent'");
|
| + return false;
|
| + }
|
| +
|
| + String hostname = parsed.getHost();
|
| + if (hostname == null) {
|
| + Log.d(TAG, "hostname was null for '" + url + "'");
|
| + return false;
|
| + }
|
| + Matcher m = DNS_HOSTNAME_PATTERN.matcher(hostname);
|
| + if (!m.matches()) {
|
| + Log.d(TAG, "hostname did not match DNS_HOSTNAME_PATTERN");
|
| + return false;
|
| + }
|
| +
|
| + String path = parsed.getPath();
|
| + if (path == null || (!path.isEmpty() && !path.equals("/"))) {
|
| + Log.d(TAG, "path was null or not '/'");
|
| + return false;
|
| + }
|
| +
|
| + // We need to get the raw, unparsed, un-URL-decoded fragment.
|
| + // parsed.getFragment() returns a URL-decoded fragment, which can
|
| + // interfere with lexing and parsing Intent extras correctly. Therefore,
|
| + // we handle the fragment "manually", but first assert that it
|
| + // URL-decodes correctly.
|
| + int fragmentStart = url.indexOf('#');
|
| + if (fragmentStart == -1 || fragmentStart == url.length() - 1) {
|
| + Log.d(TAG, "Could not find '#'");
|
| + return false;
|
| + }
|
| + String fragment = url.substring(url.indexOf('#') + 1);
|
| + try {
|
| + String f = parsed.getFragment();
|
| + if (f == null) {
|
| + Log.d(TAG, "Could not get fragment from parsed URL");
|
| + return false;
|
| + }
|
| + if (!URLDecoder.decode(fragment, "UTF-8").equals(f)) {
|
| + Log.d(TAG, "Parsed fragment does not equal lexed fragment");
|
| + return false;
|
| + }
|
| + } catch (UnsupportedEncodingException e) {
|
| + Log.d(TAG, e.toString());
|
| + return false;
|
| + }
|
| +
|
| + // Now lex and parse the correctly-encoded fragment.
|
| + String[] parts = fragment.split(";");
|
| + if (parts.length < 3
|
| + || !parts[0].equals("Intent")
|
| + || !parts[parts.length - 1].equals("end")) {
|
| + Log.d(TAG, "Invalid fragment (not enough parts, lacking Intent, "
|
| + + "or lacking end");
|
| + return false;
|
| + }
|
| +
|
| + boolean seenPackage = false;
|
| + boolean seenAction = false;
|
| + boolean seenCategory = false;
|
| + boolean seenComponent = false;
|
| + boolean seenScheme = false;
|
| +
|
| + for (int i = 1; i < parts.length - 1; ++i) {
|
| + // This is OK *only* because no valid package, action, category,
|
| + // component, or scheme contains (unencoded) "=".
|
| + String[] pair = parts[i].split("=");
|
| + if (2 != pair.length) {
|
| + Log.d(TAG, "Invalid key=value pair '" + parts[i] + "'");
|
| + return false;
|
| + }
|
| +
|
| + m = JAVA_PACKAGE_NAME_PATTERN.matcher(pair[1]);
|
| + if (pair[0].equals("package")) {
|
| + if (seenPackage || !m.matches()) {
|
| + Log.d(TAG, "Invalid package '" + pair[1] + "'");
|
| + return false;
|
| + }
|
| + seenPackage = true;
|
| + } else if (pair[0].equals("action")) {
|
| + if (seenAction || !m.matches()) {
|
| + Log.d(TAG, "Invalid action '" + pair[1] + "'");
|
| + return false;
|
| + }
|
| + seenAction = true;
|
| + } else if (pair[0].equals("category")) {
|
| + if (seenCategory || !m.matches()) {
|
| + Log.d(TAG, "Invalid category '" + pair[1] + "'");
|
| + return false;
|
| + }
|
| + seenCategory = true;
|
| + } else if (pair[0].equals("component")) {
|
| + Matcher componentMatcher = ANDROID_COMPONENT_NAME_PATTERN.matcher(pair[1]);
|
| + if (seenComponent || !componentMatcher.matches()) {
|
| + Log.d(TAG, "Invalid component '" + pair[1] + "'");
|
| + return false;
|
| + }
|
| + seenComponent = true;
|
| + } else if (pair[0].equals("scheme")) {
|
| + if (seenScheme) return false;
|
| + Matcher schemeMatcher = URL_SCHEME_PATTERN.matcher(pair[1]);
|
| + if (!schemeMatcher.matches()) {
|
| + Log.d(TAG, "Invalid scheme '" + pair[1] + "'");
|
| + return false;
|
| + }
|
| + seenScheme = true;
|
| + } else {
|
| + // Assume we are seeing an Intent Extra. Up above, we ensured
|
| + // that the #Intent... fragment was correctly URL-encoded;
|
| + // beyond that, there is no further validation we can do. Extras
|
| + // are blobs to us.
|
| + continue;
|
| + }
|
| + }
|
| +
|
| + return true;
|
| + }
|
| +
|
| private static native boolean nativeSameDomainOrHost(String primaryUrl, String secondaryUrl,
|
| boolean includePrivateRegistries);
|
| private static native String nativeGetDomainAndRegistry(String url,
|
|
|