Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d521ace
app fix
turtledreams May 13, 2026
4a78040
feat: remove build config to inside if
arifBurakDemiray May 14, 2026
fef389a
Merge pull request #566 from Countly/app-fix
arifBurakDemiray May 14, 2026
c17e043
Native crash fix
turtledreams Jun 5, 2026
fb63476
Merge pull request #567 from Countly/native-crash-fix
arifBurakDemiray Jun 8, 2026
63d5594
feat: automatic sbs settings and jtv
arifBurakDemiray Jun 10, 2026
ad598c1
Merge pull request #568 from Countly/sbs-automatic-tracking-jtv
turtledreams Jun 10, 2026
0ef457e
fix: temp id leak on ratings
arifBurakDemiray Jun 23, 2026
8a85584
feat: test for re-fetch contents after temp id
arifBurakDemiray Jun 23, 2026
1bb7dbe
Merge pull request #569 from Countly/fix_temp_id
arifBurakDemiray Jun 23, 2026
c6b91ad
feat: a config switch to disable all webview based UI
arifBurakDemiray Jun 23, 2026
5755804
feat: more checks
arifBurakDemiray Jun 23, 2026
6c5e047
feat: rename config
arifBurakDemiray Jun 24, 2026
2c73010
feat: config for disabling logging in production builds
arifBurakDemiray Jun 25, 2026
30053ed
Merge pull request #571 from Countly/disable-sdk-logging-in-production
arifBurakDemiray Jun 25, 2026
35f2303
Merge branch 'staging' into disable_webview_ui
arifBurakDemiray Jun 29, 2026
d8402c0
feat: pn and content security
arifBurakDemiray Jun 29, 2026
d17dbc6
Merge branch 'staging' into pn_security
arifBurakDemiray Jun 29, 2026
e198907
fix: changes after review
arifBurakDemiray Jun 29, 2026
c8f77cf
fix: ratings too
arifBurakDemiray Jun 30, 2026
c8f340d
feat: more tests
arifBurakDemiray Jun 30, 2026
20b53ac
Merge pull request #570 from Countly/disable_webview_ui
arifBurakDemiray Jun 30, 2026
b519550
Merge branch 'staging' into pn_security
arifBurakDemiray Jun 30, 2026
82c093a
fix: null check
arifBurakDemiray Jun 30, 2026
1b343e1
fix: add null check
arifBurakDemiray Jun 30, 2026
8356fa8
feat: last test additions
arifBurakDemiray Jun 30, 2026
9b1d810
fix: HealthCheck NPE
Jun 30, 2026
9c066fc
feat: final review changes
arifBurakDemiray Jul 1, 2026
0d45fbf
fix: curly brackets tabbing
arifBurakDemiray Jul 1, 2026
f3cefdb
Update CHANGELOG.md
arifBurakDemiray Jul 1, 2026
ffe22e6
feat: more tests
arifBurakDemiray Jul 1, 2026
19f9a50
Merge pull request #574 from dunkpi/health_counter_npe_fix
arifBurakDemiray Jul 1, 2026
d94395e
Merge branch 'staging' into pn_security
arifBurakDemiray Jul 1, 2026
46e2b57
chore: make changelog basic
arifBurakDemiray Jul 1, 2026
5f7a54a
Merge branch 'pn_security' of https://github.com/Countly/countly-sdk-…
arifBurakDemiray Jul 1, 2026
87ea0c1
Merge pull request #572 from Countly/pn_security
arifBurakDemiray Jul 1, 2026
ccb5ae5
feat: 26.1.4
arifBurakDemiray Jul 1, 2026
6c93af7
Merge pull request #575 from Countly/26_1_4
arifBurakDemiray Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
## 26.1.4
* ! Minor breaking change ! Deprecated the static field "CountlyPush.useAdditionalIntentRedirectionChecks". It is now a no-op; use "CountlyConfigPush.enableAdditionalIntentRedirectionChecks()" instead, otherwise the stricter push intent redirection checks stay disabled.

