1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.lint.checks; 18 19 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_MANIFEST_XML; 20 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI; 21 import static com.android.tools.lint.detector.api.LintConstants.ATTR_EXPORTED; 22 import static com.android.tools.lint.detector.api.LintConstants.ATTR_PATH; 23 import static com.android.tools.lint.detector.api.LintConstants.ATTR_PATH_PATTERN; 24 import static com.android.tools.lint.detector.api.LintConstants.ATTR_PATH_PREFIX; 25 import static com.android.tools.lint.detector.api.LintConstants.ATTR_PERMISSION; 26 import static com.android.tools.lint.detector.api.LintConstants.ATTR_READ_PERMISSION; 27 import static com.android.tools.lint.detector.api.LintConstants.ATTR_WRITE_PERMISSION; 28 import static com.android.tools.lint.detector.api.LintConstants.TAG_APPLICATION; 29 import static com.android.tools.lint.detector.api.LintConstants.TAG_GRANT_PERMISSION; 30 import static com.android.tools.lint.detector.api.LintConstants.TAG_INTENT_FILTER; 31 import static com.android.tools.lint.detector.api.LintConstants.TAG_PATH_PERMISSION; 32 import static com.android.tools.lint.detector.api.LintConstants.TAG_PROVIDER; 33 import static com.android.tools.lint.detector.api.LintConstants.TAG_SERVICE; 34 35 import com.android.tools.lint.detector.api.Category; 36 import com.android.tools.lint.detector.api.Context; 37 import com.android.tools.lint.detector.api.Detector; 38 import com.android.tools.lint.detector.api.Issue; 39 import com.android.tools.lint.detector.api.JavaContext; 40 import com.android.tools.lint.detector.api.LintUtils; 41 import com.android.tools.lint.detector.api.Location; 42 import com.android.tools.lint.detector.api.Scope; 43 import com.android.tools.lint.detector.api.Severity; 44 import com.android.tools.lint.detector.api.Speed; 45 import com.android.tools.lint.detector.api.XmlContext; 46 47 import org.w3c.dom.Attr; 48 import org.w3c.dom.Element; 49 import org.w3c.dom.Node; 50 51 import java.io.File; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Collection; 55 import java.util.EnumSet; 56 import java.util.Iterator; 57 import java.util.List; 58 59 import lombok.ast.AstVisitor; 60 import lombok.ast.Expression; 61 import lombok.ast.ForwardingAstVisitor; 62 import lombok.ast.Identifier; 63 import lombok.ast.MethodInvocation; 64 import lombok.ast.StrictListAccessor; 65 66 /** 67 * Checks that exported services request a permission. 68 */ 69 public class SecurityDetector extends Detector implements Detector.XmlScanner, 70 Detector.JavaScanner { 71 72 /** Exported services */ 73 public static final Issue EXPORTED_SERVICE = Issue.create( 74 "ExportedService", //$NON-NLS-1$ 75 "Checks for exported services that do not require permissions", 76 "Exported services (services which either set exported=true or contain " + 77 "an intent-filter and do not specify exported=false) should define a " + 78 "permission that an entity must have in order to launch the service " + 79 "or bind to it. Without this, any application can use this service.", 80 Category.SECURITY, 81 5, 82 Severity.WARNING, 83 SecurityDetector.class, 84 EnumSet.of(Scope.MANIFEST)); 85 86 /** Exported content providers */ 87 public static final Issue EXPORTED_PROVIDER = Issue.create( 88 "ExportedContentProvider", //$NON-NLS-1$ 89 "Checks for exported content providers that do not require permissions", 90 "Content providers are exported by default and any application on the " + 91 "system can potentially use them to read and write data. If the content" + 92 "provider provides access to sensitive data, it should be protected by " + 93 "specifying export=false in the manifest or by protecting it with a " + 94 "permission that can be granted to other applications.", 95 Category.SECURITY, 96 5, 97 Severity.WARNING, 98 SecurityDetector.class, 99 EnumSet.of(Scope.MANIFEST)); 100 101 /** Content provides which grant all URIs access */ 102 public static final Issue OPEN_PROVIDER = Issue.create( 103 "GrantAllUris", //$NON-NLS-1$ 104 "Checks for <grant-uri-permission> elements where everything is shared", 105 "The <grant-uri-permission> element allows specific paths to be shared. " + 106 "This detector checks for a path URL of just '/' (everything), which is " + 107 "probably not what you want; you should limit access to a subset.", 108 Category.SECURITY, 109 7, 110 Severity.WARNING, 111 SecurityDetector.class, 112 EnumSet.of(Scope.MANIFEST)); 113 114 /** Using the world-writable flag */ 115 public static final Issue WORLD_WRITEABLE = Issue.create( 116 "WorldWriteableFiles", //$NON-NLS-1$ 117 "Checks for openFileOutput() and getSharedPreferences() calls passing " + 118 "MODE_WORLD_WRITEABLE", 119 "There are cases where it is appropriate for an application to write " + 120 "world writeable files, but these should be reviewed carefully to " + 121 "ensure that they contain no private data, and that if the file is " + 122 "modified by a malicious application it does not trick or compromise " + 123 "your application.", 124 Category.SECURITY, 125 4, 126 Severity.WARNING, 127 SecurityDetector.class, 128 Scope.JAVA_FILE_SCOPE); 129 130 131 /** Using the world-readable flag */ 132 public static final Issue WORLD_READABLE = Issue.create( 133 "WorldReadableFiles", //$NON-NLS-1$ 134 "Checks for openFileOutput() and getSharedPreferences() calls passing " + 135 "MODE_WORLD_READABLE", 136 "There are cases where it is appropriate for an application to write " + 137 "world readable files, but these should be reviewed carefully to " + 138 "ensure that they contain no private data that is leaked to other " + 139 "applications.", 140 Category.SECURITY, 141 4, 142 Severity.WARNING, 143 SecurityDetector.class, 144 Scope.JAVA_FILE_SCOPE); 145 146 /** Constructs a new {@link SecurityDetector} check */ SecurityDetector()147 public SecurityDetector() { 148 } 149 150 @Override getSpeed()151 public Speed getSpeed() { 152 return Speed.FAST; 153 } 154 @Override appliesTo(Context context, File file)155 public boolean appliesTo(Context context, File file) { 156 return file.getName().equals(ANDROID_MANIFEST_XML); 157 } 158 159 // ---- Implements Detector.XmlScanner ---- 160 161 @Override getApplicableElements()162 public Collection<String> getApplicableElements() { 163 return Arrays.asList( 164 TAG_SERVICE, 165 TAG_GRANT_PERMISSION, 166 TAG_PROVIDER 167 ); 168 } 169 170 @Override visitElement(XmlContext context, Element element)171 public void visitElement(XmlContext context, Element element) { 172 String tag = element.getTagName(); 173 if (tag.equals(TAG_SERVICE)) { 174 checkService(context, element); 175 } else if (tag.equals(TAG_GRANT_PERMISSION)) { 176 checkGrantPermission(context, element); 177 } else if (tag.equals(TAG_PROVIDER)) { 178 checkProvider(context, element); 179 } 180 } 181 checkService(XmlContext context, Element element)182 private void checkService(XmlContext context, Element element) { 183 String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED); 184 boolean exported; 185 if (exportValue != null && exportValue.length() > 0) { 186 exported = Boolean.valueOf(exportValue); 187 } else { 188 boolean haveIntentFilters = false; 189 for (Element child : LintUtils.getChildren(element)) { 190 if (child.getTagName().equals(TAG_INTENT_FILTER)) { 191 haveIntentFilters = true; 192 break; 193 } 194 } 195 exported = haveIntentFilters; 196 } 197 198 if (exported) { 199 // Make sure this service has a permission 200 String permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); 201 if (permission == null || permission.length() == 0) { 202 Node parent = element.getParentNode(); 203 if (parent.getNodeType() == Node.ELEMENT_NODE 204 && parent.getNodeName().equals(TAG_APPLICATION)) { 205 Element application = (Element) parent; 206 permission = application.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); 207 if (permission == null || permission.length() == 0) { 208 // No declared permission for this exported service: complain 209 context.report(EXPORTED_SERVICE, element, 210 context.getLocation(element), 211 "Exported service does not require permission", null); 212 } 213 } 214 } 215 } 216 } 217 checkGrantPermission(XmlContext context, Element element)218 private void checkGrantPermission(XmlContext context, Element element) { 219 Attr path = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH); 220 Attr prefix = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PREFIX); 221 Attr pattern = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PATTERN); 222 223 String msg = "Content provider shares everything; this is potentially dangerous."; 224 if (path != null && path.getValue().equals("/")) { //$NON-NLS-1$ 225 context.report(OPEN_PROVIDER, path, context.getLocation(path), msg, null); 226 } 227 if (prefix != null && prefix.getValue().equals("/")) { //$NON-NLS-1$ 228 context.report(OPEN_PROVIDER, prefix, context.getLocation(prefix), msg, null); 229 } 230 if (pattern != null && (pattern.getValue().equals("/") //$NON-NLS-1$ 231 /* || pattern.getValue().equals(".*")*/)) { 232 context.report(OPEN_PROVIDER, pattern, context.getLocation(pattern), msg, null); 233 } 234 } 235 checkProvider(XmlContext context, Element element)236 private void checkProvider(XmlContext context, Element element) { 237 String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED); 238 // Content providers are exported by default 239 boolean exported = true; 240 if (exportValue != null && exportValue.length() > 0) { 241 exported = Boolean.valueOf(exportValue); 242 } 243 244 if (exported) { 245 // Just check for some use of permissions. Other Lint checks can check the saneness 246 // of the permissions. We'll accept the permission, readPermission, or writePermission 247 // attributes on the provider element, or a path-permission element. 248 String permission = element.getAttributeNS(ANDROID_URI, ATTR_READ_PERMISSION); 249 if (permission == null || permission.length() == 0) { 250 permission = element.getAttributeNS(ANDROID_URI, ATTR_WRITE_PERMISSION); 251 if (permission == null || permission.length() == 0) { 252 permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); 253 if (permission == null || permission.length() == 0) { 254 // No permission attributes? Check for path-permission. 255 256 // TODO: Add a Lint check to ensure the path-permission is good, similar to 257 // the grant-uri-permission check. 258 boolean hasPermission = false; 259 for (Element child : LintUtils.getChildren(element)) { 260 String tag = child.getTagName(); 261 if (tag.equals(TAG_PATH_PERMISSION)) { 262 hasPermission = true; 263 break; 264 } 265 } 266 267 if (!hasPermission) { 268 context.report(EXPORTED_PROVIDER, element, context.getLocation(element), 269 "Exported content providers can provide access to " + 270 "potentially sensitive data", 271 null); 272 } 273 } 274 } 275 } 276 } 277 } 278 279 // ---- Implements Detector.JavaScanner ---- 280 281 @Override getApplicableMethodNames()282 public List<String> getApplicableMethodNames() { 283 // These are the API calls that can accept a MODE_WORLD_READABLE/MODE_WORLD_WRITABLE 284 // argument. 285 List<String> values = new ArrayList<String>(2); 286 values.add("openFileOutput"); //$NON-NLS-1$ 287 values.add("getSharedPreferences"); //$NON-NLS-1$ 288 return values; 289 } 290 291 @Override visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node)292 public void visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) { 293 StrictListAccessor<Expression,MethodInvocation> args = node.astArguments(); 294 Iterator<Expression> iterator = args.iterator(); 295 while (iterator.hasNext()) { 296 iterator.next().accept(visitor); 297 } 298 } 299 300 @Override createJavaVisitor(JavaContext context)301 public AstVisitor createJavaVisitor(JavaContext context) { 302 return new IdentifierVisitor(context); 303 } 304 305 private static class IdentifierVisitor extends ForwardingAstVisitor { 306 private final JavaContext mContext; 307 IdentifierVisitor(JavaContext context)308 public IdentifierVisitor(JavaContext context) { 309 super(); 310 this.mContext = context; 311 } 312 313 @Override visitIdentifier(Identifier node)314 public boolean visitIdentifier(Identifier node) { 315 if ("MODE_WORLD_WRITEABLE".equals(node.getDescription())) { //$NON-NLS-1$ 316 Location location = mContext.getLocation(node); 317 mContext.report(WORLD_WRITEABLE, node, location, 318 "Using MODE_WORLD_WRITEABLE when creating files can be " + 319 "risky, review carefully", 320 null); 321 } else if ("MODE_WORLD_READABLE".equals(node.getDescription())) { //$NON-NLS-1$ 322 Location location = mContext.getLocation(node); 323 mContext.report(WORLD_READABLE, node, location, 324 "Using MODE_WORLD_READABLE when creating files can be " + 325 "risky, review carefully", 326 null); 327 } 328 329 return false; 330 } 331 } 332 } 333