blob: 6b75c9a7d6fc96629c7a0f7dcb36d1bfda7ec15a [file] [log] [blame]
/*
* Copyright (C) 2011 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 com.android.tools.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_EXPORTED;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PATH;
import static com.android.SdkConstants.ATTR_PATH_PATTERN;
import static com.android.SdkConstants.ATTR_PATH_PREFIX;
import static com.android.SdkConstants.ATTR_PERMISSION;
import static com.android.SdkConstants.ATTR_READ_PERMISSION;
import static com.android.SdkConstants.ATTR_WRITE_PERMISSION;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_GRANT_PERMISSION;
import static com.android.SdkConstants.TAG_INTENT_FILTER;
import static com.android.SdkConstants.TAG_PATH_PERMISSION;
import static com.android.SdkConstants.TAG_PROVIDER;
import static com.android.SdkConstants.TAG_RECEIVER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
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.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Checks that exported services request a permission.
*/
public class SecurityDetector extends Detector implements XmlScanner, JavaPsiScanner {
private static final Implementation IMPLEMENTATION_MANIFEST = new Implementation(
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
private static final Implementation IMPLEMENTATION_JAVA = new Implementation(
SecurityDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Exported services */
public static final Issue EXPORTED_SERVICE = Issue.create(
"ExportedService", //$NON-NLS-1$
"Exported service does not require permission",
"Exported services (services which either set `exported=true` or contain " +
"an intent-filter and do not specify `exported=false`) should define a " +
"permission that an entity must have in order to launch the service " +
"or bind to it. Without this, any application can use this service.",
Category.SECURITY,
5,
Severity.WARNING,
IMPLEMENTATION_MANIFEST);
/** Exported content providers */
public static final Issue EXPORTED_PROVIDER = Issue.create(
"ExportedContentProvider", //$NON-NLS-1$
"Content provider does not require permission",
"Content providers are exported by default and any application on the " +
"system can potentially use them to read and write data. If the content " +
"provider provides access to sensitive data, it should be protected by " +
"specifying `export=false` in the manifest or by protecting it with a " +
"permission that can be granted to other applications.",
Category.SECURITY,
5,
Severity.WARNING,
IMPLEMENTATION_MANIFEST);
/** Exported receivers */
public static final Issue EXPORTED_RECEIVER = Issue.create(
"ExportedReceiver", //$NON-NLS-1$
"Receiver does not require permission",
"Exported receivers (receivers which either set `exported=true` or contain " +
"an intent-filter and do not specify `exported=false`) should define a " +
"permission that an entity must have in order to launch the receiver " +
"or bind to it. Without this, any application can use this receiver.",
Category.SECURITY,
5,
Severity.WARNING,
IMPLEMENTATION_MANIFEST);
/** Content provides which grant all URIs access */
public static final Issue OPEN_PROVIDER = Issue.create(
"GrantAllUris", //$NON-NLS-1$
"Content provider shares everything",
"The `<grant-uri-permission>` element allows specific paths to be shared. " +
"This detector checks for a path URL of just '/' (everything), which is " +
"probably not what you want; you should limit access to a subset.",
Category.SECURITY,
7,
Severity.WARNING,
IMPLEMENTATION_MANIFEST);
/** Using java.io.File.setReadable(true, false) to set file world-readable */
public static final Issue SET_READABLE = Issue.create(
"SetWorldReadable",
"`File.setReadable()` used to make file world-readable",
"Setting files world-readable is very dangerous, and likely to cause security " +
"holes in applications. It is strongly discouraged; instead, applications should " +
"use more formal mechanisms for interactions such as `ContentProvider`, " +
"`BroadcastReceiver`, and `Service`.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION_JAVA);
/** Using java.io.File.setWritable(true, false) to set file world-writable */
public static final Issue SET_WRITABLE = Issue.create(
"SetWorldWritable",
"`File.setWritable()` used to make file world-writable",
"Setting files world-writable is very dangerous, and likely to cause security " +
"holes in applications. It is strongly discouraged; instead, applications should " +
"use more formal mechanisms for interactions such as `ContentProvider`, " +
"`BroadcastReceiver`, and `Service`.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION_JAVA);
/** Using the world-writable flag */
public static final Issue WORLD_WRITEABLE = Issue.create(
"WorldWriteableFiles", //$NON-NLS-1$
"`openFileOutput()` or similar call passing `MODE_WORLD_WRITEABLE`",
"There are cases where it is appropriate for an application to write " +
"world writeable files, but these should be reviewed carefully to " +
"ensure that they contain no private data, and that if the file is " +
"modified by a malicious application it does not trick or compromise " +
"your application.",
Category.SECURITY,
4,
Severity.WARNING,
IMPLEMENTATION_JAVA);
/** Using the world-readable flag */
public static final Issue WORLD_READABLE = Issue.create(
"WorldReadableFiles", //$NON-NLS-1$
"`openFileOutput()` or similar call passing `MODE_WORLD_READABLE`",
"There are cases where it is appropriate for an application to write " +
"world readable files, but these should be reviewed carefully to " +
"ensure that they contain no private data that is leaked to other " +
"applications.",
Category.SECURITY,
4,
Severity.WARNING,
IMPLEMENTATION_JAVA);
private static final String FILE_CLASS = "java.io.File"; //$NON-NLS-1$
/** Constructs a new {@link SecurityDetector} check */
public SecurityDetector() {
}
// ---- Implements Detector.XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_SERVICE,
TAG_GRANT_PERMISSION,
TAG_PROVIDER,
TAG_ACTIVITY,
TAG_RECEIVER
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
if (tag.equals(TAG_SERVICE)) {
checkService(context, element);
} else if (tag.equals(TAG_GRANT_PERMISSION)) {
checkGrantPermission(context, element);
} else if (tag.equals(TAG_PROVIDER)) {
checkProvider(context, element);
} else if (tag.equals(TAG_RECEIVER)) {
checkReceiver(context, element);
}
}
public static boolean getExported(Element element) {
// Used to check whether an activity, service or broadcast receiver is exported.
String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED);
if (exportValue != null && !exportValue.isEmpty()) {
return Boolean.valueOf(exportValue);
} else {
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
return true;
}
}
}
return false;
}
private static boolean isUnprotectedByPermission(Element element) {
// Used to check whether an activity, service or broadcast receiver are
// protected by a permission.
String permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION);
if (permission == null || permission.isEmpty()) {
Node parent = element.getParentNode();
if (parent.getNodeType() == Node.ELEMENT_NODE
&& parent.getNodeName().equals(TAG_APPLICATION)) {
Element application = (Element) parent;
permission = application.getAttributeNS(ANDROID_URI, ATTR_PERMISSION);
return permission == null || permission.isEmpty();
}
}
return false;
}
private static boolean isWearableBindListener(@NonNull Element element) {
// Checks whether a service has an Android Wear bind listener
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
for (Element innerChild : LintUtils.getChildren(child)) {
if (innerChild.getTagName().equals(NODE_ACTION)) {
String name = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if ("com.google.android.gms.wearable.BIND_LISTENER".equals(name)) {
return true;
}
}
}
}
}
return false;
}
private static boolean isStandardReceiver(Element element) {
// Play Services also the following receiver which we'll consider standard
// in the sense that it doesn't require a separate permission
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if ("com.google.android.gms.tagmanager.InstallReferrerReceiver".equals(name)) {
return true;
}
// Checks whether a broadcast receiver receives a standard Android action
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
for (Element innerChild : LintUtils.getChildren(child)) {
if (innerChild.getTagName().equals(NODE_ACTION)) {
String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
return categoryString.startsWith("android."); //$NON-NLS-1$
}
}
}
}
return false;
}
private static void checkReceiver(XmlContext context, Element element) {
if (getExported(element) && isUnprotectedByPermission(element) &&
!isStandardReceiver(element)) {
// No declared permission for this exported receiver: complain
context.report(EXPORTED_RECEIVER, element, context.getLocation(element),
"Exported receiver does not require permission");
}
}
private static void checkService(XmlContext context, Element element) {
if (getExported(element) && isUnprotectedByPermission(element)
&& !isWearableBindListener(element)) {
// No declared permission for this exported service: complain
context.report(EXPORTED_SERVICE, element, context.getLocation(element),
"Exported service does not require permission");
}
}
private static void checkGrantPermission(XmlContext context, Element element) {
Attr path = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH);
Attr prefix = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PREFIX);
Attr pattern = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PATTERN);
String msg = "Content provider shares everything; this is potentially dangerous.";
if (path != null && path.getValue().equals("/")) { //$NON-NLS-1$
context.report(OPEN_PROVIDER, path, context.getLocation(path), msg);
}
if (prefix != null && prefix.getValue().equals("/")) { //$NON-NLS-1$
context.report(OPEN_PROVIDER, prefix, context.getLocation(prefix), msg);
}
if (pattern != null && (pattern.getValue().equals("/") //$NON-NLS-1$
/* || pattern.getValue().equals(".*")*/)) {
context.report(OPEN_PROVIDER, pattern, context.getLocation(pattern), msg);
}
}
private static void checkProvider(XmlContext context, Element element) {
String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED);
// Content providers are exported by default
boolean exported = true;
if (exportValue != null && !exportValue.isEmpty()) {
exported = Boolean.valueOf(exportValue);
}
if (exported) {
// Just check for some use of permissions. Other Lint checks can check the saneness
// of the permissions. We'll accept the permission, readPermission, or writePermission
// attributes on the provider element, or a path-permission element.
String permission = element.getAttributeNS(ANDROID_URI, ATTR_READ_PERMISSION);
if (permission == null || permission.isEmpty()) {
permission = element.getAttributeNS(ANDROID_URI, ATTR_WRITE_PERMISSION);
if (permission == null || permission.isEmpty()) {
permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION);
if (permission == null || permission.isEmpty()) {
// No permission attributes? Check for path-permission.
// TODO: Add a Lint check to ensure the path-permission is good, similar to
// the grant-uri-permission check.
boolean hasPermission = false;
for (Element child : LintUtils.getChildren(element)) {
String tag = child.getTagName();
if (tag.equals(TAG_PATH_PERMISSION)) {
hasPermission = true;
break;
}
}
if (!hasPermission) {
context.report(EXPORTED_PROVIDER, element,
context.getLocation(element),
"Exported content providers can provide access to " +
"potentially sensitive data");
}
}
}
}
}
}
// ---- Implements Detector.JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
// These are the API calls that can accept a MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE
// argument.
List<String> values = new ArrayList<String>(3);
values.add("openFileOutput"); //$NON-NLS-1$
values.add("getSharedPreferences"); //$NON-NLS-1$
values.add("getDir"); //$NON-NLS-1$
// These API calls can be used to set files world-readable or world-writable
values.add("setReadable"); //$NON-NLS-1$
values.add("setWritable"); //$NON-NLS-1$
return values;
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiMethodCallExpression node, @NonNull PsiMethod method) {
PsiExpression[] args = node.getArgumentList().getExpressions();
String methodName = node.getMethodExpression().getReferenceName();
if (context.getEvaluator().isMemberInSubClassOf(method, FILE_CLASS, false)) {
// Report calls to java.io.File.setReadable(true, false) or
// java.io.File.setWritable(true, false)
if ("setReadable".equals(methodName)) {
if (args.length == 2 &&
Boolean.TRUE.equals(ConstantEvaluator.evaluate(context, args[0])) &&
Boolean.FALSE.equals(ConstantEvaluator.evaluate(context, args[1]))) {
context.report(SET_READABLE, node, context.getLocation(node),
"Setting file permissions to world-readable can be " +
"risky, review carefully");
}
return;
} else if ("setWritable".equals(methodName)) {
if (args.length == 2 &&
Boolean.TRUE.equals(ConstantEvaluator.evaluate(context, args[0])) &&
Boolean.FALSE.equals(ConstantEvaluator.evaluate(context, args[1]))) {
context.report(SET_WRITABLE, node, context.getLocation(node),
"Setting file permissions to world-writable can be " +
"risky, review carefully");
}
return;
}
}
assert visitor != null;
for (PsiExpression arg : args) {
arg.accept(visitor);
}
}
@Nullable
@Override
public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) {
return new IdentifierVisitor(context);
}
private static class IdentifierVisitor extends JavaElementVisitor {
private final JavaContext mContext;
public IdentifierVisitor(JavaContext context) {
super();
mContext = context;
}
@Override
public void visitReferenceExpression(PsiReferenceExpression node) {
super.visitReferenceExpression(node);
String name = node.getReferenceName();
if ("MODE_WORLD_WRITEABLE".equals(name)) { //$NON-NLS-1$
Location location = mContext.getLocation(node);
mContext.report(WORLD_WRITEABLE, node, location,
"Using `MODE_WORLD_WRITEABLE` when creating files can be " +
"risky, review carefully");
} else if ("MODE_WORLD_READABLE".equals(name)) { //$NON-NLS-1$
Location location = mContext.getLocation(node);
mContext.report(WORLD_READABLE, node, location,
"Using `MODE_WORLD_READABLE` when creating files can be " +
"risky, review carefully");
}
}
}
}