java.lang.Object
↳androidx.core.content.IntentSanitizer
Gradle dependencies
compile group: 'androidx.core', name: 'core', version: '1.15.0-alpha02'
- groupId: androidx.core
- artifactId: core
- version: 1.15.0-alpha02
Artifact androidx.core:core:1.15.0-alpha02 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.core:core com.android.support:support-compat
Overview
This class is used to make a sanitized copy of an . This could be used when
is detected.
This class is thread safe and the object created is safe to be reused.
Typical usage of the class:
Intent intent = new IntentSanitizer.Builder()
.allowComponent(“com.example.ActivityA”)
.allowData(“com.example”)
.allowType(“text/plain”)
.build()
.sanitizeByThrowing(intent);
At least one of the allowPackage, allowComponent must be called unless implicit intent is
allowed. In which case, allowAnyComponent must be called and caution has to be taken to
protect your private data.
Summary
Methods |
---|
public Intent | sanitize(Intent in, Consumer<java.lang.String> penalty)
This method sanitizes the given intent. |
public Intent | sanitizeByFiltering(Intent in)
Convenient method for filtering unwanted members from the input intent and log it. |
public Intent | sanitizeByThrowing(Intent in)
Convenient method for throwing a SecurityException when unwanted members of the input
intent is encountered. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Methods
public Intent
sanitizeByFiltering(Intent in)
Convenient method for filtering unwanted members from the input intent and log it.
Parameters:
in: input intent
Returns:
a copy of the input intent after filtering out unwanted members.
public Intent
sanitizeByThrowing(Intent in)
Convenient method for throwing a SecurityException when unwanted members of the input
intent is encountered.
Parameters:
in: input intent
Returns:
a copy of the input intent if the input intent does not contain any unwanted members.
public Intent
sanitize(Intent in,
Consumer<java.lang.String> penalty)
This method sanitizes the given intent. If dirty members are found, the errors are consumed
by the penalty object. The penalty action could be called multiple times if multiple
issues exist.
Parameters:
in: the given intent.
penalty: consumer of the error message if dirty members are found.
Returns:
a sanitized copy of the given intent.
Source
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.content;
import static androidx.core.util.Preconditions.checkNotNull;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.strictmode.UnsafeIntentLaunchViolation;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;
import androidx.core.util.Predicate;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* This class is used to make a sanitized copy of an {@link Intent}. This could be used when
* {@link UnsafeIntentLaunchViolation} is detected.
* This class is thread safe and the object created is safe to be reused.
* Typical usage of the class:
* <pre>
* {@code
* Intent intent = new IntentSanitizer.Builder()
* .allowComponent(“com.example.ActivityA”)
* .allowData(“com.example”)
* .allowType(“text/plain”)
* .build()
* .sanitizeByThrowing(intent);
* }
* </pre>
*
* At least one of the allowPackage, allowComponent must be called unless implicit intent is
* allowed. In which case, allowAnyComponent must be called and caution has to be taken to
* protect your private data.
*/
public class IntentSanitizer {
private static final String TAG = "IntentSanitizer";
private int mAllowedFlags;
private Predicate<String> mAllowedActions;
private Predicate<Uri> mAllowedData;
private Predicate<String> mAllowedTypes;
private Predicate<String> mAllowedCategories;
private Predicate<String> mAllowedPackages;
private Predicate<ComponentName> mAllowedComponents;
private boolean mAllowAnyComponent;
private Map<String, Predicate<Object>> mAllowedExtras;
private boolean mAllowClipDataText;
private Predicate<Uri> mAllowedClipDataUri;
private Predicate<ClipData> mAllowedClipData;
private boolean mAllowIdentifier;
private boolean mAllowSelector;
private boolean mAllowSourceBounds;
private IntentSanitizer() {
}
/**
* Convenient method for filtering unwanted members from the input intent and log it.
*
* @param in input intent
* @return a copy of the input intent after filtering out unwanted members.
*/
@NonNull
public Intent sanitizeByFiltering(@NonNull Intent in) {
return sanitize(in, msg -> {});
}
/**
* Convenient method for throwing a SecurityException when unwanted members of the input
* intent is encountered.
*
* @param in input intent
* @return a copy of the input intent if the input intent does not contain any unwanted members.
* @throws SecurityException if the input intent contains any unwanted members.
*/
@NonNull
public Intent sanitizeByThrowing(@NonNull Intent in) {
return sanitize(in, msg -> {
throw new SecurityException(msg);
});
}
/**
* This method sanitizes the given intent. If dirty members are found, the errors are consumed
* by the penalty object. The penalty action could be called multiple times if multiple
* issues exist.
*
* @param in the given intent.
* @param penalty consumer of the error message if dirty members are found.
* @return a sanitized copy of the given intent.
*/
@NonNull
public Intent sanitize(@NonNull Intent in,
@NonNull Consumer<String> penalty) {
Intent intent = new Intent();
ComponentName componentName = in.getComponent();
if ((mAllowAnyComponent && componentName == null)
|| mAllowedComponents.test(componentName)) {
intent.setComponent(componentName);
} else {
penalty.accept("Component is not allowed: " + componentName);
intent.setComponent(new ComponentName("android", "java.lang.Void"));
}
String packageName = in.getPackage();
if (packageName == null || mAllowedPackages.test(packageName)) {
intent.setPackage(packageName);
} else {
penalty.accept(("Package is not allowed: " + packageName));
}
if ((mAllowedFlags | in.getFlags()) == mAllowedFlags) {
intent.setFlags(in.getFlags());
} else {
intent.setFlags(mAllowedFlags & in.getFlags());
penalty.accept("The intent contains flags that are not allowed: "
+ "0x" + Integer.toHexString(in.getFlags() & ~mAllowedFlags));
}
String action = in.getAction();
if (action == null || mAllowedActions.test(action)) {
intent.setAction(action);
} else {
penalty.accept("Action is not allowed: " + action);
}
Uri data = in.getData();
if (data == null || mAllowedData.test(data)) {
intent.setData(data);
} else {
penalty.accept("Data is not allowed: " + data);
}
String type = in.getType();
if (type == null || mAllowedTypes.test(type)) {
intent.setDataAndType(intent.getData(), type);
} else {
penalty.accept("Type is not allowed: " + type);
}
Set<String> categories = in.getCategories();
if (categories != null) {
for (String category : categories) {
if (mAllowedCategories.test(category)) {
intent.addCategory(category);
} else {
penalty.accept("Category is not allowed: " + category);
}
}
}
Bundle extras = in.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
if (key.equals(Intent.EXTRA_STREAM)
&& (mAllowedFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) == 0) {
penalty.accept(
"Allowing Extra Stream requires also allowing at least "
+ " FLAG_GRANT_READ_URI_PERMISSION Flag.");
continue;
}
if (key.equals(MediaStore.EXTRA_OUTPUT)
&& (~mAllowedFlags
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) != 0) {
penalty.accept("Allowing Extra Output requires also allowing "
+ "FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION"
+ " Flags.");
continue;
}
Object value = extras.get(key);
Predicate<Object> test = mAllowedExtras.get(key);
if (test != null && test.test(value)) {
putExtra(intent, key, value);
} else {
penalty.accept("Extra is not allowed. Key: " + key + ". Value: " + value);
}
}
}
sanitizeClipData(in, intent, mAllowedClipData, mAllowClipDataText, mAllowedClipDataUri,
penalty);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (mAllowIdentifier) {
Api29Impl.setIdentifier(intent, Api29Impl.getIdentifier(in));
} else if (Api29Impl.getIdentifier(in) != null) {
penalty.accept("Identifier is not allowed: " + Api29Impl.getIdentifier(in));
}
}
if (mAllowSelector) {
intent.setSelector(in.getSelector());
} else if (in.getSelector() != null) {
penalty.accept("Selector is not allowed: " + in.getSelector());
}
if (mAllowSourceBounds) {
intent.setSourceBounds(in.getSourceBounds());
} else if (in.getSourceBounds() != null) {
penalty.accept("SourceBounds is not allowed: " + in.getSourceBounds());
}
return intent;
}
private void putExtra(Intent intent, String key, Object value) {
if (value == null) {
intent.getExtras().putString(key, null);
} else if (value instanceof Parcelable) {
intent.putExtra(key, (Parcelable) value);
} else if (value instanceof Parcelable[]) {
intent.putExtra(key, (Parcelable[]) value);
} else if (value instanceof Serializable) {
intent.putExtra(key, (Serializable) value);
} else {
throw new IllegalArgumentException("Unsupported type " + value.getClass());
}
}
/**
* General strategy of building is to only offer additive “or” operations that are chained
* together. Any more complex operations can be performed by the developer providing their
* own custom Predicate.
*/
public static final class Builder {
private static final int HISTORY_STACK_FLAGS =
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
| Intent.FLAG_ACTIVITY_MULTIPLE_TASK
| Intent.FLAG_ACTIVITY_NEW_DOCUMENT
| Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION
| Intent.FLAG_ACTIVITY_NO_HISTORY
| Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_TASK_ON_HOME;
private static final int RECEIVER_FLAGS =
Intent.FLAG_RECEIVER_FOREGROUND
| Intent.FLAG_RECEIVER_NO_ABORT
| Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_REPLACE_PENDING
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS;
private int mAllowedFlags;
private Predicate<String> mAllowedActions = v -> false;
private Predicate<Uri> mAllowedData = v -> false;
private Predicate<String> mAllowedTypes = v -> false;
private Predicate<String> mAllowedCategories = v -> false;
private Predicate<String> mAllowedPackages = v -> false;
private Predicate<ComponentName> mAllowedComponents = v -> false;
private boolean mAllowAnyComponent;
private boolean mAllowSomeComponents;
private Map<String, Predicate<Object>> mAllowedExtras = new HashMap<>();
private boolean mAllowClipDataText = false;
private Predicate<Uri> mAllowedClipDataUri = v -> false;
private Predicate<ClipData> mAllowedClipData = v -> false;
private boolean mAllowIdentifier;
private boolean mAllowSelector;
private boolean mAllowSourceBounds;
/**
* Sets allowed flags.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
* In most cases following grant URI permission related flags should
* <b>not</b> be allowed:
* <ul>
* <li>FLAG_GRANT_PERSISTABLE_URI_PERMISSION</li>
* <li>FLAG_GRANT_PREFIX_URI_PERMISSION</li>
* <li>FLAG_GRANT_READ_URI_PERMISSION</li>
* <li>FLAG_GRANT_WRITE_URI_PERMISSION</li>
* </ul>
* Setting these flags would allow others to access URIs only your
* app has permission to access. These URIs could be set in intent's data, clipData
* and/or, in certain circumstances, extras with key of {@link Intent#EXTRA_STREAM} or
* {@link MediaStore#EXTRA_OUTPUT}.
* When these flags are allowed, you should sanitize URIs. See
* {@link #allowDataWithAuthority(String)},
* {@link #allowData(Predicate)}, {@link #allowClipDataUriWithAuthority(String)},
* {@link #allowClipDataUri(Predicate)}, {@link #allowExtraStreamUriWithAuthority(String)},
* {@link #allowExtraStream(Predicate)}, {@link #allowExtraOutput(String)},
* {@link #allowExtraOutput(Predicate)}
*
* @param flags allowed flags.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowFlags(int flags) {
mAllowedFlags |= flags;
return this;
}
/**
* Adds all history stack flags into the allowed flags set. They are:
* <ul>
* <li>FLAG_ACTIVITY_BROUGHT_TO_FRONT
* <li>FLAG_ACTIVITY_CLEAR_TASK
* <li>FLAG_ACTIVITY_CLEAR_TOP
* <li>FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
* <li>FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
* <li>FLAG_ACTIVITY_LAUNCH_ADJACENT
* <li>FLAG_ACTIVITY_MULTIPLE_TASK
* <li>FLAG_ACTIVITY_NEW_DOCUMENT
* <li>FLAG_ACTIVITY_NEW_TASK
* <li>FLAG_ACTIVITY_NO_ANIMATION
* <li>FLAG_ACTIVITY_NO_HISTORY
* <li>FLAG_ACTIVITY_PREVIOUS_IS_TOP
* <li>FLAG_ACTIVITY_REORDER_TO_FRONT
* <li>FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
* <li>FLAG_ACTIVITY_RETAIN_IN_RECENTS
* <li>FLAG_ACTIVITY_SINGLE_TOP
* <li>FLAG_ACTIVITY_TASK_ON_HOME
* </ul>
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowHistoryStackFlags() {
mAllowedFlags |= HISTORY_STACK_FLAGS;
return this;
}
/**
* Adds all receiver flags into the allowed flags set. They are
* <ul>
* <li>FLAG_RECEIVER_FOREGROUND
* <li>FLAG_RECEIVER_NO_ABORT
* <li>FLAG_RECEIVER_REGISTERED_ONLY
* <li>FLAG_RECEIVER_REPLACE_PENDING
* <li>FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
* </ul>
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowReceiverFlags() {
mAllowedFlags |= RECEIVER_FLAGS;
return this;
}
/**
* Add an action to the list of allowed actions.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param action the name of an action.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowAction(@NonNull String action) {
checkNotNull(action);
allowAction(action::equals);
return this;
}
/**
* Add a filter for allowed actions.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter a filter that tests if an action is allowed.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowAction(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedActions = mAllowedActions.or(filter);
return this;
}
/**
* Convenient method to allow all data whose URI authority equals to the given.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param authority the URI's authority.
* @return this builder
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowDataWithAuthority(@NonNull String authority) {
checkNotNull(authority);
allowData(v -> authority.equals(v.getAuthority()));
return this;
}
/**
* Allow data that passes the filter test.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter data filter.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowData(@NonNull Predicate<Uri> filter) {
checkNotNull(filter);
mAllowedData = mAllowedData.or(filter);
return this;
}
/**
* Add a data type to the allowed type list. Exact match is used to check the allowed
* types. For example, if you pass in "image/*" here, it won't allow an intent with type of
* "image/png".
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param type the data type that is allowed
* @return this builder
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowType(@NonNull String type) {
checkNotNull(type);
return allowType(type::equals);
}
/**
* Add a filter for allowed data types.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the data type filter.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowType(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedTypes = mAllowedTypes.or(filter);
return this;
}
/**
* Add a category to the allowed category list.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param category the allowed category.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowCategory(@NonNull String category) {
checkNotNull(category);
return allowCategory(category::equals);
}
/**
* Add a filter for allowed categories.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the category filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowCategory(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedCategories = mAllowedCategories.or(filter);
return this;
}
/**
* Add a package to the allowed packages. This does not imply the intent can contain
* a component from the allowed package; instead, this value will be compared against
* the result returned from the Intent's {@link Intent#getPackage()} method.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowPackage(@NonNull String packageName) {
checkNotNull(packageName);
return allowPackage(packageName::equals);
}
/**
* Add a filter for allowed packages. This does not imply the intent can contain
* a component from the allowed package; instead, this value will be compared against
* the result returned from the Intent's {@link Intent#getPackage()} method.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the package name filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowPackage(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedPackages = mAllowedPackages.or(filter);
return this;
}
/**
* Add a component to the allowed components list.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param component the allowed component.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowComponent(@NonNull ComponentName component) {
checkNotNull(component);
return allowComponent(component::equals);
}
/**
* Add a filter for allowed components.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the component filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowComponent(@NonNull Predicate<ComponentName> filter) {
checkNotNull(filter);
mAllowSomeComponents = true;
mAllowedComponents = mAllowedComponents.or(filter);
return this;
}
/**
* Allow any component under the specified package. Note this does not allow the
* package itself. If the intent contains a package, call allowPackage method.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowComponentWithPackage(@NonNull String packageName) {
checkNotNull(packageName);
return allowComponent(v -> packageName.equals(v.getPackageName()));
}
/**
* Allow any components. Be cautious to call this method. When this method is called, you
* should definitely disallow the 4 grant URI permission flags.
* This method is useful in case the redirected intent is designed to support implicit
* intent. This method is made mutually exclusive to the 4 methods that allow components
* or packages.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowAnyComponent() {
mAllowAnyComponent = true;
mAllowedComponents = v -> true;
return this;
}
/**
* Allows clipData that contains text.
* overwrite each other.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowClipDataText() {
mAllowClipDataText = true;
return this;
}
/**
* Allows clipData whose items URIs authorities match the given authority.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param authority the given authority.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowClipDataUriWithAuthority(@NonNull String authority) {
checkNotNull(authority);
return allowClipDataUri(v -> authority.equals(v.getAuthority()));
}
/**
* Allows clipData whose items URIs pass the given URI filter.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the given URI filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowClipDataUri(@NonNull Predicate<Uri> filter) {
checkNotNull(filter);
mAllowedClipDataUri = mAllowedClipDataUri.or(filter);
return this;
}
/**
* Allows clipData that passes the given filter.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the given clipData filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowClipData(@NonNull Predicate<ClipData> filter) {
checkNotNull(filter);
mAllowedClipData = mAllowedClipData.or(filter);
return this;
}
/**
* Allows an extra member whose key and type of value matches the given.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param key the given extra key.
* @param clazz the given class of the extra value.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtra(@NonNull String key, @NonNull Class<?> clazz) {
return allowExtra(key, clazz, (v) -> true);
}
/**
* Allows an extra member whose key matches the given key and whose value is of the type of
* the given clazz and passes the value filter.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param key given extra key.
* @param clazz given type of the extra value.
* @param valueFilter the extra value filter.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public <T> Builder allowExtra(@NonNull String key, @NonNull Class<T> clazz,
@NonNull Predicate<T> valueFilter) {
checkNotNull(key);
checkNotNull(clazz);
checkNotNull(valueFilter);
return allowExtra(key, v -> clazz.isInstance(v) && valueFilter.test(clazz.cast(v)));
}
/**
* Allows an extra member whose key matches the given key and whose value passes the
* filter test.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param key the extra key.
* @param filter the filter for the extra value.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtra(@NonNull String key, @NonNull Predicate<Object> filter) {
checkNotNull(key);
checkNotNull(filter);
Predicate<Object> allowedExtra = mAllowedExtras.get(key);
if (allowedExtra == null) allowedExtra = v -> false;
allowedExtra = allowedExtra.or(filter);
mAllowedExtras.put(key, allowedExtra);
return this;
}
/**
* Allows an extra member with the key Intent.EXTRA_STREAM. The value type has to be URI
* and the authority matches the given parameter.
* In order to use this method, user has to be explicitly allow the
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} flag. Otherwise, it will trigger penalty
* during sanitization.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param uriAuthority the given URI authority.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtraStreamUriWithAuthority(@NonNull String uriAuthority) {
checkNotNull(uriAuthority);
allowExtra(Intent.EXTRA_STREAM, Uri.class,
(v) -> uriAuthority.equals(v.getAuthority()));
return this;
}
/**
* Allows an extra member with the key Intent.EXTRA_STREAM. The value type has to be URI
* and the value also passes the given filter test.
* In order to use this method, user has to be explicitly allow the
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} flag. Otherwise, it will trigger penalty
* during sanitization.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the given URI authority.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtraStream(@NonNull Predicate<Uri> filter) {
allowExtra(Intent.EXTRA_STREAM, Uri.class, filter);
return this;
}
/**
* Allows an extra member with the key MediaStore.EXTRA_OUTPUT. The value type has to be URI
* and the authority matches the given parameter.
* In order to use this method, user has to be explicitly allow the
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} flags. Otherwise, it will trigger penalty
* during sanitization.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param uriAuthority the given URI authority.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtraOutput(@NonNull String uriAuthority) {
allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class,
(v) -> uriAuthority.equals(v.getAuthority()));
return this;
}
/**
* Allows an extra member with the key MediaStore.EXTRA_OUTPUT. The value type has to be URI
* and the value also passes the given filter test.
* In order to use this method, user has to be explicitly allow the
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} flags. Otherwise, it will trigger penalty
* during sanitization.
* This method can be called multiple times and the result is additive. They will not
* overwrite each other.
*
* @param filter the given URI authority.
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowExtraOutput(@NonNull Predicate<Uri> filter) {
allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class, filter);
return this;
}
/**
* Allows any identifier.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowIdentifier() {
mAllowIdentifier = true;
return this;
}
/**
* Allow any selector.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowSelector() {
mAllowSelector = true;
return this;
}
/**
* Allow any source bounds.
*
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
@NonNull
public Builder allowSourceBounds() {
mAllowSourceBounds = true;
return this;
}
/**
* Build the IntentSanitizer.
*
* @return the IntentSanitizer
*/
@NonNull
public IntentSanitizer build() {
if ((mAllowAnyComponent && mAllowSomeComponents)
|| (!mAllowAnyComponent && !mAllowSomeComponents)) {
throw new SecurityException(
"You must call either allowAnyComponent or one or more "
+ "of the allowComponent methods; but not both.");
}
IntentSanitizer sanitizer = new IntentSanitizer();
sanitizer.mAllowedFlags = this.mAllowedFlags;
sanitizer.mAllowedActions = this.mAllowedActions;
sanitizer.mAllowedData = this.mAllowedData;
sanitizer.mAllowedTypes = this.mAllowedTypes;
sanitizer.mAllowedCategories = this.mAllowedCategories;
sanitizer.mAllowedPackages = this.mAllowedPackages;
sanitizer.mAllowAnyComponent = this.mAllowAnyComponent;
sanitizer.mAllowedComponents = this.mAllowedComponents;
sanitizer.mAllowedExtras = this.mAllowedExtras;
sanitizer.mAllowClipDataText = this.mAllowClipDataText;
sanitizer.mAllowedClipDataUri = this.mAllowedClipDataUri;
sanitizer.mAllowedClipData = this.mAllowedClipData;
sanitizer.mAllowIdentifier = this.mAllowIdentifier;
sanitizer.mAllowSelector = this.mAllowSelector;
sanitizer.mAllowSourceBounds = this.mAllowSourceBounds;
return sanitizer;
}
}
static void sanitizeClipData(@NonNull Intent in, Intent out,
Predicate<ClipData> mAllowedClipData,
boolean mAllowClipDataText,
Predicate<Uri> mAllowedClipDataUri, Consumer<String> penalty) {
ClipData clipData = in.getClipData();
if (clipData == null) return;
ClipData newClipData = null;
if (mAllowedClipData != null && mAllowedClipData.test(clipData)) {
out.setClipData(clipData);
} else {
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Api31Impl.checkOtherMembers(i, item, penalty);
} else {
checkOtherMembers(i, item, penalty);
}
CharSequence itemText = null;
if (mAllowClipDataText) {
itemText = item.getText();
} else {
if (item.getText() != null) {
penalty.accept(
"Item text cannot contain value. Item position: " + i + "."
+ " Text: " + item.getText());
}
}
Uri itemUri = null;
if (mAllowedClipDataUri == null) {
if (item.getUri() != null) {
penalty.accept(
"Item URI is not allowed. Item position: " + i + ". URI: "
+ item.getUri());
}
} else {
if (item.getUri() == null || mAllowedClipDataUri.test(item.getUri())) {
itemUri = item.getUri();
} else {
penalty.accept(
"Item URI is not allowed. Item position: " + i + ". URI: "
+ item.getUri());
}
}
if (itemText != null || itemUri != null) {
if (newClipData == null) {
newClipData = new ClipData(clipData.getDescription(),
new ClipData.Item(itemText, null, itemUri));
} else {
newClipData.addItem(new ClipData.Item(itemText, null, itemUri));
}
}
}
if (newClipData != null) {
out.setClipData(newClipData);
}
}
}
private static void checkOtherMembers(int i, ClipData.Item item, Consumer<String> penalty) {
if (item.getHtmlText() != null || item.getIntent() != null) {
penalty.accept("ClipData item at position " + i + " contains htmlText, "
+ "textLinks or intent: " + item);
}
}
@RequiresApi(31)
private static class Api31Impl {
private Api31Impl() {
}
static void checkOtherMembers(int i, ClipData.Item item, Consumer<String> penalty) {
if (item.getHtmlText() != null || item.getIntent() != null
|| item.getTextLinks() != null) {
penalty.accept("ClipData item at position " + i + " contains htmlText, "
+ "textLinks or intent: " + item);
}
}
}
@RequiresApi(29)
private static class Api29Impl {
private Api29Impl() {
// This class is not instantiable.
}
static Intent setIdentifier(Intent intent, String identifier) {
return intent.setIdentifier(identifier);
}
static String getIdentifier(Intent intent) {
return intent.getIdentifier();
}
}
}