/* * 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; } } }