brizental pushed to branch tor-browser-153.2.0esr-16.0-1 at The Tor Project / Applications / Tor Browser Commits: 068fb46d by Beatriz Rizental at 2026-09-10T19:33:43+02:00 TB 45621: Implement linter for Tor Browser-specific checks - - - - - 52a46146 by Beatriz Rizental at 2026-09-10T19:33:43+02:00 fixup! [android] Disable features and functionality Bug 45621: Prevent other apps from opening Tor Browser - - - - - 8 changed files: - mobile/android/fenix/app/src/main/AndroidManifest.xml - mobile/android/fenix/app/src/nightly/AndroidManifest.xml - + tools/lint/test/files/tor-browser-checks/bad/AndroidManifest.xml - + tools/lint/test/files/tor-browser-checks/good/AndroidManifest.xml - tools/lint/test/python.toml - + tools/lint/test/test_tor_browser_checks.py - + tools/lint/tor-browser-checks.yml - + tools/lint/tor-browser-checks/__init__.py Changes: ===================================== mobile/android/fenix/app/src/main/AndroidManifest.xml ===================================== @@ -505,7 +505,7 @@ <activity android:name=".HomeActivity" android:theme="@style/SplashScreenThemeBase" - android:exported="true" + android:exported="false" android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout" android:launchMode="singleTask" android:taskAffinity="" @@ -660,7 +660,7 @@ <service android:name=".customtabs.CustomTabsService" - android:exported="true" + android:exported="false" tools:ignore="ExportedService"> <intent-filter> <action android:name="android.support.customtabs.action.CustomTabsService" /> @@ -674,7 +674,7 @@ <receiver android:name="org.mozilla.gecko.search.SearchWidgetProvider" - android:exported="true"> + android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> @@ -684,7 +684,7 @@ </receiver> <receiver android:name=".onboarding.WidgetPinnedReceiver" - android:exported="true"> + android:exported="false"> <intent-filter> <action android:name="org.mozilla.fenix.onboarding.WidgetPinnedReceiver.widgetPinned"/> </intent-filter> @@ -790,7 +790,7 @@ <!-- https://dev.adjust.com/en/sdk/android/setup/preinstalled#system-installer-receiver--> <receiver android:name="com.adjust.sdk.AdjustPreinstallReferrerReceiver" - android:exported="true" + android:exported="false" tools:ignore="ExportedReceiver"> <intent-filter> <action android:name="com.attribution.SYSTEM_INSTALLER_REFERRER" /> @@ -813,7 +813,7 @@ <receiver android:name="org.mozilla.fenix.messaging.QAMessageNotificationWorkerReceiver" - android:exported="true" + android:exported="false" android:enabled="true" android:permission="android.permission.DUMP"> <intent-filter> @@ -823,7 +823,7 @@ <receiver android:name="org.mozilla.fenix.experiments.QANimbusToolingReceiver" - android:exported="true" + android:exported="false" android:enabled="true" android:permission="android.permission.DUMP"> <intent-filter> ===================================== mobile/android/fenix/app/src/nightly/AndroidManifest.xml ===================================== @@ -9,7 +9,7 @@ <service android:name=".customtabs.CustomTabsService" - android:exported="true"> + android:exported="false"> <intent-filter> <action android:name="android.support.customtabs.action.CustomTabsService" /> </intent-filter> @@ -20,7 +20,7 @@ <provider android:name=".perf.ProfilerProvider" android:authorities="${applicationId}.profiler" - android:exported="true" + android:exported="false" android:enabled="true" tools:replace="android:exported,android:enabled" /> ===================================== tools/lint/test/files/tor-browser-checks/bad/AndroidManifest.xml ===================================== @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application> + <activity + android:name=".IntentReceiverActivity" + android:exported="false"> + </activity> + + <!-- No intent-filter at all. Mirrors the original tor-browser#45145 + regression (org.mozilla.gecko.BrowserApp). --> + <activity-alias + android:name="org.mozilla.gecko.BrowserApp" + android:targetActivity=".IntentReceiverActivity" + android:exported="true"> + </activity-alias> + + <!-- Has its own "open this link" intent-filter, but that's VIEW, + not MAIN+LAUNCHER, so it still doesn't need to be exported. --> + <activity-alias + android:name="org.mozilla.gecko.PermissiveApp" + android:targetActivity=".IntentReceiverActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + </intent-filter> + </activity-alias> + + <!-- Has LAUNCHER without MAIN: still not a real launcher entry + point, so this should be flagged too since both are required. --> + <activity-alias + android:name="org.mozilla.gecko.LauncherCategoryOnlyApp" + android:targetActivity=".IntentReceiverActivity" + android:exported="true"> + <intent-filter> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity-alias> + + <!-- Not an activity at all: the check covers every exportable + component type, not just activities/aliases. --> + <receiver + android:name="com.example.evil.UnlistedReceiver" + android:exported="true"> + <intent-filter> + <action android:name="com.example.evil.SOME_ACTION" /> + </intent-filter> + </receiver> + </application> +</manifest> ===================================== tools/lint/test/files/tor-browser-checks/good/AndroidManifest.xml ===================================== @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application> + <activity + android:name=".IntentReceiverActivity" + android:exported="false"> + </activity> + + <activity-alias + android:name="org.mozilla.gecko.BrowserApp" + android:targetActivity=".IntentReceiverActivity" + android:exported="false"> + </activity-alias> + + <!-- Exported, but has a MAIN+LAUNCHER intent-filter: the one case + Android itself requires exported="true" for. --> + <activity-alias + android:name="${applicationId}.App" + android:exported="true" + android:targetActivity="HomeActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity-alias> + + <!-- Exported and not a MAIN+LAUNCHER entry point, but it doesn't + matter since exported="false" is always fine to flag as good. --> + <receiver + android:name="onboarding.WidgetPinnedReceiver" + android:exported="false"> + <intent-filter> + <action android:name="org.mozilla.fenix.onboarding.WidgetPinnedReceiver.widgetPinned" /> + </intent-filter> + </receiver> + </application> +</manifest> ===================================== tools/lint/test/python.toml ===================================== @@ -69,6 +69,10 @@ skip-if = ["os == 'win'"] ["test_shellcheck.py"] ["test_stylelint.py"] + +["test_tor_browser_checks.py"] +tags = "base-browser" + skip-if = ["os == 'win'"] # busts the tree for subsequent tasks on the same worker (bug 1708591) # Setup conflicts with eslint setup so this should run sequentially. sequential = true ===================================== tools/lint/test/test_tor_browser_checks.py ===================================== @@ -0,0 +1,29 @@ +import mozunit + +LINTER = "tor-browser-checks" + + +def test_flags_exported_non_launcher_components(lint, paths): + results = lint(paths("bad/AndroidManifest.xml")) + assert len(results) == 4 + + flagged_names = { + "org.mozilla.gecko.BrowserApp", + "org.mozilla.gecko.PermissiveApp", + "org.mozilla.gecko.LauncherCategoryOnlyApp", + "com.example.evil.UnlistedReceiver", + } + for expected_name in flagged_names: + assert any(expected_name in r.message for r in results) + + for r in results: + assert r.rule == "unnecessary-exported-component" + + +def test_allows_launcher_entries_and_unexported_components(lint, paths): + results = lint(paths("good/AndroidManifest.xml")) + assert len(results) == 0 + + +if __name__ == "__main__": + mozunit.main() ===================================== tools/lint/tor-browser-checks.yml ===================================== @@ -0,0 +1,16 @@ +--- +tor-browser-checks: + description: >- + Tor Browser specific static checks (one linter, several + independent checks -- see tools/lint/tor-browser-checks/__init__.py) + include: + - 'mobile/android/fenix/app/src/main/AndroidManifest.xml' + - 'mobile/android/fenix/app/src/debug/AndroidManifest.xml' + - 'mobile/android/fenix/app/src/beta/AndroidManifest.xml' + - 'mobile/android/fenix/app/src/release/AndroidManifest.xml' + - 'mobile/android/fenix/app/src/nightly/AndroidManifest.xml' + - 'mobile/android/fenix/app/src/benchmark/AndroidManifest.xml' + support-files: + - 'tools/lint/tor-browser-checks/**' + type: external + payload: tor-browser-checks:lint ===================================== tools/lint/tor-browser-checks/__init__.py ===================================== @@ -0,0 +1,124 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import xml.etree.ElementTree as ET + +from mozlint import result + +ANDROID_NS = "{http://schemas.android.com/apk/res/android}" + +# Component types that can carry android:exported. +EXPORTABLE_TAGS = ("activity", "activity-alias", "service", "receiver", "provider") + +MAIN_ACTION = "android.intent.action.MAIN" +LAUNCHER_CATEGORY = "android.intent.category.LAUNCHER" + + +def _find_lineno(lines, name): + # ElementTree doesn't track source positions. This is a best-effort fallback + # instead: find the line where this element's own android:name is written + # out. Good enough to jump to the right spot, but not a guarantee if `name` + # shows up as some other attribute's value too. + needle = f'name="{name}"' + for lineno, line in enumerate(lines, start=1): + if needle in line: + return lineno + return 0 + + +def _has_launcher_intent_filter(el): + # A MAIN+LAUNCHER intent-filter is the one case Android itself makes + # exported="true" load-bearing: that's what lets the home screen (a + # separate app, from the OS's point of view) resolve and start this + # component when its icon is tapped. Nothing else needs to be exported + # for the app to function -- everything else is a deliberate choice to + # let other apps reach in, and should default to false. + for intent_filter in el.findall("intent-filter"): + actions = {a.get(ANDROID_NS + "name") for a in intent_filter.findall("action")} + categories = { + c.get(ANDROID_NS + "name") for c in intent_filter.findall("category") + } + if MAIN_ACTION in actions and LAUNCHER_CATEGORY in categories: + return True + return False + + +def _check_only_launcher_entries_are_exported(path, config): + """Flags any exported component in a fenix AndroidManifest.xml that + isn't a MAIN+LAUNCHER entry point. + + This enforces the rule directly: android:exported="true" is only ever + structurally required for a component that's meant to be launched from + the home screen. Anything else that's exported can be set to exported="false" + instead, even if that means turning off the feature that needed it. + + Not autofixable: turning exported="false" off may mean losing the + feature that needed it (e.g. PWA shortcut relaunching), which needs a + human to decide whether that tradeoff is acceptable case by case. + + Only looks at AndroidManifest.xml fragments; anything else is skipped. + """ + if not path.endswith("AndroidManifest.xml"): + return [] + + try: + # Manifest fragments (e.g. src/nightly/AndroidManifest.xml) can be a + # bare <manifest> with no <application> at all -- nothing to check. + app = ET.parse(path).getroot().find("application") + except ET.ParseError: + return [] + if app is None: + return [] + + with open(path, encoding="utf-8") as f: + lines = f.readlines() + + issues = [] + for tag in EXPORTABLE_TAGS: + for el in app.findall(tag): + if el.get(ANDROID_NS + "exported") != "true": + continue + if _has_launcher_intent_filter(el): + continue + + name = el.get(ANDROID_NS + "name") + + issues.append( + result.from_config( + config, + path=path, + lineno=_find_lineno(lines, name), + message=( + f'<{tag}> "{name}" is exported="true" but has no ' + "MAIN+LAUNCHER intent-filter, so Android doesn't " + 'require it to be exported -- set exported="false" ' + "(see tor-browser#45145)." + ), + level="error", + rule="unnecessary-exported-component", + ), + ) + + return issues + + +# Add new Tor Browser security checks here. Each one receives a file path +# and is responsible for deciding whether that path is relevant to it, so +# checks are free to target entirely different kinds of files. +# +# Note: Remember to extend the linter's `include`/`extensions` in +# tor-browser-checks.yml if a new check needs paths that aren't already covered. +CHECKS = [ + _check_only_launcher_entries_are_exported, +] + + +def lint(paths, config, **lintargs): + results = [] + + for path in paths: + for check in CHECKS: + results.extend(check(path, config)) + + return results View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/8412f4a... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/8412f4a... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help
participants (1)
-
brizental (@brizental)