* Added support for SDK behavior settings that control the SDK's automatic session tracking, automatic view tracking, automatic crash reporting, and Journey Trigger Views.
* Added a new push configuration option "enableAdditionalIntentRedirectionChecks()" to enable stricter validation of the notification intent's target package and class.
* Added a new content configuration option "setAllowedIntentSchemes(List)" to restrict which URI schemes content and feedback widget links may open.
* Added a new push configuration option "setAllowedIntentSchemes(List)" to restrict which URI schemes notification links may open.
* Added a new configuration option "disableWebView()" to disable all WebView-based UI in the SDK.
* Added a new config option "disableSDKLoggingInProduction()" that keeps the SDK's console logging disabled in production (non-debuggable) builds, even when logging is enabled.
* Improved the security of the content, feedback widget, rating widget and push notifications.

* Mitigated an issue where a native crash dump was truncated by the stack trace line length limit when a global crash filter was set.
* Mitigated an issue where the rating feedback popup request could be sent while in temporary device ID mode, creating a `CLYTemporaryDeviceID` user on the server.
* Mitigated an issue where the content zone did not resume after exiting temporary device ID mode even when it was enabled by the server configuration.
* Mitigated an issue while sending health checks after SDK is halted.

## 26.1.3
* Added gradle configuration cache support to upload symbols plugin.
* Improved user properties auto-save conditions to flush event queue with every user property call.
Expand Down
17 changes: 6 additions & 11 deletions app/src/main/java/ly/count/android/demo/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,13 @@ public void onCreate() {
WebView.setWebContentsDebuggingEnabled(true);
}

COUNTLY_SERVER_URL =
DEFAULT_URL.equals(BuildConfig.COUNTLY_SERVER_URL)
? DEFAULT_URL
: BuildConfig.COUNTLY_SERVER_URL;
COUNTLY_APP_KEY =
DEFAULT_APP_KEY.equals(BuildConfig.COUNTLY_APP_KEY)
? DEFAULT_APP_KEY
: BuildConfig.COUNTLY_APP_KEY;

if (DEFAULT_URL.equals(COUNTLY_SERVER_URL) || DEFAULT_APP_KEY.equals(COUNTLY_APP_KEY)) {
Log.e("CountlyDemo", "Please provide correct COUNTLY_SERVER_URL and COUNTLY_APP_KEY");
return;
COUNTLY_SERVER_URL = BuildConfig.COUNTLY_SERVER_URL;
COUNTLY_APP_KEY = BuildConfig.COUNTLY_APP_KEY;
if (DEFAULT_URL.equals(COUNTLY_SERVER_URL) || DEFAULT_APP_KEY.equals(COUNTLY_APP_KEY)) {
Log.e("CountlyDemo", "Please provide correct COUNTLY_SERVER_URL and COUNTLY_APP_KEY");
return;
}
}

if (false) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ org.gradle.configureondemand=true
android.useAndroidX=true
android.enableJetifier=true
# RELEASE FIELD SECTION
VERSION_NAME=26.1.3
VERSION_NAME=26.1.4
GROUP=ly.count.android
POM_URL=https://github.com/Countly/countly-sdk-android
POM_SCM_URL=https://github.com/Countly/countly-sdk-android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ public void setUp() {
return true;
}

@Override public boolean getAutomaticSessionTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticViewTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticCrashReportingEnabled() {
return true;
}

@Override public boolean getLocationTrackingEnabled() {
return true;
}
Expand Down Expand Up @@ -158,6 +170,10 @@ public void setUp() {
@Override public Set<String> getJourneyTriggerEvents() {
return Collections.emptySet();
}

@Override public Set<String> getJourneyTriggerViews() {
return Collections.emptySet();
}
};

Countly.sharedInstance().setLoggingEnabled(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ private ContentOverlayView createOverlay(Activity activity,
activity.getResources().getConfiguration().orientation,
callback,
onClose != null ? onClose : () -> {
}
},
null
);
}

Expand Down Expand Up @@ -505,7 +506,7 @@ public void configs_storedCorrectly() {
overlay = new ContentOverlayView(
activity, portrait, landscape,
Configuration.ORIENTATION_PORTRAIT, null, () -> {
});
}, null);

