1 /*
2  * Copyright (C) 2021 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.google.errorprone.bugpatterns.android;
18 
19 import static com.google.errorprone.BugPattern.LinkType.NONE;
20 import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
21 import static com.google.errorprone.matchers.Matchers.allOf;
22 import static com.google.errorprone.matchers.Matchers.anyOf;
23 import static com.google.errorprone.matchers.Matchers.enclosingClass;
24 import static com.google.errorprone.matchers.Matchers.instanceMethod;
25 import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
26 import static com.google.errorprone.matchers.Matchers.methodInvocation;
27 import static com.google.errorprone.matchers.Matchers.methodIsNamed;
28 import static com.google.errorprone.matchers.Matchers.staticMethod;
29 
30 import android.annotation.RequiresPermission;
31 import android.annotation.SuppressLint;
32 
33 import com.google.auto.service.AutoService;
34 import com.google.common.base.Objects;
35 import com.google.errorprone.BugPattern;
36 import com.google.errorprone.VisitorState;
37 import com.google.errorprone.bugpatterns.BugChecker;
38 import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
39 import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
40 import com.google.errorprone.matchers.Description;
41 import com.google.errorprone.matchers.Matcher;
42 import com.google.errorprone.util.ASTHelpers;
43 import com.sun.source.tree.AssignmentTree;
44 import com.sun.source.tree.ClassTree;
45 import com.sun.source.tree.ExpressionTree;
46 import com.sun.source.tree.IdentifierTree;
47 import com.sun.source.tree.MemberSelectTree;
48 import com.sun.source.tree.MethodInvocationTree;
49 import com.sun.source.tree.MethodTree;
50 import com.sun.source.tree.NewClassTree;
51 import com.sun.source.tree.Tree;
52 import com.sun.source.tree.VariableTree;
53 import com.sun.source.util.TreeScanner;
54 import com.sun.tools.javac.code.Symbol;
55 import com.sun.tools.javac.code.Symbol.ClassSymbol;
56 import com.sun.tools.javac.code.Symbol.MethodSymbol;
57 import com.sun.tools.javac.code.Symbol.VarSymbol;
58 import com.sun.tools.javac.code.Type;
59 import com.sun.tools.javac.code.Type.ClassType;
60 
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.Collections;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Optional;
67 import java.util.Set;
68 import java.util.concurrent.atomic.AtomicReference;
69 import java.util.function.Predicate;
70 import java.util.regex.Pattern;
71 
72 import javax.lang.model.element.Name;
73 
74 /**
75  * Inspects both the client and server side of AIDL interfaces to ensure that
76  * any {@code RequiresPermission} annotations are consistently declared and
77  * enforced.
78  */
79 @AutoService(BugChecker.class)
80 @BugPattern(
81     name = "AndroidFrameworkRequiresPermission",
82     summary = "Verifies that @RequiresPermission annotations are consistent across AIDL",
83     linkType = NONE,
84     severity = WARNING)
85 public final class RequiresPermissionChecker extends BugChecker
86         implements MethodTreeMatcher, MethodInvocationTreeMatcher {
87     private static final Matcher<ExpressionTree> ENFORCE_VIA_CONTEXT = methodInvocation(
88             instanceMethod()
89                     .onDescendantOf("android.content.Context")
90                     .withNameMatching(
91                             Pattern.compile("^(enforce|check)(Calling)?(OrSelf)?Permission$")));
92     private static final Matcher<ExpressionTree> ENFORCE_VIA_CHECKER = methodInvocation(
93             staticMethod()
94                     .onClass("android.content.PermissionChecker")
95                     .withNameMatching(Pattern.compile("^check.*")));
96 
97     private static final Matcher<MethodTree> BINDER_INTERNALS = allOf(
98             enclosingClass(isSubtypeOf("android.os.IInterface")),
99             anyOf(
100                     methodIsNamed("onTransact"),
101                     methodIsNamed("dump"),
102                     enclosingClass(simpleNameMatches(Pattern.compile("^(Stub|Default|Proxy)$")))));
103     private static final Matcher<MethodTree> LOCAL_INTERNALS = anyOf(
104             methodIsNamed("finalize"),
105             allOf(
106                     enclosingClass(isSubtypeOf("android.content.BroadcastReceiver")),
107                     methodIsNamed("onReceive")),
108             allOf(
109                     enclosingClass(isSubtypeOf("android.database.ContentObserver")),
110                     methodIsNamed("onChange")),
111             allOf(
112                     enclosingClass(isSubtypeOf("android.os.Handler")),
113                     methodIsNamed("handleMessage")),
114             allOf(
115                     enclosingClass(isSubtypeOf("android.os.IBinder.DeathRecipient")),
116                     methodIsNamed("binderDied")));
117 
118     private static final Matcher<ExpressionTree> CLEAR_CALL = methodInvocation(staticMethod()
119             .onClass("android.os.Binder").withSignature("clearCallingIdentity()"));
120     private static final Matcher<ExpressionTree> RESTORE_CALL = methodInvocation(staticMethod()
121             .onClass("android.os.Binder").withSignature("restoreCallingIdentity(long)"));
122 
123     private static final Matcher<ExpressionTree> SEND_BROADCAST = methodInvocation(
124             instanceMethod()
125                     .onDescendantOf("android.content.Context")
126                     .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$")));
127     private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation(
128             instanceMethod()
129                     .onDescendantOf("android.app.PendingIntent")
130                     .named("send"));
131 
132     private static final Matcher<ExpressionTree> INTENT_SET_ACTION = methodInvocation(
133             instanceMethod().onDescendantOf("android.content.Intent").named("setAction"));
134 
135     @Override
matchMethod(MethodTree tree, VisitorState state)136     public Description matchMethod(MethodTree tree, VisitorState state) {
137         // Ignore methods without an implementation
138         if (tree.getBody() == null) return Description.NO_MATCH;
139 
140         // Ignore certain types of Binder generated code
141         if (BINDER_INTERNALS.matches(tree, state)) return Description.NO_MATCH;
142 
143         // Ignore known-local methods which don't need to propagate
144         if (LOCAL_INTERNALS.matches(tree, state)) return Description.NO_MATCH;
145 
146         // Ignore when suppressed via superclass
147         final MethodSymbol method = ASTHelpers.getSymbol(tree);
148         if (isSuppressedRecursively(method, state)) return Description.NO_MATCH;
149 
150         // First, look at all outgoing method invocations to ensure that we
151         // carry those annotations forward; yell if we're too narrow
152         final ParsedRequiresPermission expectedPerm = parseRequiresPermissionRecursively(
153                 method, state);
154         final ParsedRequiresPermission actualPerm = new ParsedRequiresPermission();
155         final Description desc = tree.accept(new TreeScanner<Description, Void>() {
156             private boolean clearedCallingIdentity = false;
157 
158             @Override
159             public Description visitMethodInvocation(MethodInvocationTree node, Void param) {
160                 if (CLEAR_CALL.matches(node, state)) {
161                     clearedCallingIdentity = true;
162                 } else if (RESTORE_CALL.matches(node, state)) {
163                     clearedCallingIdentity = false;
164                 } else if (!clearedCallingIdentity) {
165                     final ParsedRequiresPermission nodePerm = parseRequiresPermissionRecursively(
166                             node, state);
167                     if (!expectedPerm.containsAll(nodePerm)) {
168                         return buildDescription(node)
169                                 .setMessage("Method " + method.name.toString() + "() annotated "
170                                         + expectedPerm
171                                         + " but too narrow; invokes method requiring " + nodePerm)
172                                 .build();
173                     } else {
174                         actualPerm.addAll(nodePerm);
175                     }
176                 }
177                 return super.visitMethodInvocation(node, param);
178             }
179 
180             @Override
181             public Description reduce(Description r1, Description r2) {
182                 return (r1 != null) ? r1 : r2;
183             }
184         }, null);
185         if (desc != null) return desc;
186 
187         // Second, determine if we actually used all permissions that we claim
188         // to require; yell if we're too broad
189         if (!actualPerm.containsAll(expectedPerm)) {
190             return buildDescription(tree)
191                     .setMessage("Method " + method.name.toString() + "() annotated " + expectedPerm
192                             + " but too wide; only invokes methods requiring " + actualPerm
193                             + "\n  If calling an AIDL interface, it can be annotated by adding:"
194                             + "\n  @JavaPassthrough(annotation=\""
195                             + "@android.annotation.RequiresPermission(...)\")")
196                     .build();
197         }
198 
199         return Description.NO_MATCH;
200     }
201 
202     @Override
matchMethodInvocation(MethodInvocationTree tree, VisitorState state)203     public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
204         if (SEND_BROADCAST.matches(tree, state)) {
205             final ParsedRequiresPermission sourcePerm =
206                     parseBroadcastSourceRequiresPermission(tree, state);
207             final ParsedRequiresPermission targetPerm =
208                     parseBroadcastTargetRequiresPermission(tree, state);
209             if (sourcePerm == null) {
210                 return buildDescription(tree)
211                         .setMessage("Failed to resolve broadcast intent action for validation")
212                         .build();
213             } else if (!Objects.equal(sourcePerm, targetPerm)) {
214                 return buildDescription(tree)
215                         .setMessage("Broadcast annotated " + sourcePerm + " but protected with "
216                                 + targetPerm)
217                         .build();
218             }
219         }
220         return Description.NO_MATCH;
221     }
222 
223     static class ParsedRequiresPermission {
224         final Set<String> allOf = new HashSet<>();
225         final Set<String> anyOf = new HashSet<>();
226 
isEmpty()227         public boolean isEmpty() {
228             return allOf.isEmpty() && anyOf.isEmpty();
229         }
230 
231         /**
232          * Validate that this annotation effectively "contains" the given
233          * annotation. This is typically used to ensure that a method carries
234          * along all relevant annotations for the methods it invokes.
235          */
containsAll(ParsedRequiresPermission perm)236         public boolean containsAll(ParsedRequiresPermission perm) {
237             boolean allMet = allOf.containsAll(perm.allOf);
238             boolean anyMet = false;
239             if (perm.anyOf.isEmpty()) {
240                 anyMet = true;
241             } else {
242                 for (String anyPerm : perm.anyOf) {
243                     if (allOf.contains(anyPerm) || anyOf.contains(anyPerm)) {
244                         anyMet = true;
245                     }
246                 }
247             }
248             return allMet && anyMet;
249         }
250 
251         @Override
equals(Object obj)252         public boolean equals(Object obj) {
253             if (obj instanceof ParsedRequiresPermission) {
254                 final ParsedRequiresPermission other = (ParsedRequiresPermission) obj;
255                 return allOf.equals(other.allOf) && anyOf.equals(other.anyOf);
256             } else {
257                 return false;
258             }
259         }
260 
261         @Override
toString()262         public String toString() {
263             if (isEmpty()) {
264                 return "[none]";
265             }
266             String res = "{allOf=" + allOf;
267             if (!anyOf.isEmpty()) {
268                 res += " anyOf=" + anyOf;
269             }
270             res += "}";
271             return res;
272         }
273 
from(RequiresPermission perm)274         public static ParsedRequiresPermission from(RequiresPermission perm) {
275             final ParsedRequiresPermission res = new ParsedRequiresPermission();
276             res.addAll(perm);
277             return res;
278         }
279 
addAll(ParsedRequiresPermission perm)280         public void addAll(ParsedRequiresPermission perm) {
281             if (perm == null) return;
282             this.allOf.addAll(perm.allOf);
283             this.anyOf.addAll(perm.anyOf);
284         }
285 
addAll(RequiresPermission perm)286         public void addAll(RequiresPermission perm) {
287             if (perm == null) return;
288             if (!perm.value().isEmpty()) this.allOf.add(perm.value());
289             if (perm.allOf() != null) this.allOf.addAll(Arrays.asList(perm.allOf()));
290             if (perm.anyOf() != null) this.anyOf.addAll(Arrays.asList(perm.anyOf()));
291         }
292 
addConstValue(Tree tree)293         public void addConstValue(Tree tree) {
294             final Object value = ASTHelpers.constValue(tree);
295             if (value != null) {
296                 allOf.add(String.valueOf(value));
297             }
298         }
299     }
300 
findArgumentByParameterName(MethodInvocationTree tree, Predicate<String> paramName)301     private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree,
302             Predicate<String> paramName) {
303         final MethodSymbol sym = ASTHelpers.getSymbol(tree);
304         final List<VarSymbol> params = sym.getParameters();
305         for (int i = 0; i < params.size(); i++) {
306             if (paramName.test(params.get(i).name.toString())) {
307                 return tree.getArguments().get(i);
308             }
309         }
310         return null;
311     }
312 
resolveName(ExpressionTree tree)313     private static Name resolveName(ExpressionTree tree) {
314         if (tree instanceof IdentifierTree) {
315             return ((IdentifierTree) tree).getName();
316         } else if (tree instanceof MemberSelectTree) {
317             return resolveName(((MemberSelectTree) tree).getExpression());
318         } else {
319             return null;
320         }
321     }
322 
parseIntentAction(NewClassTree tree)323     private static ParsedRequiresPermission parseIntentAction(NewClassTree tree) {
324         final Optional<? extends ExpressionTree> arg = tree.getArguments().stream().findFirst();
325         if (arg.isPresent()) {
326             return ParsedRequiresPermission.from(
327                     ASTHelpers.getAnnotation(arg.get(), RequiresPermission.class));
328         } else {
329             return null;
330         }
331     }
332 
parseIntentAction(MethodInvocationTree tree)333     private static ParsedRequiresPermission parseIntentAction(MethodInvocationTree tree) {
334         return ParsedRequiresPermission.from(ASTHelpers.getAnnotation(
335                 tree.getArguments().get(0), RequiresPermission.class));
336     }
337 
parseBroadcastSourceRequiresPermission( MethodInvocationTree methodTree, VisitorState state)338     private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission(
339             MethodInvocationTree methodTree, VisitorState state) {
340         final ExpressionTree arg = findArgumentByParameterName(methodTree,
341                 (name) -> name.toLowerCase().contains("intent"));
342         if (arg instanceof IdentifierTree) {
343             final Name argName = ((IdentifierTree) arg).getName();
344             final MethodTree method = state.findEnclosing(MethodTree.class);
345             final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
346             method.accept(new TreeScanner<Void, Void>() {
347                 private ParsedRequiresPermission last;
348 
349                 @Override
350                 public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
351                     if (Objects.equal(methodTree, tree)) {
352                         res.set(last);
353                     } else {
354                         final Name name = resolveName(tree.getMethodSelect());
355                         if (Objects.equal(argName, name)
356                                 && INTENT_SET_ACTION.matches(tree, state)) {
357                             last = parseIntentAction(tree);
358                         }
359                     }
360                     return super.visitMethodInvocation(tree, param);
361                 }
362 
363                 @Override
364                 public Void visitAssignment(AssignmentTree tree, Void param) {
365                     final Name name = resolveName(tree.getVariable());
366                     final Tree init = tree.getExpression();
367                     if (Objects.equal(argName, name)
368                             && init instanceof NewClassTree) {
369                         last = parseIntentAction((NewClassTree) init);
370                     }
371                     return super.visitAssignment(tree, param);
372                 }
373 
374                 @Override
375                 public Void visitVariable(VariableTree tree, Void param) {
376                     final Name name = tree.getName();
377                     final ExpressionTree init = tree.getInitializer();
378                     if (Objects.equal(argName, name)
379                             && init instanceof NewClassTree) {
380                         last = parseIntentAction((NewClassTree) init);
381                     }
382                     return super.visitVariable(tree, param);
383                 }
384             }, null);
385             return res.get();
386         }
387         return null;
388     }
389 
parseBroadcastTargetRequiresPermission( MethodInvocationTree tree, VisitorState state)390     private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission(
391             MethodInvocationTree tree, VisitorState state) {
392         final ExpressionTree arg = findArgumentByParameterName(tree,
393                 (name) -> name.toLowerCase().contains("permission"));
394         final ParsedRequiresPermission res = new ParsedRequiresPermission();
395         if (arg != null) {
396             arg.accept(new TreeScanner<Void, Void>() {
397                 @Override
398                 public Void visitIdentifier(IdentifierTree tree, Void param) {
399                     res.addConstValue(tree);
400                     return super.visitIdentifier(tree, param);
401                 }
402 
403                 @Override
404                 public Void visitMemberSelect(MemberSelectTree tree, Void param) {
405                     res.addConstValue(tree);
406                     return super.visitMemberSelect(tree, param);
407                 }
408             }, null);
409         }
410         return res;
411     }
412 
parseRequiresPermissionRecursively( MethodInvocationTree tree, VisitorState state)413     private static ParsedRequiresPermission parseRequiresPermissionRecursively(
414             MethodInvocationTree tree, VisitorState state) {
415         if (ENFORCE_VIA_CONTEXT.matches(tree, state) && tree.getArguments().size() > 0) {
416             final ParsedRequiresPermission res = new ParsedRequiresPermission();
417             res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(0))));
418             return res;
419         } else if (ENFORCE_VIA_CHECKER.matches(tree, state) && tree.getArguments().size() > 1) {
420             final ParsedRequiresPermission res = new ParsedRequiresPermission();
421             res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(1))));
422             return res;
423         } else {
424             final MethodSymbol method = ASTHelpers.getSymbol(tree);
425             return parseRequiresPermissionRecursively(method, state);
426         }
427     }
428 
429     /**
430      * Parse any {@code RequiresPermission} annotations associated with the
431      * given method, defined either directly on the method or by any superclass.
432      */
parseRequiresPermissionRecursively( MethodSymbol method, VisitorState state)433     private static ParsedRequiresPermission parseRequiresPermissionRecursively(
434             MethodSymbol method, VisitorState state) {
435         final List<MethodSymbol> symbols = new ArrayList<>();
436         symbols.add(method);
437         symbols.addAll(ASTHelpers.findSuperMethods(method, state.getTypes()));
438 
439         final ParsedRequiresPermission res = new ParsedRequiresPermission();
440         for (MethodSymbol symbol : symbols) {
441             res.addAll(symbol.getAnnotation(RequiresPermission.class));
442         }
443         return res;
444     }
445 
isSuppressedRecursively(MethodSymbol method, VisitorState state)446     private boolean isSuppressedRecursively(MethodSymbol method, VisitorState state) {
447         // Is method suppressed anywhere?
448         if (isSuppressed(method)) return true;
449         for (MethodSymbol symbol : ASTHelpers.findSuperMethods(method, state.getTypes())) {
450             if (isSuppressed(symbol)) return true;
451         }
452 
453         // Is class suppressed anywhere?
454         final ClassSymbol clazz = ASTHelpers.enclosingClass(method);
455         if (isSuppressed(clazz)) return true;
456         Type type = clazz.getSuperclass();
457         while (type != null) {
458             if (isSuppressed(type.tsym)) return true;
459             if (type instanceof ClassType) {
460                 type = ((ClassType) type).supertype_field;
461             } else {
462                 type = null;
463             }
464         }
465         return false;
466     }
467 
isSuppressed(Symbol symbol)468     public boolean isSuppressed(Symbol symbol) {
469         return isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressWarnings.class))
470                 || isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressLint.class));
471     }
472 
isSuppressed(SuppressWarnings anno)473     private boolean isSuppressed(SuppressWarnings anno) {
474         return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames());
475     }
476 
isSuppressed(SuppressLint anno)477     private boolean isSuppressed(SuppressLint anno) {
478         return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames());
479     }
480 
simpleNameMatches(Pattern pattern)481     static Matcher<ClassTree> simpleNameMatches(Pattern pattern) {
482         return new Matcher<ClassTree>() {
483             @Override
484             public boolean matches(ClassTree tree, VisitorState state) {
485                 final CharSequence name = tree.getSimpleName().toString();
486                 return pattern.matcher(name).matches();
487             }
488         };
489     }
490 }
491