Real‑time Android Studio Source Code Security Scanning with FindBugs Extension (Code Arbiter)
Code Arbiter extends the FindBugs plugin to provide real‑time Android Studio source‑code security scanning, implementing line‑by‑line API misuse detection, empty TrustManager checks, taint‑analysis of sources and sinks, and custom bytecode checks for unprotected Intent/Bundle reads, all packaged as a JAR for instant developer feedback.
Android applications contain many security‑related bugs, but most existing vulnerability scanners work on the packaged APK, requiring a long scan cycle and manual source‑code mapping. Code Arbiter was created to provide real‑time feedback by scanning the source code directly inside Android Studio.
The most straightforward solution would be to write a brand‑new Android Studio plugin, but this approach demands a large amount of effort (learning IDE open APIs, building UI, re‑implementing low‑level modules) and may not achieve the detection accuracy of existing tools. Instead, the authors extended an existing static analysis plugin, FindBugs, which already supports class‑file inspection.
FindBugs was chosen because it can be packaged as a JAR and integrated as an IDE plugin. The extension process consists of importing a custom JAR into the FindBugs plugin and restarting the IDE. The following diagram shows the JAR installation process (omitted).
Extension methods
Four detection strategies are implemented:
Line‑by‑line inspection – overrides sawOpcode() to detect insecure Android API calls such as external storage access.
Method‑by‑method inspection – overrides visitClassContext() to find empty implementations of TrustManager.checkServerTrusted and similar methods.
Taint analysis – defines sources (e.g., EditText.getText(), Intent.getStringExtra()) and sinks (e.g., Runtime.exec()) and reports a bug when tainted data reaches a sink.
Custom detection – analyzes raw class bytecode (via javap) to locate Intent/Bundle parameter reads that are not wrapped in try/catch blocks, reporting a local denial‑of‑service issue.
Example of line‑by‑line detection code:
public class ExternalFileAccessDetector extends OpcodeStackDetector {
private static final String ANDROID_EXTERNAL_FILE_ACCESS_TYPE = "ANDROID_EXTERNAL_FILE_ACCESS";
private BugReporter bugReporter;
public ExternalFileAccessDetector(BugReporter bugReporter) { this.bugReporter = bugReporter; }
@Override
public void sawOpcode(int seen) {
if (seen == Constants.INVOKEVIRTUAL && (
getNameConstantOperand().equals("getExternalCacheDir") ||
getNameConstantOperand().equals("getExternalCacheDirs") ||
getNameConstantOperand().equals("getExternalFilesDir") ||
getNameConstantOperand().equals("getExternalFilesDirs") ||
getNameConstantOperand().equals("getExternalMediaDirs"))) {
bugReporter.reportBug(new BugInstance(this, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY)
.addClass(this).addMethod(this).addSourceLine(this));
} else if (seen == Constants.INVOKESTATIC && getClassConstantOperand().equals("android/os/Environment") &&
(getNameConstantOperand().equals("getExternalStorageDirectory") ||
getNameConstantOperand().equals("getExternalStoragePublicDirectory"))) {
bugReporter.reportBug(new BugInstance(this, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY)
.addClass(this).addMethod(this).addSourceLine(this));
}
}
}Example of method‑by‑method detection for empty TrustManager implementations:
public class WeakTrustManagerDetector implements Detector {
...
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass javaClass = classContext.getJavaClass();
boolean isTrustManager = InterfaceUtils.isSubtype(javaClass, "javax.net.ssl.X509TrustManager");
for (Method m : javaClass.getMethods()) {
MethodGen methodGen = classContext.getMethodGen(m);
if (isTrustManager && (m.getName().equals("checkClientTrusted") ||
m.getName().equals("checkServerTrusted") ||
m.getName().equals("getAcceptedIssuers"))) {
if (isEmptyImplementation(methodGen)) {
bugReporter.reportBug(new BugInstance(this, WEAK_TRUST_MANAGER_TYPE, Priorities.NORMAL_PRIORITY)
.addClassAndMethod(javaClass, m));
}
}
}
}
private boolean isEmptyImplementation(MethodGen methodGen) {
boolean invokeInst = false, loadField = false;
for (Iterator itIns = methodGen.getInstructionList().iterator(); itIns.hasNext(); ) {
Instruction inst = ((InstructionHandle) itIns.next()).getInstruction();
if (inst instanceof InvokeInstruction) invokeInst = true;
if (inst instanceof GETFIELD) loadField = true;
}
return !invokeInst && !loadField;
}
}Taint analysis defines sources such as:
- EditText.getText():TAINTED
- Intent.getStringExtra():TAINTED
- Bundle.getString():TAINTEDand sinks like:
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process;:0
java/lang/ProcessBuilder.command([Ljava/lang/String;)Ljava/lang/ProcessBuilder;:0The custom detector for local denial‑of‑service parses raw bytecode to locate Intent/Bundle reads and checks whether the corresponding instruction lies inside a try/catch block. If not, it reports a LOCAL_DENIAL_SERVICE bug.
private void analyzeMethod(JavaClass javaClass, Method m, ClassContext classContext) throws CFGBuilderException {
HashMap<String, List<Location>> all_line_location = get_line_location(m, classContext);
Code code = m.getCode();
StringCodeAnalysis sca = new StringCodeAnalysis(code);
String[] codes = sca.codes_String_Array();
int code_length = sca.get_Code_Length(sca.get_First_Code(codes));
int[] exception_scop = sca.getExceptionScope();
for (int i = 1; i < codes.length; i++) {
int line_index = sca.get_code_line_index(codes[i]);
if (line_index < code_length && codes[i].toLowerCase().contains("invokevirtual") &&
(codes[i].contains("android.content.Intent.get") || codes[i].contains("android.os.Bundle.get"))) {
boolean is_scope = false;
for (int j = 0; j < exception_scop.length; j += 2) {
if (line_index >= exception_scop[j] && line_index <= exception_scop[j + 1]) { is_scope = true; break; }
}
if (!is_scope) {
String method_name = get_method_name(codes[i]);
if (all_line_location.containsKey(method_name)) {
for (Location loc : all_line_location.get(method_name)) {
bugReporter.reportBug(new BugInstance(this, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY)
.addClass(javaClass).addMethod(javaClass, m).addSourceLine(classContext, m, loc));
}
} else {
bugReporter.reportBug(new BugInstance(this, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY)
.addClass(javaClass).addMethod(javaClass, m));
}
}
}
}
}After implementing the detectors, they must be registered in findbugs.xml and messages.xml. The registration links the detector class with a bug pattern and provides Chinese descriptions for developers.
Finally, the plugin is packaged as a JAR with Maven, installed into Android Studio, and used to scan source code in real time. The article concludes with a brief outlook: expanding the rule set to cover more Android security issues and improving rule precision to reduce false positives.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Meituan Technology Team
Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