// Note: setupConfig may modify width/height if < 1, but ours are > 0
Assert.assertEquals(10, (int) overlay.configPortrait.x);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ void assertDefaultValues(CountlyConfig config, boolean includeConstructorValues)
Assert.assertNull(config.starRatingTextMessage);
Assert.assertNull(config.starRatingTextTitle);
Assert.assertFalse(config.loggingEnabled);
Assert.assertFalse(config.disableSDKLoggingInProduction);
Assert.assertFalse(config.crashes.enableUnhandledCrashReporting);
Assert.assertFalse(config.enableAutomaticViewTracking);
Assert.assertFalse(config.autoTrackingUseShortName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
Expand Down Expand Up @@ -315,4 +317,84 @@ public void criticalResource_imageExtensions_detected() {
Assert.assertEquals(1, callbackResults.size());
Assert.assertTrue(callbackResults.get(0));
}

// =====================================
// shouldInterceptRequest - sub-resource scheme blocking
// =====================================

/**
* "shouldInterceptRequest" with http(s) sub-resources should return null so they load normally.
*/
@Test
public void shouldInterceptRequest_httpAndHttps_allowed() {
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("https://example.com/photo.png", false)));
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("http://example.com/app.js", false)));
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("HTTPS://EXAMPLE.COM/x.css", true)));
}

/**
* "shouldInterceptRequest" must block every local-data / script scheme an attacker could use to
* read local data or run script from a content sub-resource (img/iframe/script/xhr): file://,
* content://, javascript:, jar:file://, plus a file:// pointing at app-private storage. "data:"
* and "blob:" are NOT blocked here — they are inline / runtime-generated assets widgets embed
* (covered by shouldInterceptRequest_inlineAssetSchemes_allowed).
*/
@Test
public void shouldInterceptRequest_nonWebSchemes_blocked() {
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("file:///data/data/ly.count.android.sdk/shared_prefs/secret.xml", false)));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("file:///etc/hosts", false)));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("content://com.app.provider/private", false)));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("content://media/external/images/media/1", false)));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("javascript:alert(document.cookie)", false)));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("jar:file:///data/app/x.apk!/a.html", false)));
// also blocked for a main-frame request, not just sub-resources
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("file:///data/data/ly.count.android.sdk/databases/countly.db", true)));
}

/**
* "data:" and "blob:" sub-resources (inline images/fonts/CSS and runtime-generated assets that
* widgets legitimately embed) load in the default (no allow-list) mode, but like any other
* non-https scheme they are blocked in allow-list mode unless the integrator lists them.
*/
@Test
public void shouldInterceptRequest_inlineAssetSchemes_defaultAllowedAllowlistGoverned() {
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("data:image/png;base64,iVBORw0KGgo=", false)));
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("blob:https://example.com/uuid", false)));
// allow-list mode governs data/blob (not force-allowed): blocked unless listed
CountlyWebViewClient allowlisted = new CountlyWebViewClient(new HashSet<>(Arrays.asList("myapp")));
assertBlocked(allowlisted.shouldInterceptRequest(null, fakeRequest("data:image/png;base64,iVBORw0KGgo=", false)));
assertBlocked(allowlisted.shouldInterceptRequest(null, fakeRequest("blob:https://example.com/uuid", false)));
// https still always loads, and an explicitly listed inline scheme loads
Assert.assertNull(allowlisted.shouldInterceptRequest(null, fakeRequest("https://example.com/a.png", false)));
CountlyWebViewClient dataAllowed = new CountlyWebViewClient(new HashSet<>(Arrays.asList("data")));
Assert.assertNull(dataAllowed.shouldInterceptRequest(null, fakeRequest("data:image/png;base64,iVBORw0KGgo=", false)));
}

private void assertBlocked(WebResourceResponse response) {
Assert.assertNotNull(response);
Assert.assertNotNull(response.getData());
}

