想写这篇文章好久了,重度拖延症患者就是拖拖拖...
然后本文主要是给大家介绍一下怎么实现一个IDEA静态代码检测插件,现在都在讲安全左移嘛,我觉得静态代码检测插件就是一个安全左移很好的落地,于是就想着学习一下
我自己在写这个插件的起步阶段其实遇到了很多问题,这些问题肯定也会是每一个想要写插件的人都会遇到的,所以,我就简单记录下我的开发过程供来者参考
开发环境搭建
首先得确保你的Plugin DevKit插件成功安装并启用了,一般情况下idea都自带了这个插件,你只需要手动启用一下就行,启用过后记得重启idea
然后创建工程,我这里使用gradle创建一个IntelliJ platform plugin的工程,如果你没有启用devkit插件的话,这里应该是找不到intellij platform plugin这个选项的
通过gradle创建的工程,build.gradle
文件中会有这么一项
gradle会根据这里的version字段去下载对应版本的intellij依赖包,这一阶段需要花费大量的时间(还有可能一直下载不下来,可以尝试挂代理),可能是因为源在国外吧
gradle项目构建好过后,项目结构大概如下
这里有个比较关键的文件plugin.xml
,这是插件的配置文件,很重要,里面有如下常见字段
<idea-plugin>
<!-- 插件相关信息, 会展示在IDEA插件的描述中 -->
<!-- 插件唯一id, 遵循使用包名的原则 -->
<id>com.test.sast</id>
<!-- 插件名称 -->
<name>AxinSAST</name>
<!-- 插件版本 -->
<version>1.0</version>
<!-- 开发者信息 -->
<vendor email="wuhu@gmail.com" url="http://wuhu.top">$Company|$Name</vendor>
<!-- 插件的描述 -->
<description>my plugin description</description>
<!-- 插件版本变更信息 -->
<change-notes>Initial release of the plugin.</change-notes>
<!-- 如果该插件还依赖了其他插件,则配置对对应的插件id -->
<depends>com.intellij.modules.all</depends>
<!-- 插件兼容IDEA的最大和最小build号,不配置则不做限制 -->
<idea-version since-build="94.539" until-build="192"/>
<!-- Actions: 如添加一个文件右击菜单按钮 -->
<actions>
<action id="FinderAction" class="com.test.finder.FinderAction" text="FileFinder" description="FileFinder">
<add-to-group group-id="ProjectViewPopupMenu" anchor="first"/>
</action>
</actions>
<!-- 插件定义的扩展点,以供其他插件扩展该插件,类似Java的抽象类的功能 -->
<extensionPoints>
...
</extensionPoints>
<!-- 声明该插件对IDEA core或其他插件的扩展 -->
<extensions xmlns="com.intellij">
...
</extensions>
</idea-plugin>
通常来说,按照上面的操作应该就可以成功建立一个插件工程了,比较容易卡住的就是下载idea依赖那一块儿
当我们写好代码过后,可以使用gradle的runIde这个task测试自己的插件,这个task首次执行时会去下载一个jbr(jetbrains runtime)压缩包,然后利用这个runtime来启动我们的插件
runIde这个task会在一个沙箱环境里运行我们编写的插件,不会影响我们当前正在使用的idea环境,也就是会新起一个测试用的idea,然后我们的插件会在这个测试idea上跑起来
为了能够对idea插件有一个更加清晰的认识,我们来写一个小demo,这个demo也是网上最常见的一个demo
插件开发初体验
我们使用devkit 新建一个action
然后填写好对应的信息:
上述操作完成后,会在我们的项目目录下生成一个对应TestAction类文件,我们重写下该类的ationPerformed方法:
代码语言:javascript复制import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.ui.MessageType;
public class TestAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
// TODO: insert action logic here
// 这里的testid需要和你刚刚填写的action id保持一致
NotificationGroup notificationGroup = new NotificationGroup("testid", NotificationDisplayType.BALLOON, false);
/**
* content : 通知内容
* type :通知的类型,warning,info,error
*/
Notification notification = notificationGroup.createNotification("测试通知", MessageType.INFO);
Notifications.Bus.notify(notification);
}
}
完成了上面的操作,我们可以看下plugin.xml文件有什么变化
你会发现这里多了个action,这个action其实是devkit帮我们自动注册的,所以,到这里你也应该能够理解devkit有啥用了吧,它帮我们封装了一些方法,让我们更加便利的进行插件开发
如果我们没有使用devkit来创建刚刚那么一个action的话,我们就需要手动在plugin.xml文件里注册action,然后在对应的类实现对应的方法
ok,到此其实我们的一个小demo就算是完成了,到目前为止你都不需要去理解上面的代码有啥用,你只需要按照我的步骤走一遍就行,至于上面的代码能实现什么效果,有什么是比亲眼见证更好的呢?
接下来就是验证插件的效果了,直接执行intellij的runIde任务就好
运行这个会新起一个idea,然后我们的插件会自动安装到这个idea上,我们这个插件会在tools菜单下注册一个item,然后点击该item会在idea右下角弹出消息「测试通知」
然后我们到idea的plugins里也可以看到,我们的插件的确是成功安装并启用了的
上图中的就是我们的测试插件,红框中的展示文案都是可以在plugin.xml文件中进行配置的
插件的编写说白了还是调用各种api,想要写好一个插件,就需要清楚intellij sdk提供的各种方法以及接口的使用
而我们要编写一个静态代码审计插件的难点也就在「到底该使用哪个接口来进行代码检查」以及「SDK都提供了哪些工具或方法来方便我们完成代码审计」,只要克服了这两个问题,从AST里去找代码中可能存在的问题也就变得千篇一律了
在idea中进行静态代码审计
首先解决第一个问题,用SDK的哪个接口来进行代码审计
虽然intellij的文档写的很烂,但是还好他在github上放了一些代码示例,其中inspection_basics模块以及comparing_references_inspection模块(尤其是这个模块)给了我们提示,通过这两个模块我知道了在intellij platform上进行自动化代码检查需要用到的功能被称作inspection,那么在这个基础上我们就可以进一步查询inspection的使用文档了
既然写到这里了,那为了让屏幕前的大家更好地了解inspection的使用,我干脆来简单的给大家讲解下comparing_references_inspection这个模块里的一些知识点吧,这个简单的demo主要的功能就是检测java代码中有没有错误地进行引用类型的比较,比如对于String类型,错误的使用!=
或者==
而不是.equals()
方法
如果发现有错误的用法插件会将对应的代码高亮,并且提供一键修复功能,除此之外这个demo里还给我们展示了怎么编写测试用例
这简直就是我们的SAST插件想要做的事情——发现漏洞对漏洞代码进行高亮,如果可以,提供一键修复功能
当然,除此之外,我们还希望发现漏洞代码及时上报到soc方便后续人工确认
首先,我们要对java做inspection,需要在build.gradle中的intellij配置好java的plugin
这个配好过后,我们就可以实现一个自己的inspection类啦,这个类需要继承AbstractBaseJavaLocalInspectionTool,然后我们自己实现的inspection类大体架构如下:
代码语言:javascript复制public class ComparingReferencesInspection extends AbstractBaseJavaLocalInspectionTool{
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly){
return new JavaElementVisitor() {
public void visitBinaryExpression(PsiBinaryExpression expression) {
doSomething...
if(condition){
holder.registerProblem(expression, "问题描述字符串")
}
}
public void visitXXXXX(){
xxxxx
}
}
}
}
其中最重要的方法就是visitXXX方法,上面代码中举了visitBinaryExpression
这么个方法,这个方法有什么用呢?
当插件跑起来过后,这个方法就会检查代码中的BinaryExpression,你可能会问BinaryExpression是啥?
intellij platform中的BinaryExpression其实代表的就是代码中的二项式,例如:
代码语言:javascript复制"select * from table where id=" id
1 2
1-2
idea中用BinaryExpression封装了上面这种表达式,代码中的每一个二项式都会作为参数传入visitBinaryExpression
方法,然后你可以在这个方法里处理这些二项式,比如判断这个二项式是不是潜在的sql注入语句(例如上面第一条二项式那样)
除了visitBinaryExpression方法,intellij还提供了很多种visit方法,这些方法都是为了方便我们进行代码inspection的,例如visitMethodCallExpression
是可以访问到所有的方法调用语句,visitNewExpression
可以访问到所有的类似new Xxx()
语句
有了上面的基础知识,我们尝试着想一想怎么实现一个简单的检测sql注入的插件,我们不考虑特别复杂的sql注入,我们就考虑怎么检测sql语句拼接就行,例如下面这些句子
代码语言:javascript复制public test(String id){
String id2 = '2';
String sql1 = "select * from table where id=" id;
String sql2 = "select * from table where id=" id2;
}
上面两个sql语句都是二项表达式,所以我们可以用visitBinaryExpression拿到他们,拿到他们过后,我们怎么确定他是一个sql语句呢?毕竟这两个二项表达式是被封装到binaryExpression这个对象里的,怎么通过这个对象判断他是一个sql语句这是一个需要我们解决的问题
这里我们借用一个插件「PsiViewer」,这个插件可以查看当前源码文件的AST树,看图
上图左侧是我们的源代码,右侧是psiviewer插件窗口,当我们在把光标停留在源码某处时,psiviewer窗口会对应展示我们正处在AST树的哪个位置,反过来,当我们在psiviewer窗口中选中ast树某处时,对应的源码也会高亮出来
这个插件可以让我们对intellij platform解析出来的AST树有更加清晰的认知
现在,让我们回到最初的问题,在拿到了BinaryExpression后,我们要怎么判断它到底是不是一个sql语句呢?
其实
如果你观察仔细,你可以从上图中发现psiviewer窗口的下半部分展示了当前PsiBinaryExpression对象所有的属性以及其值
可以看到前两个属性分别是LOperand(左操作数)以及ROperand(右操作数),除此之外还有个属性是operationTokenType
,上面截图没有截到,这个属性指明了当前二项式的操作符是啥
常见的操作符有
、-
等
所以,到目前为止怎么判断一个二项式是不是一个sql语句应该很明显了吧:
先判断当前二项式的操作符是不是加号,如果是加号,拿到左操作数以及右操作数,解析出他们的值,然后把他们的值拼接起来,最后用正则判断是不是一个sql语句
当然,这只是很理想的一个状况,如果你要检测sql注入,还需要考虑很多种情况,举一部分?:
代码语言:javascript复制public test(String id){
String id2 = '2';
String sql1 = "select * from table where id="
String sql2 = sql1 id;
String sql1 = id;
String sql3 = "select * from table where id=" id2;
String sql4 = "select * from" "table" id2;
String sql5 = "select * from table where id=" getId();
String sql6 = "select * from table where id=%s";
sql6.format(id);
}
ok,写到这里好像就已经差不多了,要是再细节一点,就是把代码直接贴出来,然后一句句注释了
然后下面是我写的demo插件运行的效果:
我自己在写了一些通过AST进行代码审计的demo后,发现AST能做的事情是很有限的,QL才是更正确的自动化代码审计姿势
代码已上传github:https://github.com/Maskhe/AxinSAST,点击阅读原文可跳转
还在开发中,代码很烂,希望大家别喷我,ball ball 大家?
我又来要赞?,如果你觉得本文写的不错就点个赞,有帮助就点个在看,如果你分享到朋友圈了,什么都别说了,好兄弟!你懂我!各位股东们的一键三连是我持续真诚写作的最大动力✌️