Skip to content

Commit

Permalink
[DAL][WebView] Add Digital Asset Link validation for WebView
Browse files Browse the repository at this point in the history
Experimental feature to validate main frame origins using Digital Asset
Links.
Feature is disabled by default via a feature flag.

Low-Coverage-Reason: The actual origin verification logic is tested and the initialization is tested manually.
Bug: crbug.com/1376958
Change-Id: I6d4f4876622a02e6efc0d3f58fd2d5d4035a9cf0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3966337
Reviewed-by: Peter Conn <peconn@chromium.org>
Reviewed-by: Richard Coles <torne@chromium.org>
Reviewed-by: Rayan Kanso <rayankans@chromium.org>
Commit-Queue: Susanne Westphal <swestphal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1084906}
  • Loading branch information
s-westphal authored and Chromium LUCI CQ committed Dec 19, 2022
1 parent 05a67cf commit da24b14
Show file tree
Hide file tree
Showing 23 changed files with 513 additions and 13 deletions.
4 changes: 4 additions & 0 deletions android_webview/BUILD.gn
Expand Up @@ -568,6 +568,8 @@ android_library("browser_java") {
"java/src/org/chromium/android_webview/AwHttpAuthHandler.java",
"java/src/org/chromium/android_webview/AwLayoutSizer.java",
"java/src/org/chromium/android_webview/AwNetworkChangeNotifierRegistrationPolicy.java",
"java/src/org/chromium/android_webview/AwOriginVerificationScheduler.java",
"java/src/org/chromium/android_webview/AwOriginVerifier.java",
"java/src/org/chromium/android_webview/AwPacProcessor.java",
"java/src/org/chromium/android_webview/AwPdfExporter.java",
"java/src/org/chromium/android_webview/AwPrintDocumentAdapter.java",
Expand All @@ -583,6 +585,7 @@ android_library("browser_java") {
"java/src/org/chromium/android_webview/AwSupportLibIsomorphic.java",
"java/src/org/chromium/android_webview/AwThreadUtils.java",
"java/src/org/chromium/android_webview/AwTracingController.java",
"java/src/org/chromium/android_webview/AwVerificationResultStore.java",
"java/src/org/chromium/android_webview/AwViewAndroidDelegate.java",
"java/src/org/chromium/android_webview/AwViewMethods.java",
"java/src/org/chromium/android_webview/AwWebContentsDelegate.java",
Expand Down Expand Up @@ -662,6 +665,7 @@ android_library("browser_java") {
"//components/content_capture/android:java",
"//components/crash/android:handler_java",
"//components/crash/android:java",
"//components/digital_asset_links/android:java",
"//components/embedder_support/android:util_java",
"//components/embedder_support/android:web_contents_delegate_java",
"//components/embedder_support/android/metrics:java",
Expand Down
35 changes: 35 additions & 0 deletions android_webview/browser/aw_contents_io_thread_client.cc
Expand Up @@ -388,6 +388,24 @@ std::unique_ptr<AwWebResourceInterceptResponse> RunShouldInterceptRequest(
return web_resource_intercept_response;
}

bool RunShouldBlockRequest(AwWebResourceRequest request,
JavaObjectWeakGlobalRef ref) {
if (!request.is_outermost_main_frame) {
return false;
}

JNIEnv* env = AttachCurrentThread();
base::android::ScopedJavaLocalRef<jobject> obj = ref.get(env);
if (!obj) {
return true;
}
AwWebResourceRequest::AwJavaWebResourceRequest java_web_resource_request;
AwWebResourceRequest::ConvertToJava(env, request, &java_web_resource_request);

return Java_AwContentsBackgroundThreadClient_shouldBlockRequestFromNative(
env, obj, java_web_resource_request.jurl);
}

} // namespace

void AwContentsIoThreadClient::ShouldInterceptRequestAsync(
Expand All @@ -411,6 +429,23 @@ void AwContentsIoThreadClient::ShouldInterceptRequestAsync(
FROM_HERE, std::move(get_response), std::move(callback));
}

bool AwContentsIoThreadClient::ShouldBlockRequest(
AwWebResourceRequest request) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
JNIEnv* env = AttachCurrentThread();
if (!bg_thread_client_object_) {
bg_thread_client_object_.Reset(
Java_AwContentsIoThreadClient_getBackgroundThreadClient(env,
java_object_));
}
if (bg_thread_client_object_) {
return RunShouldBlockRequest(
request, JavaObjectWeakGlobalRef(env, bg_thread_client_object_.obj()));
}
// Block request if validation did not run.
return true;
}

bool AwContentsIoThreadClient::ShouldBlockContentUrls() const {
DCHECK_CURRENTLY_ON(BrowserThread::IO);

Expand Down
3 changes: 3 additions & 0 deletions android_webview/browser/aw_contents_io_thread_client.h
Expand Up @@ -108,6 +108,9 @@ class AwContentsIoThreadClient {
AwWebResourceRequest request,
ShouldInterceptRequestResponseCallback callback);

// Check if the request should be blocked based on web content ownership.
bool ShouldBlockRequest(AwWebResourceRequest request);

// Retrieve the AllowContentAccess setting value of this AwContents.
// This method is called on the IO thread only.
bool ShouldBlockContentUrls() const;
Expand Down
1 change: 1 addition & 0 deletions android_webview/browser/aw_feature_list.cc
Expand Up @@ -32,6 +32,7 @@ const base::Feature* const kFeaturesExposedToJava[] = {
&features::kWebViewUseMetricsUploadService,
&features::kWebViewXRequestedWithHeaderControl,
&features::kWebViewXRequestedWithHeaderManifestAllowList,
&features::kWebViewRestrictThirdPartyContent,
};

const base::Feature* FindFeatureExposedToJava(const std::string& feature_name) {
Expand Down
Expand Up @@ -405,6 +405,13 @@ void InterceptedRequest::Restart() {
request_.referrer.spec());
}

if (io_thread_client->ShouldBlockRequest(AwWebResourceRequest(request_))) {
// TODO(swestphal): Show alternative UI to inform the user about blocked
// third party web content.
SendErrorAndCompleteImmediately(net::ERR_ACCESS_DENIED);
return;
}

base::RepeatingClosure arg_ready_closure;
// Pointer lifetime is tied to |arg_ready_closure|.
InterceptResponseReceivedArgs* intercept_response_received_args;
Expand Down
6 changes: 6 additions & 0 deletions android_webview/common/aw_features.cc
Expand Up @@ -103,6 +103,12 @@ BASE_FEATURE(kWebViewRecordAppDataDirectorySize,
"WebViewRecordAppDataDirectorySize",
base::FEATURE_DISABLED_BY_DEFAULT);

// Flag to restrict main frame Web Content to verified web content. Verification
// happens via Digital Asset Links.
BASE_FEATURE(kWebViewRestrictThirdPartyContent,
"WebViewRestrictThirdPartyContent",
base::FEATURE_DISABLED_BY_DEFAULT);

// Disallows window.{alert, prompt, confirm} if triggered inside a subframe that
// is not same origin with the main frame.
BASE_FEATURE(kWebViewSuppressDifferentOriginSubframeJSDialogs,
Expand Down
1 change: 1 addition & 0 deletions android_webview/common/aw_features.h
Expand Up @@ -32,6 +32,7 @@ BASE_DECLARE_FEATURE(kWebViewMeasureScreenCoverage);
BASE_DECLARE_FEATURE(kWebViewMixedContentAutoupgrades);
BASE_DECLARE_FEATURE(kWebViewOriginTrials);
BASE_DECLARE_FEATURE(kWebViewRecordAppDataDirectorySize);
BASE_DECLARE_FEATURE(kWebViewRestrictThirdPartyContent);
BASE_DECLARE_FEATURE(kWebViewSuppressDifferentOriginSubframeJSDialogs);
BASE_DECLARE_FEATURE(kWebViewTestFeature);
BASE_DECLARE_FEATURE(kWebViewUseMetricsUploadService);
Expand Down
Expand Up @@ -26,8 +26,10 @@
import org.chromium.android_webview.AwContentsStatics;
import org.chromium.android_webview.AwCookieManager;
import org.chromium.android_webview.AwDarkMode;
import org.chromium.android_webview.AwFeatureList;
import org.chromium.android_webview.AwLocaleConfig;
import org.chromium.android_webview.AwNetworkChangeNotifierRegistrationPolicy;
import org.chromium.android_webview.AwOriginVerificationScheduler;
import org.chromium.android_webview.AwProxyController;
import org.chromium.android_webview.AwServiceWorkerController;
import org.chromium.android_webview.AwThreadUtils;
Expand All @@ -36,6 +38,7 @@
import org.chromium.android_webview.ProductConfig;
import org.chromium.android_webview.R;
import org.chromium.android_webview.WebViewChromiumRunQueue;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.android_webview.common.AwResource;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.gfx.AwDrawFnImpl;
Expand Down Expand Up @@ -229,6 +232,10 @@ protected void startChromiumLocked() {
mFactory, awBrowserContext.getGeolocationPermissions());
mWebStorage =
new WebStorageAdapter(mFactory, mBrowserContext.getQuotaManagerBridge());
if (AwFeatureList.isEnabled(AwFeatures.WEBVIEW_RESTRICT_THIRD_PARTY_CONTENT)) {
AwOriginVerificationScheduler.initAndScheduleAll(
context.getPackageName(), context, awBrowserContext, null);
}
mAwTracingController = getTracingController();
mServiceWorkerController = awBrowserContext.getServiceWorkerController();
mAwProxyController = new AwProxyController();
Expand Down
1 change: 1 addition & 0 deletions android_webview/java/DEPS
@@ -1,5 +1,6 @@
include_rules = [
"+components/autofill/android/java",
"+components/digital_asset_links/android/java",
"+components/embedder_support/android/java",
"+components/embedder_support/android/metrics/java",
"+components/navigation_interception/android/java",
Expand Down
Expand Up @@ -137,6 +137,9 @@
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -679,6 +682,39 @@ public WebResourceResponseInfo shouldInterceptRequest(
}
return webResourceResponseInfo;
}

@Override
public boolean shouldBlockRequest(String url) {
if (!AwFeatureList.isEnabled(AwFeatures.WEBVIEW_RESTRICT_THIRD_PARTY_CONTENT)) {
return false;
}
// TODO(1376958): Implement a URLLoaderThrottle to not block the IO thread before
// enabling the feature.

CountDownLatch countDownLatch = new CountDownLatch(1);
AtomicBoolean verified = new AtomicBoolean(false);

// Verifications are scheduled when WebView is initialized, so when this is called, the
// verification is likely finished here.
if (AwOriginVerificationScheduler.getInstance().getOriginVerifier().checkForSavedResult(
url)) {
return false;
}

AwThreadUtils.postToUiThreadLooper(() -> {
AwOriginVerificationScheduler.getInstance().verify(
url, mBrowserContext, (result) -> {
verified.set(result);
countDownLatch.countDown();
});
});
try {
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// Returning the default value as no successful verification was performed.
}
return !verified.get();
}
}

//--------------------------------------------------------------------------------------------
Expand Down
Expand Up @@ -22,6 +22,8 @@ public abstract class AwContentsBackgroundThreadClient {
public abstract WebResourceResponseInfo shouldInterceptRequest(
AwContentsClient.AwWebResourceRequest request);

public abstract boolean shouldBlockRequest(String url);

// Protected methods ---------------------------------------------------------------------------

@NonNull
Expand All @@ -47,4 +49,9 @@ private AwWebResourceInterceptResponse shouldInterceptRequestFromNative(String u
return new AwWebResourceInterceptResponse(null, /*raisedException=*/true);
}
}

@CalledByNative
private boolean shouldBlockRequestFromNative(String url) {
return shouldBlockRequest(url);
}
}
@@ -0,0 +1,75 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.content.Context;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.components.digital_asset_links.OriginVerificationScheduler;
import org.chromium.components.digital_asset_links.OriginVerifier;
import org.chromium.components.digital_asset_links.OriginVerifierHelper;
import org.chromium.components.embedder_support.util.Origin;

import java.util.Set;

/**
* Singleton.
* AwOriginVerificationScheduler provides a WebView specific implementation of {@link
* OriginVerificationScheduler}.
*
* Call {@link AwOriginVerificationScheduler#init} to initialize the statement list and call
* {@link AwOriginVerificationScheduler#validate} to perform a validation.
*/
public class AwOriginVerificationScheduler extends OriginVerificationScheduler {
private static final String TAG = "AwOriginVerification";

/** Lock on creation of sInstance. */
private static final Object sLock = new Object();

private static AwOriginVerificationScheduler sInstance;

private AwOriginVerificationScheduler(
AwOriginVerifier originVerifier, Set<Origin> pendingOrigins) {
super(originVerifier, pendingOrigins);
}

/**
* Initializes the AwOriginVerificationScheduler.
* This should be called exactly only once as it parses the AndroidManifest and statement list.
*
* @param packageName the package name of the host application.
* @param context a context associated with an Activity/Service to load resources.
*/
public static void init(String packageName, Context context) {
ThreadUtils.assertOnUiThread();
synchronized (sLock) {
assert sInstance
== null
: "`init(String packageName, Context context)` must only be called once";

sInstance = new AwOriginVerificationScheduler(
new AwOriginVerifier(packageName, OriginVerifier.HANDLE_ALL_URLS,
AwVerificationResultStore.getInstance()),
OriginVerifierHelper.getClaimedOriginsFromManifest(packageName, context));
}
}

public static void initAndScheduleAll(String packageName, Context context,
AwBrowserContext browserContext, @Nullable Callback<Boolean> callback) {
init(packageName, context);
synchronized (sLock) {
sInstance.scheduleAllPendingVerifications(browserContext, callback);
}
}

public static AwOriginVerificationScheduler getInstance() {
synchronized (sLock) {
return sInstance;
}
}
}
@@ -0,0 +1,62 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import androidx.annotation.Nullable;

import org.chromium.components.digital_asset_links.OriginVerifier;
import org.chromium.components.digital_asset_links.Relationship;
import org.chromium.components.embedder_support.util.Origin;

import java.util.List;

/**
* AwOriginVerifier performs OriginVerifications for WebView.
*/
public class AwOriginVerifier extends OriginVerifier {
public AwOriginVerifier(String packageName, String relationship,
@Nullable AwVerificationResultStore verificationResultStore) {
super(packageName, relationship, null, verificationResultStore);
}

@Override
public boolean isAllowlisted(String packageName, Origin origin, String relation) {
return false;
}

@Override
public boolean wasPreviouslyVerified(Origin origin) {
return wasPreviouslyVerified(mPackageName, mSignatureFingerprints, origin, mRelation);
}

/**
* Returns whether an origin is first-party relative to a given package name.
*
* This only returns data from previously cached relations, and does not trigger an asynchronous
* validation.
*
* @param packageName The package name.
* @param signatureFingerprint The signatures of the package.
* @param origin The origin to verify.
* @param relation The Digital Asset Links relation to verify for.
*/
private static boolean wasPreviouslyVerified(String packageName,
List<String> signatureFingerprints, Origin origin, String relation) {
AwVerificationResultStore resultStore = AwVerificationResultStore.getInstance();
return resultStore.shouldOverride(packageName, origin, relation)
|| resultStore.isRelationshipSaved(
new Relationship(packageName, signatureFingerprints, origin, relation));
}

@Override
public void recordResultMetrics(OriginVerifier.VerifierResult result) {
// TODO(crbug.com/1376958): Implement UMA logging.
}

@Override
public void recordVerificationTimeMetrics(long duration, boolean online) {
// TODO(crbug.com/1376958): Implement UMA logging.
}
}

0 comments on commit da24b14

Please sign in to comment.