Android 静态代码扫描流程及工具说明

1. 静态扫描流程

1.1 版本发布流程

大致分为5个阶段,静态代码扫描的工作在第3步进行,如图:

版本发布流程图

1.2 典型案例分析

  • [空指针]空指针引用
  • [内存泄露]Stream资源关闭
  • [性能]使用indexOf(字符)
  • [兼容]系统API兼容性隐患
  • [越界]数组下标越界隐患
  • [异常] 使用除法或求余没有判断分母长度隐患
  • [SQL]注入风险
  • [应用安全] AndroidMannifest.xml文件中allowBackup设置为true时会导致数据泄露

更多的错误检查示例请查看各检查工具的检查规则说明文档。

1.2.1 [空指针]空指针引用

错误位置:4

public class StringUtil {
  public static final String queryParams(String param) {
    String ret = "";
    if (param != null || param.length() > 2) {
      ret = param.substring(1, param.length() - 1);
    }
  return ret;
}

存在空指针引用,会导致空指针异常。解决方案:

public class StringUtil {
  public static final String queryParams(String param) {
    String ret = "";
    if (param != null && param.length() > 2) {
      ret = param.substring(1, param.length() - 1);
    }
  return ret;
}

1.2.2 [内存泄露]Stream资源关闭

错误位置:17

private static void write2logfile(String msg) {
    try {

        File sdCardDir = android.os.Environment
                .getExternalStorageDirectory();

        File logfile = new File(sdCardDir.getAbsolutePath()
                + File.separator + logfileName);

        if (!logfile.exists()) {
            logfile.createNewFile();
        }

        msg += "\n";

        FileOutputStream outputStream = new FileOutputStream(logfile, true);
        outputStream.write(msg.getBytes());
        outputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

资源对象在被关闭或者Return之前可能出现异常,导致无法正常关闭或Return。比如连续关闭多个资源对象时没有进行异常捕获,或者资源对象在Return之前进行了未捕获异常的操作。解决方案:

private static void write2logfile(String msg) {
    try {

        File sdCardDir = android.os.Environment
                .getExternalStorageDirectory();

        File logfile = new File(sdCardDir.getAbsolutePath()
                + File.separator + logfileName);

        if (!logfile.exists()) {
            logfile.createNewFile();
        }

        msg += "\n";

        FileOutputStream outputStream = new FileOutputStream(logfile, true);
        outputStream.write(msg.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally{
      if(null != outputStream){
        outputStream.close();
      }
    }
}

1.2.3 [性能]使用indexOf(字符)

错误位置:340

338                         int index = result.indexOf(Keyword);
339                         line = result.substring(index + Keyword.length());
340                         index = line.indexOf(" ");
341                         kernelVersion = line.substring(0, index);
342                     }
343                 } catch (IndexOutOfBoundsException e) {

当你检测单个字符的位置时使用String.indexOf(字符),它执行的很快。
解决方案:不要使用indexOf(字符串)。

340                         index = line.indexOf(' ');

1.2.4 [兼容]系统API兼容性隐患

public static String getSupportMap(Context context, String seInfo) {
    StringBuffer support = new StringBuffer("000");
    if (!"000".equals(seInfo)) {
        support.setCharAt(2, '1');
    }

    if (VERSION.SDK_INT < 10) {
        return support.toString();
    }

    NfcManager manager = (NfcManager) context
            .getSystemService(Context.NFC_SERVICE);
    NfcAdapter adapter = manager.getDefaultAdapter();
    if (null == adapter) {
        return support.toString();
    } else {
        if (adapter.isEnabled()) {
            support.setCharAt(0, '1');
        } else {
            support.setCharAt(0, '2');
        }

        if (VERSION.SDK_INT >= 19) {
            PackageManager pm = context.getPackageManager();
            boolean hasNfcHce = pm
                    .hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
            if (hasNfcHce) {
                support.setCharAt(1, '1');
            }
        }
    }

    return support.toString();
}

getDefaultAdapter方法不支持:10(android2.3.3) 以下的版本。
解决方案:加入对版本的系统版本的判别

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) {
       // 包含新API的代码块
else
{
// 包含旧的API的代码块
}

1.2.5 [越界]数组下标越界隐患

private void doSelectItem(int pos) {
    mButtonViews[mSelectedButtonIndex].line.setVisibility(View.GONE);
    mButtonViews[mSelectedButtonIndex].buttonText.setTextColor(Color.BLACK);
    mButtonViews[mSelectedButtonIndex].contentView.setVisibility(View.GONE);
    mButtonViews[pos].line.setVisibility(View.VISIBLE);
    mButtonViews[pos].buttonText
            .setTextColor(KDimens.K_COLOR_PROM_INDICATOR);
    mButtonViews[pos].contentView.setVisibility(View.VISIBLE);
    mSelectedButtonIndex = pos;
}

采用下标的方式获取数组元素时,如果下标越界,将产生java.lang.ArrayIndexOutOfBoundsException的异常,导致app出现Crash。
解决方案:在使用下标的方式获取数组元素时,需判断下标的有效性。

1.2.6 [异常] 使用除法或求余没有判断分母长度隐患

public static Drawable zoomDrawable(Context context, Drawable in, int scaledW, int scaledH) {
  Drawable zoomed = null;
  if (in instanceof BitmapDrawable) {
      Bitmap bm = ((BitmapDrawable) in).getBitmap();
      if (scaledH != -1 && scaledW == -1) {
          scaledW = (int) ((float) (bm.getWidth() / bm.getHeight()) * scaledH);
      } else if (scaledH == -1 && scaledW != -1) {
          scaledH = (int) ((float) (bm.getHeight() / bm.getWidth()) * scaledW);
      }

      Bitmap sbm = Bitmap.createScaledBitmap(bm, scaledW, scaledW, true);
      zoomed = new BitmapDrawable(context.getResources(), sbm);
  }
  return zoomed;
}

使用除法或者求余运算时,如果分母是通过调用函数返回的int,未对返回值进行判断,当返回值为0时,会出现java.lang.ArithmeticException: / by zero异常。
解决方案:调用函数前,对函数的返回值的长度进行判断。

1.2.6 [SQL]注入风险

描述:对Content Provider进行增删改查操作时,程序没有对用户的输入进行过滤,未采用参数化查询的方式,可能导致sql注入攻击。

代码示例:

private SQLiteDatabase db;
db.rawQuery("select * from person", null);//触发规则

推荐写法:

  1. 服务端充分校验参数
  2. 使用参数化查询,比如SQLiteStatement
  3. 避免使用rawQuery()方法
  4. 对用户输入进行过滤
SQLiteStatement sqLiteStatement = db.compileStatement("insert into msgTable(uid, msg) values(?, ?)");
sqLiteStatement.bindLong(1, 12);
sqLiteStatement.bindString(3, "text");
long newRowId = sqLiteStatement.executeInsert();

1.2.7 [应用安全] AndroidMannifest.xml文件中allowBackup设置为true时会导致数据泄露

描述:建议将AndroidMannifest.xml文件android:allowBackup属性设置为false。当allowBackup标志值为true时,攻击者可通过adb backup和adb restore来备份和恢复应用程序数据。

推荐写法:

描述:建议将AndroidMannifest.xml文件android:allowBackup属性设置为false。当allowBackup标志值为true时,攻击者可通过adb backup和adb restore来备份和恢复应用程序数据。

推荐写法:

  1. minSdkVersion不低于9。
  2. android:allowBackup属性显示设置为false。

1.3 Android 客户端扫描流程

分为3个阶段:

Android客户端扫描流程图

1.3.1 基础内容扫描

使用 Android Lint 对项目进行扫描,该工具已将扫描到的问题进行了分组,同时定义了问题的严重级别: errorwarning。在 Android StudioInspection Results 视图窗中,点击问题标题即可在右边的详情视图中查看该问题的具体解释等内容,针对部分内容还有直接进行自动修复的按钮,如图:

Android Lint

关注点

由于检查的内容繁多,我们重点关注以下几个问题组的相关内容:

  • Android 开头的组,例如

    • Android > Lint > Correctness (可能影响程序正确性)
    • Android > Lint > Performance (可能影响程序性能)
    • Android > Lint > Security (可能影响程序安全性)
    • 等等
  • Class structure 组:指出类的设计上可能存在的问题

  • Code style issues 组:有助于提供代码书写规范

  • Probable bugs 组:有助于发现隐藏的问题

检查通过标准

上述的列出的问题组别不出现或者出现但只包含warning类型的问题

1.3.2 可能引起Crash的问题扫描

使用360火线Godeyes对项目进行扫描,下面将分别说明二者扫描时的关注点和检查通过标准:

360火线

360火线共有61个检查项,按级别分为Block风险建议优化,检查报告以html文件输出,
按规则分类查看的Tab中,可以查看具体问题位置及示例代码。如图

360 fireline
360 fireline
关注点

重点关注Block风险两类标记的问题

检查通过标准

扫描结果中不出现Block风险两类问题

Godeyes

Godeyes共检查23个错误,扫描结果以html文档的方式输出,文档中包含了检查问题的描述示例以及推荐方案,方便理解。值得注意的是在扫描结果的显示上,报告只会给出问题所在的行号。如图

godeyes
关注点

所有列表检查出的问题。

检查通过标准

各扫描项扫描结果为0

1.3.3 空指针和资源泄露扫描

使用Infer工具对可能的空指针可能的资源泄露进行扫描,Infer工具会在项目的根文件夹下生成infer-out的文件夹,重点关注bugs.txt这个文件,文件中会详细指出可能存在的问题的代码片段及相应的解释,示例如下:

Found 70 issues

219: error: RESOURCE_LEAK
   resource of type `java.io.DataInputStream` acquired to `dis` by call to `new()` at line 159 is not released after line 219
**Note**: potential exception at line 164
  217.                  dis.close();
  218.                  is.close();
  219. >            } catch (IOException e) {
  220.                  e.printStackTrace();
  221.                  dr = null;

error: NULL_DEREFERENCE
  object returned by `getItemByName("instalment")` could be null and is dereferenced at line 402
  400.           } else {
  401.               ((UPDropDownWidget) getItemByName(Rules.TYPE_INSTALMENT))
  402. >                     .setmCanShow(true);
  403.               ((UPDropDownWidget) getItemByName(Rules.TYPE_INSTALMENT))
  404.                       .onCheckBoxStatusChanged(true);
关注点

所有列表检查出的问题。

检查通过标准

对检查的问题尽量修复或者编写保护语句避免抛出异常。

2. 工具使用

以下内容均以 Android Studio 为默认的开发环境

2.1 Android Lint

该工具已经默认集成 Android Studio

使用方法:

Android Stuido -> 菜单栏 -> Analyze -> Inspect Code -> 根据需要选择相应的扫描范围 -> OK -> 启动扫描

如图:

Android Lint

Android Lint

Android Lint

参考资料

2.2 360 火线

使用方法

详情参考 官方使用方法
火线插件目前可以在Android Studio中进行在线搜索安装。

  • jar包版本使用

    java -jar D:\test\fireline.jar -s=D:\test\TestCase -r=E:\RedlineReport
    // 参数解释:
    //【必填项】-s或scanSrcDir为被扫描的项目工程路径
    //【必填项】-r或reportSaveDir为火线报告输出路径
    
360 fireline
  • Android stuido版本
    1. Android Studio -> 菜单栏 -> File -> Settings... -> Plugins
    2. 搜索框 -> 搜索fireline -> install -> 重启
    3. 使用 -> Project视图 -> 鼠标右键 -> fireline -> run 生成报告
360 fireline
360 fireline

参考资料

2.3 Godeyes

使用方法

详情参考 官方使用方法

  1. 下载 Android Studio版本插件 下载地址

  2. Android Studio-> 菜单栏 -> File -> Settings -> Plugins

  3. 选择Install plugin from disk -> 选择已下载的Godeyes_Android_Vx.x_(for_AndroidStudio).zip -> OK -> 安装完成 -> 重启

  4. Project视图 -> 鼠标右键 -> Run Godeyes -> 生成报告

godeyes
godeyes
参考资料

2.4 Infer

注意:

  1. 仅支持 Mac 和 Linux 环境
  2. 需要Python 且 Python >= 2.7

使用方法

cd {项目的根目录}
./gradlew clean
infer -- ./gradlew build

一段时间后会在项目的根目录下生成infer-out这个文件夹,里面的bugs.txt文档里记录的就是扫描出的问题。

参考资料

3. 疑问解答

3.1 Android Studio 按照教程安装完Godeyes后扫描项目发现没有得到的报告没有任何错误,说明项目没有任何问题么?

不一定。Godeyes插件使用前,需要设置输出报告类型,若都两种类型都没选择,则生成的html报告中就会是0错误。如果这里也设置了但是html报告中还是0个错误则说明你的代码没有问题。

godeyes bug

3.2 Android Lint 工具检查的项目太多了,只关注error就可以了吧?

并不是,Android Lint 检查项目繁琐是由于他自身也集成了一些检查工具。例如FindBugs,单独用Findbugs检查的内容基本都涵盖在了 Android Lint检查的 Probable bugs 分组中,但Android Lint 只将这些错误视为warning,而在FindBugs则可能是Scary(严重问题)的,所以除了errorwarning也是必须关注的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容