Java tutorial
/* * Copyright (C) 2015 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 com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.ide.common.repository.GradleVersion; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.ClassContext; 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.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Project; 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.google.common.base.Charsets; import com.google.common.io.Files; import com.google.common.io.LineProcessor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodNode; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayDeque; import java.util.Deque; import java.util.EnumSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Checks to detect vulnerable versions of Apache Cordova. */ public class CordovaVersionDetector extends Detector implements Detector.ClassScanner { private static final Implementation IMPL = new Implementation(CordovaVersionDetector.class, EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES)); /** Vulnerable Cordova Version */ public static final Issue ISSUE = Issue .create("VulnerableCordovaVersion", "Vulnerable Cordova Version", "The version of Cordova used in the app is vulnerable to security " + "issues. Please update to the latest Apache Cordova version.", Category.SECURITY, 9, Severity.WARNING, IMPL) .addMoreInfo("https://cordova.apache.org/announcements/2015/11/20/security.html"); // All cordova versions below 4.1.1 are considered vulnerable. private static final GradleVersion VALID_CORDOVA_VERSION = GradleVersion.parse("4.1.1"); /** Version string format in a class file. Note that any qualifiers such as -dev are ignored.*/ private static final Pattern VERSION_STR = Pattern.compile("(\\d+\\.\\d+\\.\\d+).*"); /** The class name that declares the cordovaVersion for versions < 3.x.x */ private static final String FQN_CORDOVA_DEVICE = "org/apache/cordova/Device"; //$NON-NLS-1$ /** Class name that declares CORDOVA_VERSION static field for versions >= 3.x.x */ /** The name of the static field if present in the {@code FQN_CORDOVA_DEVICE} */ private static final String FIELD_NAME_CORDOVA_VERSION = "cordovaVersion"; //$NON-NLS-1$ private static final String FQN_CORDOVA_WEBVIEW = "org/apache/cordova/CordovaWebView"; //$NON-NLS-1$ /** The name of the static final field present in {@code FQN_CORDOVA_WEBVIEW} */ private static final String FIELD_NAME_CORDOVA_VERSION_WEBVIEW = "CORDOVA_VERSION"; //$NON-NLS-1$ private static final String CORDOVA_DOT_JS = "cordova.js"; //$NON-NLS-1$ private static final FilenameFilter CORDOVA_JS_FILTER = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.startsWith(CORDOVA_DOT_JS) || new File(dir, filename).isDirectory(); } }; /** Constructs a new {@link CordovaVersionDetector} check */ public CordovaVersionDetector() { } /** * The cordova version is similar to a dewey decimal like system * used by {@link GradleVersion} except for the named qualifiers. */ private GradleVersion mCordovaVersion; @NonNull @Override public Speed getSpeed() { return Speed.NORMAL; } @Override public void afterCheckProject(@NonNull Context context) { checkAssetsFolder(context); } private void checkAssetsFolder(@NonNull Context context) { if (!context.getProject().getReportIssues()) { // If this is a library project not being analyzed, ignore it return; } Project project = context.getProject(); List<File> assetFolders = project.getAssetFolders(); Deque<File> files = new ArrayDeque<File>(); for (File assetFolder : assetFolders) { // The js file could be at an arbitrary level in the directory tree. // examples: // assets/www/cordova.js // assets/www/plugins/phonegap/cordova.js.2.2.android files.push(assetFolder); while (!files.isEmpty() && mCordovaVersion == null) { File current = files.pop(); if (current.isDirectory()) { File[] filtered = current.listFiles(CORDOVA_JS_FILTER); if (filtered != null) { for (File file : filtered) { files.push(file); } } } else { checkFile(context, current); } } files.clear(); } } private void checkFile(@NonNull Context context, @NonNull File file) { if (mCordovaVersion == null && file.getPath().contains(CORDOVA_DOT_JS) && file.getName().startsWith(CORDOVA_DOT_JS)) { try { mCordovaVersion = Files.readLines(file, Charsets.UTF_8, new JsVersionLineProcessor()); if (mCordovaVersion != null) { validateCordovaVersion(context, mCordovaVersion, Location.create(file)); } } catch (IOException ignore) { // Ignore this exception. } } } private static void validateCordovaVersion(@NonNull Context context, @NonNull GradleVersion cordovaVersion, Location location) { if (cordovaVersion.compareIgnoringQualifiers(VALID_CORDOVA_VERSION) < 0) { String message = String.format("You are using a vulnerable version of Cordova: %1$s", cordovaVersion.toString()); context.report(ISSUE, location, message); } } // ---- Implements ClassScanner ---- @Override public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { //noinspection VariableNotUsedInsideIf if (mCordovaVersion != null) { // Exit early if we have already found the cordova version in the JS file. // This will be the case for all versions > 3.x.x return; } // For cordova versions such as 2.7.1, the version is a static *non-final* field in // a class named Device. Since it is non-final, it is initialized in the <clinit> method. // Example: // // ldc #5 // String 2.7.1 // putstatic #6 // Field cordovaVersion:Ljava/lang/String; // ... if (classNode.name.equals(FQN_CORDOVA_DEVICE)) { //noinspection unchecked ASM api. List<MethodNode> methods = classNode.methods; for (MethodNode method : methods) { if (SdkConstants.CLASS_CONSTRUCTOR.equals(method.name)) { InsnList nodes = method.instructions; for (int i = 0, n = nodes.size(); i < n; i++) { AbstractInsnNode instruction = nodes.get(i); int type = instruction.getType(); if (type == AbstractInsnNode.FIELD_INSN) { checkInstructionInternal(context, classNode, instruction); break; } } } } } else if (classNode.name.equals(FQN_CORDOVA_WEBVIEW)) { // For versions > 3.x.x, the version string is stored as a static final String in // CordovaWebView. // Note that this is also stored in the cordova.js.* but from a lint api perspective, // it's much faster to look it up here than load and check in the JS file. // e.g. // public static final java.lang.String CORDOVA_VERSION; // descriptor: Ljava/lang/String; // flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL // ConstantValue: String 4.1.1 // //noinspection unchecked ASM api. List<FieldNode> fields = classNode.fields; for (FieldNode node : fields) { if (FIELD_NAME_CORDOVA_VERSION_WEBVIEW.equals(node.name) && (node.access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL // is final && (node.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC // is static && node.value instanceof String) { mCordovaVersion = createVersion((String) node.value); if (mCordovaVersion != null) { validateCordovaVersion(context, mCordovaVersion, context.getLocation(classNode)); } } } } } private void checkInstructionInternal(ClassContext context, ClassNode classNode, AbstractInsnNode instruction) { FieldInsnNode node = (FieldInsnNode) instruction; if (node.getOpcode() == Opcodes.PUTSTATIC && node.owner.equals(FQN_CORDOVA_DEVICE) && node.name.equals(FIELD_NAME_CORDOVA_VERSION)) { AbstractInsnNode prevInstruction = LintUtils.getPrevInstruction(node); if (prevInstruction == null || prevInstruction.getOpcode() != Opcodes.LDC) { return; } LdcInsnNode ldcInsnNode = (LdcInsnNode) prevInstruction; if (ldcInsnNode.cst instanceof String) { mCordovaVersion = createVersion((String) ldcInsnNode.cst); if (mCordovaVersion != null) { validateCordovaVersion(context, mCordovaVersion, context.getLocation(classNode)); } } } } private static GradleVersion createVersion(String version) { Matcher matcher = VERSION_STR.matcher(version); if (matcher.matches()) { return GradleVersion.tryParse(matcher.group(1)); } return null; } /** * A {@link LineProcessor} that matches a specific cordova pattern and if successful terminates * early. The constant is typically declared at the start of the file so it makes it efficient * to terminate early. */ public static class JsVersionLineProcessor implements LineProcessor<GradleVersion> { // var CORDOVA_JS_BUILD_LABEL = '3.5.1-dev'; private static final Pattern PATTERN = Pattern .compile("var\\s*(PLATFORM_VERSION_BUILD_LABEL|CORDOVA_JS_BUILD_LABEL)\\s*" + "=\\s*'(\\d+\\.\\d+\\.\\d+)[^']*';.*"); private GradleVersion mVersion; @Override public boolean processLine(@NonNull String line) throws IOException { Matcher matcher = PATTERN.matcher(line); if (matcher.matches()) { mVersion = GradleVersion.tryParse(matcher.group(2)); return false; // stop processing other lines. } return true; } @Override public GradleVersion getResult() { return mVersion; } } }