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
-
52a46146
by Beatriz Rizental at 2026-09-10T19:33:43+02:00
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:
| ... | ... | @@ -505,7 +505,7 @@ |
| 505 | 505 | <activity
|
| 506 | 506 | android:name=".HomeActivity"
|
| 507 | 507 | android:theme="@style/SplashScreenThemeBase"
|
| 508 | - android:exported="true"
|
|
| 508 | + android:exported="false"
|
|
| 509 | 509 | android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
|
| 510 | 510 | android:launchMode="singleTask"
|
| 511 | 511 | android:taskAffinity=""
|
| ... | ... | @@ -660,7 +660,7 @@ |
| 660 | 660 | |
| 661 | 661 | <service
|
| 662 | 662 | android:name=".customtabs.CustomTabsService"
|
| 663 | - android:exported="true"
|
|
| 663 | + android:exported="false"
|
|
| 664 | 664 | tools:ignore="ExportedService">
|
| 665 | 665 | <intent-filter>
|
| 666 | 666 | <action android:name="android.support.customtabs.action.CustomTabsService" />
|
| ... | ... | @@ -674,7 +674,7 @@ |
| 674 | 674 | |
| 675 | 675 | <receiver
|
| 676 | 676 | android:name="org.mozilla.gecko.search.SearchWidgetProvider"
|
| 677 | - android:exported="true">
|
|
| 677 | + android:exported="false">
|
|
| 678 | 678 | <intent-filter>
|
| 679 | 679 | <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
| 680 | 680 | </intent-filter>
|
| ... | ... | @@ -684,7 +684,7 @@ |
| 684 | 684 | </receiver>
|
| 685 | 685 | |
| 686 | 686 | <receiver android:name=".onboarding.WidgetPinnedReceiver"
|
| 687 | - android:exported="true">
|
|
| 687 | + android:exported="false">
|
|
| 688 | 688 | <intent-filter>
|
| 689 | 689 | <action android:name="org.mozilla.fenix.onboarding.WidgetPinnedReceiver.widgetPinned"/>
|
| 690 | 690 | </intent-filter>
|
| ... | ... | @@ -790,7 +790,7 @@ |
| 790 | 790 | <!-- https://dev.adjust.com/en/sdk/android/setup/preinstalled#system-installer-receiver-->
|
| 791 | 791 | <receiver
|
| 792 | 792 | android:name="com.adjust.sdk.AdjustPreinstallReferrerReceiver"
|
| 793 | - android:exported="true"
|
|
| 793 | + android:exported="false"
|
|
| 794 | 794 | tools:ignore="ExportedReceiver">
|
| 795 | 795 | <intent-filter>
|
| 796 | 796 | <action android:name="com.attribution.SYSTEM_INSTALLER_REFERRER" />
|
| ... | ... | @@ -813,7 +813,7 @@ |
| 813 | 813 | |
| 814 | 814 | <receiver
|
| 815 | 815 | android:name="org.mozilla.fenix.messaging.QAMessageNotificationWorkerReceiver"
|
| 816 | - android:exported="true"
|
|
| 816 | + android:exported="false"
|
|
| 817 | 817 | android:enabled="true"
|
| 818 | 818 | android:permission="android.permission.DUMP">
|
| 819 | 819 | <intent-filter>
|
| ... | ... | @@ -823,7 +823,7 @@ |
| 823 | 823 | |
| 824 | 824 | <receiver
|
| 825 | 825 | android:name="org.mozilla.fenix.experiments.QANimbusToolingReceiver"
|
| 826 | - android:exported="true"
|
|
| 826 | + android:exported="false"
|
|
| 827 | 827 | android:enabled="true"
|
| 828 | 828 | android:permission="android.permission.DUMP">
|
| 829 | 829 | <intent-filter>
|
| ... | ... | @@ -9,7 +9,7 @@ |
| 9 | 9 | |
| 10 | 10 | <service
|
| 11 | 11 | android:name=".customtabs.CustomTabsService"
|
| 12 | - android:exported="true">
|
|
| 12 | + android:exported="false">
|
|
| 13 | 13 | <intent-filter>
|
| 14 | 14 | <action android:name="android.support.customtabs.action.CustomTabsService" />
|
| 15 | 15 | </intent-filter>
|
| ... | ... | @@ -20,7 +20,7 @@ |
| 20 | 20 | <provider
|
| 21 | 21 | android:name=".perf.ProfilerProvider"
|
| 22 | 22 | android:authorities="${applicationId}.profiler"
|
| 23 | - android:exported="true"
|
|
| 23 | + android:exported="false"
|
|
| 24 | 24 | android:enabled="true"
|
| 25 | 25 | tools:replace="android:exported,android:enabled" />
|
| 26 | 26 |
| 1 | +<?xml version="1.0" encoding="utf-8"?>
|
|
| 2 | +<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
| 3 | + <application>
|
|
| 4 | + <activity
|
|
| 5 | + android:name=".IntentReceiverActivity"
|
|
| 6 | + android:exported="false">
|
|
| 7 | + </activity>
|
|
| 8 | + |
|
| 9 | + <!-- No intent-filter at all. Mirrors the original tor-browser#45145
|
|
| 10 | + regression (org.mozilla.gecko.BrowserApp). -->
|
|
| 11 | + <activity-alias
|
|
| 12 | + android:name="org.mozilla.gecko.BrowserApp"
|
|
| 13 | + android:targetActivity=".IntentReceiverActivity"
|
|
| 14 | + android:exported="true">
|
|
| 15 | + </activity-alias>
|
|
| 16 | + |
|
| 17 | + <!-- Has its own "open this link" intent-filter, but that's VIEW,
|
|
| 18 | + not MAIN+LAUNCHER, so it still doesn't need to be exported. -->
|
|
| 19 | + <activity-alias
|
|
| 20 | + android:name="org.mozilla.gecko.PermissiveApp"
|
|
| 21 | + android:targetActivity=".IntentReceiverActivity"
|
|
| 22 | + android:exported="true">
|
|
| 23 | + <intent-filter>
|
|
| 24 | + <action android:name="android.intent.action.VIEW" />
|
|
| 25 | + <category android:name="android.intent.category.BROWSABLE" />
|
|
| 26 | + <category android:name="android.intent.category.DEFAULT" />
|
|
| 27 | + <data android:scheme="http" />
|
|
| 28 | + <data android:scheme="https" />
|
|
| 29 | + </intent-filter>
|
|
| 30 | + </activity-alias>
|
|
| 31 | + |
|
| 32 | + <!-- Has LAUNCHER without MAIN: still not a real launcher entry
|
|
| 33 | + point, so this should be flagged too since both are required. -->
|
|
| 34 | + <activity-alias
|
|
| 35 | + android:name="org.mozilla.gecko.LauncherCategoryOnlyApp"
|
|
| 36 | + android:targetActivity=".IntentReceiverActivity"
|
|
| 37 | + android:exported="true">
|
|
| 38 | + <intent-filter>
|
|
| 39 | + <category android:name="android.intent.category.LAUNCHER" />
|
|
| 40 | + </intent-filter>
|
|
| 41 | + </activity-alias>
|
|
| 42 | + |
|
| 43 | + <!-- Not an activity at all: the check covers every exportable
|
|
| 44 | + component type, not just activities/aliases. -->
|
|
| 45 | + <receiver
|
|
| 46 | + android:name="com.example.evil.UnlistedReceiver"
|
|
| 47 | + android:exported="true">
|
|
| 48 | + <intent-filter>
|
|
| 49 | + <action android:name="com.example.evil.SOME_ACTION" />
|
|
| 50 | + </intent-filter>
|
|
| 51 | + </receiver>
|
|
| 52 | + </application>
|
|
| 53 | +</manifest> |
| 1 | +<?xml version="1.0" encoding="utf-8"?>
|
|
| 2 | +<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
| 3 | + <application>
|
|
| 4 | + <activity
|
|
| 5 | + android:name=".IntentReceiverActivity"
|
|
| 6 | + android:exported="false">
|
|
| 7 | + </activity>
|
|
| 8 | + |
|
| 9 | + <activity-alias
|
|
| 10 | + android:name="org.mozilla.gecko.BrowserApp"
|
|
| 11 | + android:targetActivity=".IntentReceiverActivity"
|
|
| 12 | + android:exported="false">
|
|
| 13 | + </activity-alias>
|
|
| 14 | + |
|
| 15 | + <!-- Exported, but has a MAIN+LAUNCHER intent-filter: the one case
|
|
| 16 | + Android itself requires exported="true" for. -->
|
|
| 17 | + <activity-alias
|
|
| 18 | + android:name="${applicationId}.App"
|
|
| 19 | + android:exported="true"
|
|
| 20 | + android:targetActivity="HomeActivity">
|
|
| 21 | + <intent-filter>
|
|
| 22 | + <action android:name="android.intent.action.MAIN" />
|
|
| 23 | + <category android:name="android.intent.category.LAUNCHER" />
|
|
| 24 | + </intent-filter>
|
|
| 25 | + </activity-alias>
|
|
| 26 | + |
|
| 27 | + <!-- Exported and not a MAIN+LAUNCHER entry point, but it doesn't
|
|
| 28 | + matter since exported="false" is always fine to flag as good. -->
|
|
| 29 | + <receiver
|
|
| 30 | + android:name="onboarding.WidgetPinnedReceiver"
|
|
| 31 | + android:exported="false">
|
|
| 32 | + <intent-filter>
|
|
| 33 | + <action android:name="org.mozilla.fenix.onboarding.WidgetPinnedReceiver.widgetPinned" />
|
|
| 34 | + </intent-filter>
|
|
| 35 | + </receiver>
|
|
| 36 | + </application>
|
|
| 37 | +</manifest> |
| ... | ... | @@ -69,6 +69,10 @@ skip-if = ["os == 'win'"] |
| 69 | 69 | ["test_shellcheck.py"]
|
| 70 | 70 | |
| 71 | 71 | ["test_stylelint.py"]
|
| 72 | + |
|
| 73 | +["test_tor_browser_checks.py"]
|
|
| 74 | +tags = "base-browser"
|
|
| 75 | + |
|
| 72 | 76 | skip-if = ["os == 'win'"] # busts the tree for subsequent tasks on the same worker (bug 1708591)
|
| 73 | 77 | # Setup conflicts with eslint setup so this should run sequentially.
|
| 74 | 78 | sequential = true
|
| 1 | +import mozunit
|
|
| 2 | + |
|
| 3 | +LINTER = "tor-browser-checks"
|
|
| 4 | + |
|
| 5 | + |
|
| 6 | +def test_flags_exported_non_launcher_components(lint, paths):
|
|
| 7 | + results = lint(paths("bad/AndroidManifest.xml"))
|
|
| 8 | + assert len(results) == 4
|
|
| 9 | + |
|
| 10 | + flagged_names = {
|
|
| 11 | + "org.mozilla.gecko.BrowserApp",
|
|
| 12 | + "org.mozilla.gecko.PermissiveApp",
|
|
| 13 | + "org.mozilla.gecko.LauncherCategoryOnlyApp",
|
|
| 14 | + "com.example.evil.UnlistedReceiver",
|
|
| 15 | + }
|
|
| 16 | + for expected_name in flagged_names:
|
|
| 17 | + assert any(expected_name in r.message for r in results)
|
|
| 18 | + |
|
| 19 | + for r in results:
|
|
| 20 | + assert r.rule == "unnecessary-exported-component"
|
|
| 21 | + |
|
| 22 | + |
|
| 23 | +def test_allows_launcher_entries_and_unexported_components(lint, paths):
|
|
| 24 | + results = lint(paths("good/AndroidManifest.xml"))
|
|
| 25 | + assert len(results) == 0
|
|
| 26 | + |
|
| 27 | + |
|
| 28 | +if __name__ == "__main__":
|
|
| 29 | + mozunit.main() |
| 1 | +---
|
|
| 2 | +tor-browser-checks:
|
|
| 3 | + description: >-
|
|
| 4 | + Tor Browser specific static checks (one linter, several
|
|
| 5 | + independent checks -- see tools/lint/tor-browser-checks/__init__.py)
|
|
| 6 | + include:
|
|
| 7 | + - 'mobile/android/fenix/app/src/main/AndroidManifest.xml'
|
|
| 8 | + - 'mobile/android/fenix/app/src/debug/AndroidManifest.xml'
|
|
| 9 | + - 'mobile/android/fenix/app/src/beta/AndroidManifest.xml'
|
|
| 10 | + - 'mobile/android/fenix/app/src/release/AndroidManifest.xml'
|
|
| 11 | + - 'mobile/android/fenix/app/src/nightly/AndroidManifest.xml'
|
|
| 12 | + - 'mobile/android/fenix/app/src/benchmark/AndroidManifest.xml'
|
|
| 13 | + support-files:
|
|
| 14 | + - 'tools/lint/tor-browser-checks/**'
|
|
| 15 | + type: external
|
|
| 16 | + payload: tor-browser-checks:lint |
| 1 | +# This Source Code Form is subject to the terms of the Mozilla Public
|
|
| 2 | +# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
| 3 | +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
| 4 | + |
|
| 5 | +import xml.etree.ElementTree as ET
|
|
| 6 | + |
|
| 7 | +from mozlint import result
|
|
| 8 | + |
|
| 9 | +ANDROID_NS = "{http://schemas.android.com/apk/res/android}"
|
|
| 10 | + |
|
| 11 | +# Component types that can carry android:exported.
|
|
| 12 | +EXPORTABLE_TAGS = ("activity", "activity-alias", "service", "receiver", "provider")
|
|
| 13 | + |
|
| 14 | +MAIN_ACTION = "android.intent.action.MAIN"
|
|
| 15 | +LAUNCHER_CATEGORY = "android.intent.category.LAUNCHER"
|
|
| 16 | + |
|
| 17 | + |
|
| 18 | +def _find_lineno(lines, name):
|
|
| 19 | + # ElementTree doesn't track source positions. This is a best-effort fallback
|
|
| 20 | + # instead: find the line where this element's own android:name is written
|
|
| 21 | + # out. Good enough to jump to the right spot, but not a guarantee if `name`
|
|
| 22 | + # shows up as some other attribute's value too.
|
|
| 23 | + needle = f'name="{name}"'
|
|
| 24 | + for lineno, line in enumerate(lines, start=1):
|
|
| 25 | + if needle in line:
|
|
| 26 | + return lineno
|
|
| 27 | + return 0
|
|
| 28 | + |
|
| 29 | + |
|
| 30 | +def _has_launcher_intent_filter(el):
|
|
| 31 | + # A MAIN+LAUNCHER intent-filter is the one case Android itself makes
|
|
| 32 | + # exported="true" load-bearing: that's what lets the home screen (a
|
|
| 33 | + # separate app, from the OS's point of view) resolve and start this
|
|
| 34 | + # component when its icon is tapped. Nothing else needs to be exported
|
|
| 35 | + # for the app to function -- everything else is a deliberate choice to
|
|
| 36 | + # let other apps reach in, and should default to false.
|
|
| 37 | + for intent_filter in el.findall("intent-filter"):
|
|
| 38 | + actions = {a.get(ANDROID_NS + "name") for a in intent_filter.findall("action")}
|
|
| 39 | + categories = {
|
|
| 40 | + c.get(ANDROID_NS + "name") for c in intent_filter.findall("category")
|
|
| 41 | + }
|
|
| 42 | + if MAIN_ACTION in actions and LAUNCHER_CATEGORY in categories:
|
|
| 43 | + return True
|
|
| 44 | + return False
|
|
| 45 | + |
|
| 46 | + |
|
| 47 | +def _check_only_launcher_entries_are_exported(path, config):
|
|
| 48 | + """Flags any exported component in a fenix AndroidManifest.xml that
|
|
| 49 | + isn't a MAIN+LAUNCHER entry point.
|
|
| 50 | + |
|
| 51 | + This enforces the rule directly: android:exported="true" is only ever
|
|
| 52 | + structurally required for a component that's meant to be launched from
|
|
| 53 | + the home screen. Anything else that's exported can be set to exported="false"
|
|
| 54 | + instead, even if that means turning off the feature that needed it.
|
|
| 55 | + |
|
| 56 | + Not autofixable: turning exported="false" off may mean losing the
|
|
| 57 | + feature that needed it (e.g. PWA shortcut relaunching), which needs a
|
|
| 58 | + human to decide whether that tradeoff is acceptable case by case.
|
|
| 59 | + |
|
| 60 | + Only looks at AndroidManifest.xml fragments; anything else is skipped.
|
|
| 61 | + """
|
|
| 62 | + if not path.endswith("AndroidManifest.xml"):
|
|
| 63 | + return []
|
|
| 64 | + |
|
| 65 | + try:
|
|
| 66 | + # Manifest fragments (e.g. src/nightly/AndroidManifest.xml) can be a
|
|
| 67 | + # bare <manifest> with no <application> at all -- nothing to check.
|
|
| 68 | + app = ET.parse(path).getroot().find("application")
|
|
| 69 | + except ET.ParseError:
|
|
| 70 | + return []
|
|
| 71 | + if app is None:
|
|
| 72 | + return []
|
|
| 73 | + |
|
| 74 | + with open(path, encoding="utf-8") as f:
|
|
| 75 | + lines = f.readlines()
|
|
| 76 | + |
|
| 77 | + issues = []
|
|
| 78 | + for tag in EXPORTABLE_TAGS:
|
|
| 79 | + for el in app.findall(tag):
|
|
| 80 | + if el.get(ANDROID_NS + "exported") != "true":
|
|
| 81 | + continue
|
|
| 82 | + if _has_launcher_intent_filter(el):
|
|
| 83 | + continue
|
|
| 84 | + |
|
| 85 | + name = el.get(ANDROID_NS + "name")
|
|
| 86 | + |
|
| 87 | + issues.append(
|
|
| 88 | + result.from_config(
|
|
| 89 | + config,
|
|
| 90 | + path=path,
|
|
| 91 | + lineno=_find_lineno(lines, name),
|
|
| 92 | + message=(
|
|
| 93 | + f'<{tag}> "{name}" is exported="true" but has no '
|
|
| 94 | + "MAIN+LAUNCHER intent-filter, so Android doesn't "
|
|
| 95 | + 'require it to be exported -- set exported="false" '
|
|
| 96 | + "(see tor-browser#45145)."
|
|
| 97 | + ),
|
|
| 98 | + level="error",
|
|
| 99 | + rule="unnecessary-exported-component",
|
|
| 100 | + ),
|
|
| 101 | + )
|
|
| 102 | + |
|
| 103 | + return issues
|
|
| 104 | + |
|
| 105 | + |
|
| 106 | +# Add new Tor Browser security checks here. Each one receives a file path
|
|
| 107 | +# and is responsible for deciding whether that path is relevant to it, so
|
|
| 108 | +# checks are free to target entirely different kinds of files.
|
|
| 109 | +#
|
|
| 110 | +# Note: Remember to extend the linter's `include`/`extensions` in
|
|
| 111 | +# tor-browser-checks.yml if a new check needs paths that aren't already covered.
|
|
| 112 | +CHECKS = [
|
|
| 113 | + _check_only_launcher_entries_are_exported,
|
|
| 114 | +]
|
|
| 115 | + |
|
| 116 | + |
|
| 117 | +def lint(paths, config, **lintargs):
|
|
| 118 | + results = []
|
|
| 119 | + |
|
| 120 | + for path in paths:
|
|
| 121 | + for check in CHECKS:
|
|
| 122 | + results.extend(check(path, config))
|
|
| 123 | + |
|
| 124 | + return results |