Chrome · WebView
CVE-2026-17953
Logic Error in WebView
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.java |
modified | |
ifandroid_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.java |
modified | |
ifandroid_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java |
modified |
Files Changed
android_webview/java/src/org/chromium/android_webview/common/AwSupervisedUserUrlClassifierDelegate.javaandroid_webview/java/src/org/chromium/android_webview/common/PlatformServiceBridge.javaandroid_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.javaandroid_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.javaandroid_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
Patch
From 35ef18f1aad291729f59c9c7acb7af92996c2513 Mon Sep 17 00:00:00 2001
From: Chidera Olibie <colibie@google.com>
Date: Tue, 09 Jun 2026 08:55:02 -0700
Subject: [PATCH] Only update cached SB/RCB preference if non-null result received
This CL updates Safe Browsing and Restricted Content Blocking config
helpers to only update their cached preferences (and log associated
histograms) when GMS queries return a non-null result (i.e. not on
timeout or error).
It also updates corresponding histograms.xml to document this behavior,
and adds test cases to verify it.
TAG=agy
CONV=151e4036-1bf7-4756-a458-17bb9a74d65f
Bug: 508162988, 517335150
Change-Id: I03aa75a1043647a696b0fdc479af55184f92fef1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7899996
Auto-Submit: Chidera Olibie <colibie@google.com>
Reviewed-by: Nate Fischer <ntfschr@chromium.org>
Reviewed-by: Daniel Rubery <drubery@chromium.org>
Reviewed-by: Michael van Ouwerkerk <mvanouwerkerk@chromium.org>
Commit-Queue: Daniel Rubery <drubery@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644001}
---
diff --git a/android_webview/java/src/org/chromium/android_webview/common/AwSupervisedUserUrlClassifierDelegate.java b/android_webview/java/src/org/chromium/android_webview/common/AwSupervisedUserUrlClassifierDelegate.java
index 2e86f943..ca7066bb2 100644
--- a/android_webview/java/src/org/chromium/android_webview/common/AwSupervisedUserUrlClassifierDelegate.java
+++ b/android_webview/java/src/org/chromium/android_webview/common/AwSupervisedUserUrlClassifierDelegate.java
@@ -6,6 +6,7 @@
import org.chromium.base.Callback;
import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
import org.chromium.url.GURL;
/**
@@ -35,5 +36,5 @@
* <p>callback.onResult(false) - indicates the user does not require restricted content
* blocking. callback.onResult(true) - indicates the user requires restricted content blocking.
*/
- void needsRestrictedContentBlocking(final Callback<Boolean> callback);
+ void needsRestrictedContentBlocking(final Callback<@Nullable Boolean> callback);
}
diff --git a/android_webview/java/src/org/chromium/android_webview/common/PlatformServiceBridge.java b/android_webview/java/src/org/chromium/android_webview/common/PlatformServiceBridge.java
index df35c5a..0c829bb1 100644
--- a/android_webview/java/src/org/chromium/android_webview/common/PlatformServiceBridge.java
+++ b/android_webview/java/src/org/chromium/android_webview/common/PlatformServiceBridge.java
@@ -63,7 +63,7 @@
}
// Overriding implementations may call "callback" asynchronously, on any thread.
- public void querySafeBrowsingUserConsent(final Callback<Boolean> callback) {
+ public void querySafeBrowsingUserConsent(final Callback<@Nullable Boolean> callback) {
// User opt-in preference depends on a SafetyNet API. In purely upstream builds (which don't
// communicate with GMS), assume the user has not opted in.
callback.onResult(false);
diff --git a/android_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.java b/android_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.java
index f64b87f4..d95a5eb 100644
--- a/android_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.java
+++ b/android_webview/java/src/org/chromium/android_webview/safe_browsing/AwSafeBrowsingConfigHelper.java
@@ -77,14 +77,15 @@
}
public static void maybeEnableSafeBrowsingFromGms() {
- Callback<Boolean> cb =
+ Callback<@Nullable Boolean> cb =
verifyAppsValue -> {
- ThreadUtils.postOnUiThread(
- () -> {
- AwSafeBrowsingConfigHelperJni.get()
- .setSafeBrowsingUserOptIn(
- Boolean.TRUE.equals(verifyAppsValue));
- });
+ if (verifyAppsValue != null) {
+ ThreadUtils.postOnUiThread(
+ () -> {
+ AwSafeBrowsingConfigHelperJni.get()
+ .setSafeBrowsingUserOptIn(verifyAppsValue);
+ });
+ }
};
PlatformServiceBridge.getInstance().querySafeBrowsingUserConsent(cb);
}
diff --git a/android_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.java b/android_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.java
index cf2bffa..43025df 100644
--- a/android_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.java
+++ b/android_webview/java/src/org/chromium/android_webview/supervised_user/AwSupervisedUserUrlClassifier.java
@@ -74,11 +74,13 @@
public void checkIfNeedRestrictedContentBlocking() {
mDelegate.needsRestrictedContentBlocking(
result -> {
- ThreadUtils.postOnUiThread(
- () -> {
- AwSupervisedUserUrlClassifierJni.get()
- .setUserRequiresUrlChecks(result);
- });
+ if (result != null) {
+ ThreadUtils.postOnUiThread(
+ () -> {
+ AwSupervisedUserUrlClassifierJni.get()
+ .setUserRequiresUrlChecks(result);
+ });
+ }
});
}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
index 9dc78d71..577147b 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
@@ -50,6 +50,7 @@
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.content_public.browser.MessagePayload;
@@ -420,13 +421,15 @@
private static class OnProgressChangedClient extends TestAwContentsClient {
private final CallbackHelper mCallbackHelper = new CallbackHelper();
+ private int mLastProgress = -1;
@Override
public void onProgressChanged(int progress) {
super.onProgressChanged(progress);
- if (progress == 100) {
+ if (progress == 100 && mLastProgress != 100) {
mCallbackHelper.notifyCalled();
}
+ mLastProgress = progress;
}
public void waitForFullLoad() throws TimeoutException {
@@ -462,7 +465,7 @@
// works.
private final Executor mExecutor = new BackgroundThreadExecutor("TEST_BACKGROUND_THREAD");
private final CallbackHelper mNeedsRestrictionHelper = new CallbackHelper();
- private boolean mNeedsRestrictionResponse;
+ private @Nullable Boolean mNeedsRestrictionResponse;
private static final Set RESTRICTED_CONTENT_BLOCKLIST =
Set.of(MATURE_SITE_PATH, MATURE_SITE_IFRAME_PATH);
@@ -477,7 +480,7 @@
}
@Override
- public void needsRestrictedContentBlocking(final Callback<Boolean> callback) {
+ public void needsRestrictedContentBlocking(final Callback<@Nullable Boolean> callback) {
mExecutor.execute(
() -> {
callback.onResult(mNeedsRestrictionResponse);
@@ -485,7 +488,7 @@
});
}
- public void setNeedsRestrictedContentBlockingResponse(boolean value) {
+ public void setNeedsRestrictedContentBlockingResponse(@Nullable Boolean value) {
mNeedsRestrictionResponse = value;
}
@@ -507,7 +510,7 @@
}
}
- private void resetNeedsRestriction(boolean value) throws Exception {
+ private void resetNeedsRestriction(@Nullable Boolean value) throws Exception {
mDelegate.setNeedsRestrictedContentBlockingResponse(value);
int count = mDelegate.getNeedsRestrictionHelper().getCallCount();
AwSupervisedUserUrlClassifier classifier = AwSupervisedUserUrlClassifier.getInstance();
@@ -516,4 +519,49 @@
classifier.checkIfNeedRestrictedContentBlocking();
mDelegate.getNeedsRestrictionHelper().waitForCallback(count);
}
+
+ @Test
+ @SmallTest
+ @Feature({"AndroidWebView"})
+ public void testRestrictedContentBlockingNullDoesNotUpdateCache() throws Throwable {
+ String embeddedUrl = setUpWebPage(MATURE_SITE_IFRAME_PATH, MATURE_SITE_IFRAME_TITLE, null);
+ String requestUrl = setUpWebPage(MATURE_SITE_PATH, MATURE_SITE_TITLE, embeddedUrl);
+
+ // Start with restriction enabled (setUp sets it to true, but let's be explicit)
+ resetNeedsRestriction(true);
+ loadUrl(requestUrl);
+ assertPageTitle(BLOCKED_SITE_TITLE);
+
+ // Now set restriction response to null (simulating timeout/error)
+ // It should NOT update the cache, so restriction should remain enabled (mature pages
+ // blocked)
+ // We also check that the histogram is NOT recorded.
+ try (HistogramWatcher watcher =
+ HistogramWatcher.newBuilder()
+ .expectNoRecords(
+ "Android.WebView.RestrictedContentBlocking.ApiCallMatchesDiskCache")
+ .build()) {
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
index 9dc78d71..577147b 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwSupervisedUserTest.java
@@ -50,6 +50,7 @@
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.content_public.browser.MessagePayload;
@@ -420,13 +421,15 @@
private static class OnProgressChangedClient extends TestAwContentsClient {
private final CallbackHelper mCallbackHelper = new CallbackHelper();
+ private int mLastProgress = -1;
@Override
public void onProgressChanged(int progress) {
super.onProgressChanged(progress);
- if (progress == 100) {
+ if (progress == 100 && mLastProgress != 100) {
mCallbackHelper.notifyCalled();
}
+ mLastProgress = progress;
}
public void waitForFullLoad() throws TimeoutException {
@@ -462,7 +465,7 @@
// works.
private final Executor mExecutor = new BackgroundThreadExecutor("TEST_BACKGROUND_THREAD");
private final CallbackHelper mNeedsRestrictionHelper = new CallbackHelper();
- private boolean mNeedsRestrictionResponse;
+ private @Nullable Boolean mNeedsRestrictionResponse;
private static final Set RESTRICTED_CONTENT_BLOCKLIST =
Set.of(MATURE_SITE_PATH, MATURE_SITE_IFRAME_PATH);
@@ -477,7 +480,7 @@
}
@Override
- public void needsRestrictedContentBlocking(final Callback<Boolean> callback) {
+ public void needsRestrictedContentBlocking(final Callback<@Nullable Boolean> callback) {
mExecutor.execute(
() -> {
callback.onResult(mNeedsRestrictionResponse);
@@ -485,7 +488,7 @@
});
}
- public void setNeedsRestrictedContentBlockingResponse(boolean value) {
+ public void setNeedsRestrictedContentBlockingResponse(@Nullable Boolean value) {
mNeedsRestrictionResponse = value;
}
@@ -507,7 +510,7 @@
}
}
- private void resetNeedsRestriction(boolean value) throws Exception {
+ private void resetNeedsRestriction(@Nullable Boolean value) throws Exception {
mDelegate.setNeedsRestrictedContentBlockingResponse(value);
int count = mDelegate.getNeedsRestrictionHelper().getCallCount();
AwSupervisedUserUrlClassifier classifier = AwSupervisedUserUrlClassifier.getInstance();
@@ -516,4 +519,49 @@
classifier.checkIfNeedRestrictedContentBlocking();
mDelegate.getNeedsRestrictionHelper().waitForCallback(count);
}
+
+ @Test
+ @SmallTest
+ @Feature({"AndroidWebView"})
+ public void testRestrictedContentBlockingNullDoesNotUpdateCache() throws Throwable {
+ String embeddedUrl = setUpWebPage(MATURE_SITE_IFRAME_PATH, MATURE_SITE_IFRAME_TITLE, null);
+ String requestUrl = setUpWebPage(MATURE_SITE_PATH, MATURE_SITE_TITLE, embeddedUrl);
+
+ // Start with restriction enabled (setUp sets it to true, but let's be explicit)
+ resetNeedsRestriction(true);
+ loadUrl(requestUrl);
+ assertPageTitle(BLOCKED_SITE_TITLE);
+
+ // Now set restriction response to null (simulating timeout/error)
+ // It should NOT update the cache, so restriction should remain enabled (mature pages
+ // blocked)
+ // We also check that the histogram is NOT recorded.
+ try (HistogramWatcher watcher =
+ HistogramWatcher.newBuilder()
+ .expectNoRecords(
+ "Android.WebView.RestrictedContentBlocking.ApiCallMatchesDiskCache")
+ .build()) {
+ resetNeedsRestriction(null);
+ loadUrl(requestUrl);
+ assertPageTitle(BLOCKED_SITE_TITLE);
+ }
+
+ // Now set restriction to false
+ resetNeedsRestriction(false);
+ loadUrl(requestUrl);
+ assertPageTitle(MATURE_SITE_TITLE);
+
+ // Now set restriction response to null again
+ // It should NOT update the cache, so restriction should remain disabled (mature pages
+ // allowed)
+ try (HistogramWatcher watcher =
+ HistogramWatcher.newBuilder()
+ .expectNoRecords(
+ "Android.WebView.RestrictedContentBlocking.ApiCallMatchesDiskCache")
+ .build()) {
+ resetNeedsRestriction(null);
+ loadUrl(requestUrl);
+ assertPageTitle(MATURE_SITE_TITLE);
+ }
+ }
}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
index 84c84447..dab5556 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
@@ -56,6 +56,7 @@
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.HistogramWatcher;
+import org.chromium.build.annotations.Nullable;
import org.chromium.components.safe_browsing.SafeBrowsingApiBridge;
import org.chromium.components.safe_browsing.SafeBrowsingApiHandler;
import org.chromium.net.test.EmbeddedTestServer;
@@ -183,8 +184,8 @@
* A fake PlatformServiceBridge that allows tests to make safe browsing requests without GMS.
*/
private static class MockPlatformServiceBridge extends PlatformServiceBridge {
- private Callback<Boolean> mCallback;
- private Boolean mConsent;
+ private Callback<@Nullable Boolean> mCallback;
+ private @Nullable Boolean mConsent;
@Override
public boolean canUseGms() {
@@ -192,7 +193,7 @@
}
@Override
- public void querySafeBrowsingUserConsent(Callback<Boolean> callback) {
+ public void querySafeBrowsingUserConsent(Callback<@Nullable Boolean> callback) {
mCallback = callback;
if (mConsent != null) {
callback.onResult(mConsent);
@@ -201,7 +202,7 @@
public void setConsent(Boolean consent) {
mConsent = consent;
- if (mCallback != null && consent != null) {
+ if (mCallback != null) {
mCallback.onResult(consent);
}
}
@@ -1281,4 +1282,49 @@
Assert.assertFalse(AwSafeBrowsingConfigHelper.getSafeBrowsingUserOptInForTesting());
}
}
+
+ @Test
+ @SmallTest
+ @Feature({"AndroidWebView"})
+ public void testSafeBrowsingUserOptInNullDoesNotUpdateCache() throws Throwable {
+ MockPlatformServiceBridge bridge =
+ (MockPlatformServiceBridge) PlatformServiceBridge.getInstance();
+
+ // Set initial consent to true in pref
+ AwSafeBrowsingConfigHelper.setSafeBrowsingUserOptInForTesting(true);
+
+ bridge.setConsent(null); // Don't return immediately
+
+ AwSafeBrowsingConfigHelper.maybeEnableSafeBrowsingFromGms();
+
+ // Now trigger callback with null (simulating timeout/error)
+ // We expect that the cache is NOT updated (stays true)
+ // And we expect NO histogram record for ApiCallMatchesDiskCache
+ try (HistogramWatcher watcher =
+ HistogramWatcher.newBuilder()
+ .expectNoRecords("SafeBrowsing.WebView.GmsOptIn.ApiCallMatchesDiskCache")
+ .build()) {
+ bridge.setConsent(null);
+ // Waits for posted task from callback to complete (if any)
+ // and verifies pref is still true
+ Assert.assertTrue(AwSafeBrowsingConfigHelper.getSafeBrowsingUserOptInForTesting());
+ }
+
+ // Set initial consent to false in pref
+ AwSafeBrowsingConfigHelper.setSafeBrowsingUserOptInForTesting(false);
+
+ bridge.setConsent(null); // Reset
+
+ AwSafeBrowsingConfigHelper.maybeEnableSafeBrowsingFromGms();
+
+ // Trigger callback with null again
+ // We expect that the cache is NOT updated (stays false)
+ try (HistogramWatcher watcher =
+ HistogramWatcher.newBuilder()
+ .expectNoRecords("SafeBrowsing.WebView.GmsOptIn.ApiCallMatchesDiskCache")
+ .build()) {
+ bridge.setConsent(null);
+ Assert.assertFalse(AwSafeBrowsingConfigHelper.getSafeBrowsingUserOptInForTesting());
+ }
+ }
}
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page