Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 2 additions & 17 deletions AnkiDroid/src/main/java/com/ichi2/utils/FileUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ object FileUtil {
// If we got a real file name, do a copy from it
val inputStream: InputStream =
try {
contentResolver.openInputStream(uri)!!
contentResolver.openInputStreamSafe(uri)!!
} catch (e: Exception) {
Timber.w(e, "internalizeUri() unable to open input stream from content resolver for Uri %s", uri)
throw e
Expand All @@ -125,21 +125,6 @@ object FileUtil {
fun listFiles(dir: File): Array<File> =
dir.listFiles()
?: throw IOException("Failed to list the contents of '$dir'")

/**
* Returns a sequence containing the provided file, and its parents
* up to the root of the filesystem.
*/
fun File.getParentsAndSelfRecursive() =
sequence {
var currentPath: File? = [email protected]
while (currentPath != null) {
yield(currentPath)
currentPath = currentPath.parentFile?.canonicalFile
}
}

fun File.isDescendantOf(ancestor: File) = this.getParentsAndSelfRecursive().drop(1).contains(ancestor)
}

/**
Expand Down Expand Up @@ -219,5 +204,5 @@ fun ContentResolver.openInputStreamSafe(uri: Uri): InputStream? {
if (normalized.startsWith("/data")) {
throw SecurityException("java/android/unsafe-content-uri-resolution")
}
return openInputStream(uri)
return openInputStreamSafe(uri)
}
2 changes: 2 additions & 0 deletions lint-rules/src/main/java/com/ichi2/anki/lint/IssueRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.ichi2.anki.lint.rules.InvalidStringFormatDetector
import com.ichi2.anki.lint.rules.JUnitNullAssertionDetector
import com.ichi2.anki.lint.rules.LocaleRootDetector
import com.ichi2.anki.lint.rules.NonPositionalFormatSubstitutions
import com.ichi2.anki.lint.rules.OpenInputStreamSafeDetector
import com.ichi2.anki.lint.rules.PrintStackTraceUsage
import com.ichi2.anki.lint.rules.SentenceCaseConventions
import com.ichi2.anki.lint.rules.TranslationTypo
Expand All @@ -62,6 +63,7 @@ class IssueRegistry : IssueRegistry() {
LocaleRootDetector.ISSUE,
PrintStackTraceUsage.ISSUE,
NonPositionalFormatSubstitutions.ISSUE,
OpenInputStreamSafeDetector.ISSUE,
SentenceCaseConventions.ISSUE,
TranslationTypo.ISSUE,
FixedPreferencesTitleLength.PREFERENCES_ISSUE_MAX_LENGTH,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.ichi2.anki.lint.rules

import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression

/**
* Detector that ensures ContentResolver.openInputStream() is not called directly.
* Instead, developers should use the openInputStreamSafe() extension function
* which includes path traversal protection.
*/

class OpenInputStreamSafeDetector :
Detector(),
SourceCodeScanner {
companion object {
private const val EXPLANATION = """
Use openInputStreamSafe() instead of openInputStream() to prevent \
path traversal vulnerabilities. The safe version normalizes paths and blocks \
access to sensitive directories like /data.
"""

val ISSUE =
Issue.create(
id = "UnsafeOpenInputStream",
briefDescription = "Use openInputStreamSafe() instead of openInputStream()",
explanation = EXPLANATION,
category = Category.SECURITY,
priority = 9,
severity = Severity.ERROR,
implementation =
Implementation(
OpenInputStreamSafeDetector::class.java,
Scope.JAVA_FILE_SCOPE,
),
)
}

override fun getApplicableMethodNames(): List<String> = listOf("openInputStream")

override fun visitMethodCall(
context: JavaContext,
node: UCallExpression,
method: PsiMethod,
) {
// Only warn on ContentResolver.openInputStream()
if (!context.evaluator.isMemberInClass(method, "android.content.ContentResolver")) return
context.report(
issue = ISSUE,
location = context.getNameLocation(node),
message = "Use openInputStreamSafe() instead of openInputStream()",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.ichi2.anki.lint.rules

import com.android.tools.lint.checks.infrastructure.TestFiles.java
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class OpenInputStreamSafeDetectorTest {
// Stub files for Android classes needed by the tests
private val contentResolverStub =
java(
"""
package android.content;

import android.net.Uri;
import java.io.InputStream;

public abstract class ContentResolver {
public final InputStream openInputStream(Uri uri) {
return null;
}
}
""",
).indented()

private val uriStub =
java(
"""
package android.net;

public abstract class Uri {
}
""",
).indented()

@Test
fun testDirectOpenInputStreamCall() {
lint()
.allowMissingSdk()
.files(
contentResolverStub,
uriStub,
Comment on lines +44 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These stubs provide fake Android classes for the test environment. Without them, the lint detector wouldn't know what ContentResolver or Uri are, and couldn't check if openInputStream() belongs to the android.content.ContentResolver class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes

Index: lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt b/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt
--- a/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt	(revision a7e7533b8af81829e5d940559c77442272219534)
+++ b/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt	(date 1764850400435)
@@ -9,40 +9,12 @@
 
 @RunWith(JUnit4::class)
 class OpenInputStreamSafeDetectorTest {
-    // Stub files for Android classes needed by the tests
-    private val contentResolverStub =
-        java(
-            """
-        package android.content;
-        
-        import android.net.Uri;
-        import java.io.InputStream;
-        
-        public abstract class ContentResolver {
-            public final InputStream openInputStream(Uri uri) {
-                return null;
-            }
-        }
-        """,
-        ).indented()
-
-    private val uriStub =
-        java(
-            """
-        package android.net;
-        
-        public abstract class Uri {
-        }
-        """,
-        ).indented()
 
     @Test
     fun testDirectOpenInputStreamCall() {
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 kotlin(
                     """
                     class MyClass {
@@ -62,8 +34,6 @@
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 kotlin(
                     """
                     class MyClass {
@@ -83,8 +53,6 @@
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 java(
                     """
                     public class MyClass {
@@ -104,8 +72,6 @@
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 kotlin(
                     """
                     class MyClass {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still pending

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests fail without those lines

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works fine here, how are you testing?

Screenshot 2025-12-09 at 13 47 06
Index: lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt b/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt
--- a/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt	(revision 5f313280ebc06bb23f1bc3419d6bd0577458147a)
+++ b/lint-rules/src/test/java/com/ichi2/anki/lint/rules/OpenInputStreamSafeDetectorTest.kt	(date 1765262787110)
@@ -9,40 +9,12 @@
 
 @RunWith(JUnit4::class)
 class OpenInputStreamSafeDetectorTest {
-    // Stub files for Android classes needed by the tests
-    private val contentResolverStub =
-        java(
-            """
-        package android.content;
-        
-        import android.net.Uri;
-        import java.io.InputStream;
-        
-        public abstract class ContentResolver {
-            public final InputStream openInputStream(Uri uri) {
-                return null;
-            }
-        }
-        """,
-        ).indented()
-
-    private val uriStub =
-        java(
-            """
-        package android.net;
-        
-        public abstract class Uri {
-        }
-        """,
-        ).indented()
 
     @Test
     fun testDirectOpenInputStreamCall() {
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 kotlin(
                     """
                     class MyClass {
@@ -62,8 +34,6 @@
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 kotlin(
                     """
                     class MyClass {
@@ -83,8 +53,6 @@
         lint()
             .allowMissingSdk()
             .files(
-                contentResolverStub,
-                uriStub,
                 java(
                     """
                     public class MyClass {

kotlin(
"""
class MyClass {
fun loadData(resolver: android.content.ContentResolver, uri: android.net.Uri) {
val stream = resolver.openInputStream(uri)
}
}
""",
).indented(),
).issues(OpenInputStreamSafeDetector.ISSUE)
.run()
.expectContains("Use openInputStreamSafe() instead of openInputStream()")
}

@Test
fun testOpenInputStreamSafeCall() {
lint()
.allowMissingSdk()
.files(
contentResolverStub,
uriStub,
kotlin(
"""
class MyClass {
fun loadData(resolver: android.content.ContentResolver, uri: android.net.Uri) {
val stream = resolver.openInputStreamSafe(uri)
}
}
""",
).indented(),
).issues(OpenInputStreamSafeDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun testJavaDirectOpenInputStreamCall() {
lint()
.allowMissingSdk()
.files(
contentResolverStub,
uriStub,
java(
"""
public class MyClass {
public void loadData(android.content.ContentResolver resolver, android.net.Uri uri) {
java.io.InputStream stream = resolver.openInputStream(uri);
}
}
""",
).indented(),
).issues(OpenInputStreamSafeDetector.ISSUE)
.run()
.expectContains("Use openInputStreamSafe() instead of openInputStream()")
}
}