blob: 9071724ce95a0cb833948f5362cc4c7cbb1b1c24 [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_MANIFEST_XML;
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.Context;
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.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.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Identifier;
import lombok.ast.MethodInvocation;
import lombok.ast.StrictListAccessor;
/**
* Checks that exported services request a permission.
*/
public class SecurityDetector extends Detector implements Detector.XmlScanner,
Detector.JavaScanner {
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 the world-writable flag */
public static final Issue WORLD_WRITEABLE = Issue.create(
"WorldWriteableFiles", //$NON-NLS-1$
"`openFileOutput()` 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()` 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);
/** Constructs a new {@link SecurityDetector} check */
public SecurityDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return file.getName().equals(ANDROID_MANIFEST_XML);
}
// ---- 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_WRITABLE
// argument.
List<String> values = new ArrayList<String>(2);
values.add("openFileOutput"); //$NON-NLS-1$
values.add("getSharedPreferences"); //$NON-NLS-1$
return values;
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
StrictListAccessor<Expression,MethodInvocation> args = node.astArguments();
for (Expression arg : args) {
arg.accept(visitor);
}
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new IdentifierVisitor(context);
}
private static class IdentifierVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
public IdentifierVisitor(JavaContext context) {
super();
mContext = context;
}
@Override
public boolean visitIdentifier(Identifier node) {
if ("MODE_WORLD_WRITEABLE".equals(node.astValue())) { //$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(node.astValue())) { //$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");
}
return false;
}
}
}