Defer whitelist decisions to plugins

There is a default policy, which is implemented in the case where no plugins override any of the whitelist methods:
 * Error URLs must start with file://
 * Navigation is allowed to file:// and data: URLs which do not contain "app_webview"
 * External URLs do not launch intents
 * XHRs are allowed to file:// and data: URLs which do not contain "app_webview"
This commit is contained in:
Ian Clelland 2014-10-22 16:16:52 -04:00
parent 44aa98887f
commit 23584274d2
5 changed files with 95 additions and 32 deletions

View File

@ -89,6 +89,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
private Whitelist internalWhitelist;
private Whitelist externalWhitelist;
private CordovaPreferences preferences;
private CordovaUriHelper helper;
// The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl;
@ -120,10 +121,11 @@ public class AndroidWebView extends WebView implements CordovaWebView {
this.internalWhitelist = internalWhitelist;
this.externalWhitelist = externalWhitelist;
this.preferences = preferences;
this.helper = new CordovaUriHelper(cordova, this);
pluginManager = new PluginManager(this, this.cordova, pluginEntries);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova));
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova), helper);
pluginManager.addService("App", "org.apache.cordova.CoreAndroid");
initWebViewSettings();
@ -354,7 +356,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
LOG.d(TAG, ">>> loadUrlNow()");
}
if (url.startsWith("file://") || url.startsWith("javascript:") || internalWhitelist.isUrlWhiteListed(url)) {
if (url.startsWith("javascript:") || helper.shouldAllowNavigation(url)) {
super.loadUrl(url);
}
}
@ -429,7 +431,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
if (!openExternal) {
// Make sure url is in whitelist
if (url.startsWith("file://") || internalWhitelist.isUrlWhiteListed(url)) {
if (helper.shouldAllowNavigation(url)) {
// TODO: What about params?
// Load new URL
loadUrlIntoView(url, true);

View File

@ -554,7 +554,11 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// If errorUrl specified, then load it
final String errorUrl = preferences.getString("errorUrl", null);
if ((errorUrl != null) && (errorUrl.startsWith("file://") || internalWhitelist.isUrlWhiteListed(errorUrl)) && (!failingUrl.equals(errorUrl))) {
CordovaUriHelper helper = new CordovaUriHelper(this, appView);
if ((errorUrl != null) &&
(!failingUrl.equals(errorUrl)) &&
(appView != null && helper.shouldAllowNavigation(errorUrl))
) {
// Load URL on UI thread
me.runOnUiThread(new Runnable() {
public void run() {

View File

@ -37,12 +37,14 @@ public class CordovaBridge {
private NativeToJsMessageQueue jsMessageQueue;
private volatile int expectedBridgeSecret = -1; // written by UI thread, read by JS thread.
private String loadedUrl;
protected CordovaUriHelper helper;
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue, CordovaUriHelper helper) {
this.pluginManager = pluginManager;
this.jsMessageQueue = jsMessageQueue;
this.helper = helper;
}
public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
if (!verifySecret("exec()", bridgeSecret)) {
return null;
@ -163,9 +165,9 @@ public class CordovaBridge {
}
else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
// Protect against random iframes being able to talk through the bridge.
// Trust only file URLs and the start URL's domain.
// The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin.
if (origin.startsWith("file:") || (origin.startsWith("http") && loadedUrl.startsWith(origin))) {
// Trust only file URLs and pages which the app would have been allowed
// to navigate to anyway.
if (origin.startsWith("file:") || helper.shouldAllowNavigation(origin)) {
// Enable the bridge
int bridgeMode = Integer.parseInt(defaultValue.substring(9));
jsMessageQueue.setBridgeMode(bridgeMode);

View File

@ -23,6 +23,7 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.WebView;
public class CordovaUriHelper {
@ -37,11 +38,77 @@ public class CordovaUriHelper {
appView = webView;
cordova = cdv;
}
/**
* Determine whether the webview should be allowed to navigate to a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldAllowNavigation
*/
public boolean shouldAllowNavigation(String url) {
Boolean pluginManagerAllowsNavigation = this.appView.getPluginManager().shouldAllowNavigation(url);
if (pluginManagerAllowsNavigation == null) {
// Default policy:
// Internal urls on file:// or data:// that do not contain "app_webview" are allowed for navigation
if(url.startsWith("file://") || url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("app_webview");
}
return false;
}
return pluginManagerAllowsNavigation;
}
/**
* Determine whether the webview should be allowed to launch an intent for a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldOpenExternalUrl
*/
public boolean shouldOpenExternalUrl(String url) {
Boolean pluginManagerAllowsExternalUrl = this.appView.getPluginManager().shouldOpenExternalUrl(url);
if (pluginManagerAllowsExternalUrl == null) {
// Default policy:
// External URLs are not allowed
return false;
}
return pluginManagerAllowsExternalUrl;
}
/**
* Determine whether the webview should be allowed to request a resource from a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldAllowRequest
*/
public boolean shouldAllowRequest(String url) {
Boolean pluginManagerAllowsRequest = this.appView.getPluginManager().shouldAllowRequest(url);
if (pluginManagerAllowsRequest == null) {
// Default policy:
// Internal urls on file:// or data:// that do not contain "app_webview" are allowed for navigation
if(url.startsWith("file://") || url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("app_webview");
}
return false;
}
return pluginManagerAllowsRequest;
}
/**
* Give the host application a chance to take over the control when a new url
* is about to be loaded in the current WebView.
*
* This method implements the default whitelist policy when no plugins override
* the whitelist methods:
* Internal urls on file:// or data:// that do not contain "app_webview" are allowed for navigation
* External urls are not allowed.
*
* @param view The WebView that is initiating the callback.
* @param url The url to be loaded.
* @return true to override, false for default behavior
@ -49,23 +116,15 @@ public class CordovaUriHelper {
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
public boolean shouldOverrideUrlLoading(String url) {
// Give plugins the chance to handle the url
if (this.appView.getPluginManager().onOverrideUrlLoading(url)) {
// Do nothing other than what the plugins wanted.
// If any returned true, then the request was handled.
return true;
}
else if(url.startsWith("file://") | url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return url.contains("app_webview");
}
else if (appView.getWhitelist().isUrlWhiteListed(url)) {
if (shouldAllowNavigation(url)) {
// Allow internal navigation
return false;
}
else if (appView.getExternalWhitelist().isUrlWhiteListed(url))
{
if (shouldOpenExternalUrl(url)) {
// Do nothing other than what the plugins wanted.
// If any returned false, then the request was either blocked
// completely, or handled out-of-band by the plugin. If they all
// returned true, then we should open the URL here.
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
@ -77,10 +136,11 @@ public class CordovaUriHelper {
this.cordova.getActivity().startActivity(intent);
return true;
} catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e);
Log.e(TAG, "Error loading url " + url, e);
}
return true;
}
// Intercept the request and do nothing with it -- block it
// Block by default
return true;
}
}

View File

@ -45,7 +45,7 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient {
try {
// Check the against the whitelist and lock out access to the WebView directory
// Changing this will cause problems for your application
if (isUrlHarmful(url)) {
if (!helper.shouldAllowRequest(url)) {
LOG.w(TAG, "URL blocked by whitelist: " + url);
// Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null);
@ -71,11 +71,6 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient {
}
}
private boolean isUrlHarmful(String url) {
return ((url.startsWith("http:") || url.startsWith("https:")) && !appView.getWhitelist().isUrlWhiteListed(url))
|| url.contains("app_webview");
}
private static boolean needsKitKatContentUrlFix(Uri uri) {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme());
}