CVE-2025-26427 AI-generated analysis
platform/packages/apps/DocumentsUI (picker ActionHandler), platform/frameworks/base (FileSystemProvider / ExternalStorageProvider), platform/packages/providers/DownloadProvider (DownloadStorageProvider)
Extends the Android 11 SAF privacy restriction on /Android/data, /Android/obb and /Android/sandbox to their whole subtrees. ExternalStorageProvider now hides those subtrees through a new FileSystemProvider.shouldHideDocument() hook, and the DocumentsUI picker no longer launches to an EXTRA_INITIAL_URI inside them.
Root cause
Two deny-lists were too narrow. 1. DocumentsUI picker/ActionHandler.launchToInitialUri(): the old PATTERN_BLOCK_PATH (".*:android\\/(?:data|obb|sandbox)$") was matched against the lowercased raw URI path. It only matched when that path ended exactly in Android/data, Android/obb or Android/sandbox. A document URI for any subdirectory of those passed the check, and launchToDocument() loaded the picker directly into it. 2. Provider side: FileSystemProvider.shouldHide() used PATTERN_HIDDEN_PATH ("^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)$"), which again covers only the exact directories. As the fix comment states, ExternalStorageProvider "hides" only Android/data/, Android/obb/ and Android/sandbox/, "but NOT their subdirectories". The DocumentsUI path was not canonicalized before matching. Together, this let a requesting app steer the ACTION_OPEN_DOCUMENT / ACTION_OPEN_DOCUMENT_TREE picker into restricted subtrees that the Android 11 restriction is meant to exclude.
How the fix works
- frameworks/base core/java/com/android/internal/content/FileSystemProvider.java: - removes PATTERN_HIDDEN_PATH, shouldHide(File) and shouldShow(); - adds an overridable shouldHideDocument(String documentId) (default false), plus a final File variant that maps through getDocIdForFile(); - renames shouldBlockFromTree() to shouldBlockDirectoryFromTree().
- FileSystemProvider.queryChildDocuments(): the old variants (public queryChildDocuments, queryChildDocumentsShowAll, and a private predicate variant) are collapsed into one protected queryChildDocuments(documentId, projection, sortOrder, boolean includeHidden). - The public 3-arg overload calls it with includeHidden=false. - queryChildDocumentsForManage() becomes final and calls it with includeHidden=true. - When includeHidden is false, the queried directory itself and each child are skipped if shouldHideDocument() is true.
- FileSystemProvider.querySearchDocuments(): the BFS is rewritten with an ArrayDeque. It skips any file or directory for which shouldHideDocument(file) is true, so hidden subtrees are not descended into. The loop condition is now getCount() < MAX_RESULTS_NUMBER (23).
- FileSystemProvider.includeFile(): FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE is now set on any directory where shouldBlockDirectoryFromTree(docId) is true, whether or not file.canWrite() is true. Previously it was set only inside the canWrite branch.
- frameworks/base packages/ExternalStorageProvider ExternalStorageProvider.java: - implements shouldHideDocument(documentId): it returns false on removable USB roots (new isOnRemovableUsbStorage()); otherwise it matches the canonicalized getPathFromDocId() result against PATTERN_RESTRICTED_ANDROID_SUBTREES "^Android/(?:data|obb|sandbox)(?:/.+)?" (CASE_INSENSITIVE). - rewrites shouldBlockDirectoryFromTree(): it blocks the storage root, Download, Android, and anything shouldHideDocument() hides, and no longer wraps IOException into IllegalArgumentException. - getPathFromDocId() no longer throws; on IOException it logs and returns "". - removes the old queryChildDocumentsForManage() override. - resolveDocId/buildFile drop the 'visible' parameter.
- DownloadProvider DownloadStorageProvider.java: drops its own queryChildDocuments()/queryChildDocumentsForManage() overrides and instead overrides the protected queryChildDocuments(documentId, projection, sortOrder, includeHidden). It passes includeHidden through to super and to includeDownloadsFromMediaStore(), where the old 'manage' flag was used.
- DocumentsUI src/com/android/documentsui/picker/ActionHandler.java launchToInitialUri(): removes PATTERN_BLOCK_PATH and the loadDeviceRoot() redirect. It now classifies the URI: - root URI: loadRoot(); - neither root nor document: return false; - document URI: calls the new shouldPreemptivelyRestrictRequestedInitialUri(uri). If that returns true, it logs a warning and returns false; otherwise it calls launchToDocument(uri). The commit message says a restricted request is redirected to the default location (last accessed stack).
- ActionHandler.shouldPreemptivelyRestrictRequestedInitialUri(): - returns false if !Shared.shouldRestrictStorageAccessFramework(mActivity), or if the authority is not Providers.AUTHORITY_STORAGE; - otherwise canonicalizes the docId path via the new FileUtils.getPathFromStorageDocId() and returns true if it matches PATTERN_RESTRICTED_INITIAL_PATH "^/Android/(?:data|obb|sandbox).*" (CASE_INSENSITIVE); - an IOException during canonicalization is treated as restricted; - a hard-coded externalProviderImplementsSafRestrictions=false keeps this client-side check active.
- DocumentsUI src/com/android/documentsui/util/FileUtils.java (new file): getCanonicalPath(String) and getPathFromStorageDocId(String docId). The latter strips the "root:" prefix and canonicalizes with File.getCanonicalPath(), without trimming slashes.
- DocumentsUI AbstractActionHandler.launchToDocument()/onStackToLaunchToLoaded(): refactor only (early return on archive URI, callback renamed from onStackLoaded, debug logging). The stack-reset and metrics behaviour is unchanged.
Completeness
- Provider side: ExternalStorageProvider.shouldHideDocument() is consulted in FileSystemProvider.queryChildDocuments() (non-manage path) and querySearchDocuments(), and it feeds shouldBlockDirectoryFromTree(). It covers the full Android/data, Android/obb and Android/sandbox subtrees on non-USB roots. The material does not show shouldHideDocument() being applied in queryDocument(), openDocument() or findDocumentPath().
- Client side: the DocumentsUI guard exists only in picker ActionHandler.launchToInitialUri(). It applies only to document URIs with authority Providers.AUTHORITY_STORAGE, and only when Shared.shouldRestrictStorageAccessFramework(mActivity) is true. Root URIs go straight to loadRoot() without the check, and other authorities are not inspected.
- The DocumentsUI pattern "^/Android/(?:data|obb|sandbox).*" has no path separator after the directory name, so it also matches siblings that share the prefix (for example /Android/database). This over-restricts rather than under-restricts. The ExternalStorageProvider pattern requires a separator.
- FileSystemProvider's default shouldHideDocument() returns false, so the old absolute-path hiding (PATTERN_HIDDEN_PATH) no longer applies to other FileSystemProvider subclasses unless they override the hook. In these diffs only ExternalStorageProvider overrides it.
- The DocumentsUI check is explicitly kept active (externalProviderImplementsSafRestrictions=false) even when the provider fix is present.
Behavior changes
- When the picker is launched with an EXTRA_INITIAL_URI document inside Android/data, Android/obb or Android/sandbox (or any subdirectory) on com.android.externalstorage.documents, launchToInitialUri() no longer opens it and logs a warning. Per the commit message the picker goes to the default (last accessed) location; previously the exact directories were redirected to the device root.
- ExternalStorageProvider child listings and search results omit Android/data, Android/obb, Android/sandbox and everything beneath them on non-USB roots, except when queried via queryChildDocumentsForManage(). Hidden directories are also flagged FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE.
- FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE is now set on blocked directories even when they are not writable.
- Subclass API changes: FileSystemProvider.queryChildDocumentsForManage() is final; queryChildDocumentsShowAll() and shouldHide(File) are removed; shouldBlockFromTree() is renamed to shouldBlockDirectoryFromTree().
- ExternalStorageProvider.getPathFromDocId() returns "" instead of throwing when canonicalization fails. shouldBlockDirectoryFromTree() propagates FileNotFoundException instead of wrapping it in IllegalArgumentException.
- querySearchDocuments() now returns at most 23 results (MAX_RESULTS_NUMBER); the previous '< 24' loop condition allowed up to 24.
Per major
- Android 14: same fix: frameworks/base 4af5db76f253 and DownloadProvider 5acd646e0cf6 landed in android-security-14.0.0_r5, and DocumentsUI 954b6d5397ee in android-security-14.0.0_r19. The code context at android-security-14.0.0_r29 shows the patched launchToDocument()/onStackToLaunchToLoaded(), the new imports in picker/ActionHandler.java, util/FileUtils.java, and MAX_RESULTS_NUMBER in FileSystemProvider. The 3/8 line-match metric understates this.
- Android 15: not affected (not listed by the bulletin). android-security-15.0.0_r17 shows the same 3/8 region match as the fixed 14 branch.
- Android 16: not affected (not listed by the bulletin). android-security-16.0.0_r8 shows the same 3/8 region match.
- Android 17: not affected (not listed by the bulletin). android-security-17.0.0_r1 shows the same 3/8 region match.
Unknowns
- The criteria used by Shared.shouldRestrictStorageAccessFramework() are not in the material.
- Whether ExternalStorageProvider.queryDocument()/openDocument()/findDocumentPath() consult shouldHideDocument() is not shown.
- Whether DownloadStorageProvider or other FileSystemProvider subclasses override shouldHideDocument() is not shown.
- The rest of ActionHandler.initLocation() after launchToInitialUri() returns false is not shown.
- Which of the three commits the bulletin considers the primary fix, and the fixed tags for Android 13 and 15-next, are not shown.
- Whether the picker UI has additional guards against navigating into restricted directories is not shown.