/**
* With a configured scheme allow-list, sub-resources follow allow-list mode: listed schemes load
* and everything else is blocked, EXCEPT https which always loads because it serves the content
* itself (so an outbound-link allow-list does not break the page's own https assets). Plain http
* is NOT auto-allowed — it must be listed explicitly, so the integrator decides whether to permit it.
*/
@Test
public void shouldInterceptRequest_allowlistMode() {
CountlyWebViewClient allowlisted = new CountlyWebViewClient(new HashSet<>(Arrays.asList("myapp")));
// https always loads regardless of the allow-list
Assert.assertNull(allowlisted.shouldInterceptRequest(null, fakeRequest("https://example.com/a.png", false)));
// a listed non-web scheme loads
Assert.assertNull(allowlisted.shouldInterceptRequest(null, fakeRequest("myapp://x", false)));
// http is not auto-allowed: blocked unless explicitly listed
assertBlocked(allowlisted.shouldInterceptRequest(null, fakeRequest("http://example.com/a.png", false)));
// other unlisted non-web schemes are blocked
assertBlocked(allowlisted.shouldInterceptRequest(null, fakeRequest("market://details?id=x", false)));
assertBlocked(allowlisted.shouldInterceptRequest(null, fakeRequest("file:///etc/hosts", false)));
// http loads when explicitly listed
CountlyWebViewClient httpAllowed = new CountlyWebViewClient(new HashSet<>(Arrays.asList("http")));
Assert.assertNull(httpAllowed.shouldInterceptRequest(null, fakeRequest("http://example.com/a.png", false)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package ly.count.android.sdk;

import android.net.Uri;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
* Locks in the security hardening of the rating/feedback-dialog WebView client (the twin of
* {@link CountlyWebViewClient}, which is covered by CountlyWebViewClientTests). Without these, a
* regression that drops the scheme blocking or the allow-list threading would ship silently.
*/
@RunWith(AndroidJUnit4.class)
public class FeedbackDialogWebViewClientTests {

private WebResourceRequest fakeRequest(String url) {
Uri uri = Uri.parse(url);
return new WebResourceRequest() {
@Override public Uri getUrl() {
return uri;
}

@Override public boolean isForMainFrame() {
return false;
}

@Override public boolean isRedirect() {
return false;
}

@Override public boolean hasGesture() {
return false;
}

@Override public String getMethod() {
return "GET";
}

@Override public Map<String, String> getRequestHeaders() {
return new HashMap<>();
}
};
}

private void assertBlocked(WebResourceResponse response) {
Assert.assertNotNull(response);
Assert.assertNotNull(response.getData());
}

/** Dangerous local/script sub-resource schemes are blocked; https/http load (default denylist). */
@Test
public void shouldInterceptRequest_defaultDenylist() {
ModuleRatings.FeedbackDialogWebViewClient client = new ModuleRatings.FeedbackDialogWebViewClient(null);
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("https://example.com/a.png")));
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("http://example.com/a.js")));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("file:///data/data/ly.count.android.sdk/shared_prefs/secret.xml")));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("content://com.app.provider/private")));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("javascript:alert(document.cookie)")));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("jar:file:///x.apk!/a.html")));
// data:/blob: are inline / runtime-generated assets widgets embed -> load normally
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("data:image/png;base64,iVBORw0KGgo=")));
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("blob:https://example.com/uuid")));
}

/** With a configured allow-list, https still loads but http and other unlisted schemes are blocked. */
@Test
public void shouldInterceptRequest_allowlistThreaded() {
ModuleRatings.FeedbackDialogWebViewClient client =
new ModuleRatings.FeedbackDialogWebViewClient(new HashSet<>(Arrays.asList("myapp")));
// https always loads (serves the widget itself)
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("https://example.com/a.png")));
// a listed non-web scheme loads
Assert.assertNull(client.shouldInterceptRequest(null, fakeRequest("myapp://x")));
// http is not auto-allowed in allow-list mode unless listed
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("http://example.com/a.png")));
// unlisted non-web schemes blocked
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("market://details?id=x")));
assertBlocked(client.shouldInterceptRequest(null, fakeRequest("file:///etc/hosts")));
}

/** The deprecated String overload must not NPE on a null url (a null scheme is blocked, fail-secure). */
@Test
public void shouldInterceptRequest_stringOverload_nullSafe() {
ModuleRatings.FeedbackDialogWebViewClient client = new ModuleRatings.FeedbackDialogWebViewClient(null);
assertBlocked(client.shouldInterceptRequest(null, (String) null)); // null url -> null scheme -> blocked, no NPE
assertBlocked(client.shouldInterceptRequest(null, "file:///etc/hosts"));
Assert.assertNull(client.shouldInterceptRequest(null, "https://example.com/a.png"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,18 @@ private ConfigurationProvider createConfigurationProvider() {
return true;
}

@Override public boolean getAutomaticSessionTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticViewTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticCrashReportingEnabled() {
return true;
}

@Override public boolean getLocationTrackingEnabled() {
return true;
}
Expand Down Expand Up @@ -932,6 +944,10 @@ private ConfigurationProvider createConfigurationProvider() {
@Override public Set<String> getJourneyTriggerEvents() {
return Collections.emptySet();
}

@Override public Set<String> getJourneyTriggerViews() {
return Collections.emptySet();
}
};
}

Expand Down
Loading
Loading