blob: 57f4af4e7e1ce60e8dafd5c8673562c89f0fb11c [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 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.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.Iterator;
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 {
/** Exported services */
public static final Issue EXPORTED_SERVICE = Issue.create(
"ExportedService", //$NON-NLS-1$
"Checks for exported services that do not require permissions",
"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,
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
/** Exported content providers */
public static final Issue EXPORTED_PROVIDER = Issue.create(
"ExportedContentProvider", //$NON-NLS-1$
"Checks for exported content providers that do not require permissions",
"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,
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
/** Exported activities */
public static final Issue EXPORTED_ACTIVITY = Issue.create(
"ExportedActivity", //$NON-NLS-1$
"Checks for exported activities that do not require permissions",
"Exported activities (activities 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 activity " +
"or bind to it. Without this, any application can use this activity.",
Category.SECURITY,
2,
Severity.WARNING,
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
/** Exported receivers */
public static final Issue EXPORTED_RECEIVER = Issue.create(
"ExportedReceiver", //$NON-NLS-1$
"Checks for exported receivers that do not require permissions",
"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,
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
/** Content provides which grant all URIs access */
public static final Issue OPEN_PROVIDER = Issue.create(
"GrantAllUris", //$NON-NLS-1$
"Checks for <grant-uri-permission> elements where everything is shared",
"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,
SecurityDetector.class,
Scope.MANIFEST_SCOPE);
/** Using the world-writable flag */
public static final Issue WORLD_WRITEABLE = Issue.create(
"WorldWriteableFiles", //$NON-NLS-1$
"Checks for openFileOutput() and getSharedPreferences() calls 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,
SecurityDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Using the world-readable flag */
public static final Issue WORLD_READABLE = Issue.create(
"WorldReadableFiles", //$NON-NLS-1$
"Checks for openFileOutput() and getSharedPreferences() calls 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,
SecurityDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Constructs a new {@link SecurityDetector} check */
public SecurityDetector() {
}
@Override
public @NonNull 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_ACTIVITY)) {
checkActivity(context, element);
} else if (tag.equals(TAG_RECEIVER)) {
checkReceiver(context, element);
}
}
private 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.length() > 0) {
return Boolean.valueOf(exportValue);
} else {
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
return true;
}
}
}
return false;
}
private 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.length() == 0) {
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.length() == 0;
}
}
return false;
}
private boolean isLauncher(Element element) {
// Checks whether an element is a launcher activity.
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
for (Element innerChild: LintUtils.getChildren(child)) {
if (innerChild.getTagName().equals("category")) { //$NON-NLS-1$
String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
return "android.intent.category.LAUNCHER".equals(categoryString); //$NON-NLS-1$
}
}
}
}
return false;
}
private void checkActivity(XmlContext context, Element element) {
// Do not flag launch activities. Even if not explicitly exported, it's
// safe to assume that those activities should be exported.
if (getExported(element) && isUnprotectedByPermission(element) && !isLauncher(element)) {
// No declared permission for this exported activity: complain
context.report(EXPORTED_ACTIVITY, element, context.getLocation(element),
"Exported activity does not require permission", null);
}
}
private boolean isStandardReceiver(Element element) {
// 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("action")) { //$NON-NLS-1$
String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
return categoryString.startsWith("android."); //$NON-NLS-1$
}
}
}
}
return false;
}
private 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", null);
}
}
private void checkService(XmlContext context, Element element) {
if (getExported(element) && isUnprotectedByPermission(element)) {
// No declared permission for this exported service: complain
context.report(EXPORTED_SERVICE, element, context.getLocation(element),
"Exported service does not require permission", null);
}
}
private 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, null);
}
if (prefix != null && prefix.getValue().equals("/")) { //$NON-NLS-1$
context.report(OPEN_PROVIDER, prefix, context.getLocation(prefix), msg, null);
}
if (pattern != null && (pattern.getValue().equals("/") //$NON-NLS-1$
/* || pattern.getValue().equals(".*")*/)) {
context.report(OPEN_PROVIDER, pattern, context.getLocation(pattern), msg, null);
}
}
private 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.length() > 0) {
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.length() == 0) {
permission = element.getAttributeNS(ANDROID_URI, ATTR_WRITE_PERMISSION);
if (permission == null || permission.length() == 0) {
permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION);
if (permission == null || permission.length() == 0) {
// 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",
null);
}
}
}
}
}
}
// ---- 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();
Iterator<Expression> iterator = args.iterator();
while (iterator.hasNext()) {
iterator.next().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.getDescription())) { //$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",
null);
} else if ("MODE_WORLD_READABLE".equals(node.getDescription())) { //$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",
null);
}
return false;
}
}
}