背景
当今的大部分公司都有对自己的业务代码进行安全性审计的需求,来保证业务代码的安全性,同时代码审计作为SDL中重要的一环,可有效保证业务的CIA。但是人工审计存在严重的性能瓶颈,单纯的使用代码扫描器效果也不尽如意,误报问题较多。目前较好的方法:结合业务,自定义规则,结合两者优势。但是网上关于这方面的介绍较少,希望本文章能帮助到有需求的同学。选择的扫描为SonarQube,这款扫描器是开源扫描器中较为出色的一款,有丰富的图像化界面和强大的语法解析能力。
准备工作
- 下载并运行SonarQube,具体步骤请参考官网教程。
- 下载sonar-java插件源代码,这也是Java扫描规则集,我们会基于这个规则集编写我们自己的规则,下载地址:
https://github.com/SonarSource/sonar-java
sonar-java插件关键结构
java-checks模块:该模块包含最重要的JAVA扫描规则集
java-frontend模块:该模块提供JAVA语法解析类,是该插件的基础
一条规则的必要构成
- java-check中添加一条规则
- java-check test模块中添加测试用例
- java-check resource模块中添加规则描述,包括一个html和一个json文件
- 在org.sonar.java.checks.CheckList中注册规则
示例解析
我们先使用java-check中的一条扫描规则作为示例,先了解下如何编写和注册规则,规则路径如下:
org.sonar.java.checks.spring.RequestMappingMethodPublicCheck
先看规则本体:
package org.sonar.java.checks.spring;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
@Rule(key = "S3751")
public class RequestMappingMethodPublicCheck extends IssuableSubscriptionVisitor {
@Override
public List<Tree.Kind> nodesToVisit() {
return Collections.singletonList(Tree.Kind.METHOD);
}
private static final List<String> CONTROLLER_ANNOTATIONS = Arrays.asList(
"org.springframework.stereotype.Controller",
"org.springframework.web.bind.annotation.RestController"
);
private static final List<String> REQUEST_ANNOTATIONS = Arrays.asList(
"org.springframework.web.bind.annotation.RequestMapping",
"org.springframework.web.bind.annotation.GetMapping",
"org.springframework.web.bind.annotation.PostMapping",
"org.springframework.web.bind.annotation.PutMapping",
"org.springframework.web.bind.annotation.DeleteMapping",
"org.springframework.web.bind.annotation.PatchMapping"
);
@Override
public void visitNode(Tree tree) {
if (!hasSemantic()) {
return;
}
MethodTree methodTree = (MethodTree) tree;
Symbol.MethodSymbol methodSymbol = methodTree.symbol();
if (isClassController(methodSymbol)
&& isRequestMappingAnnotated(methodSymbol)
&& !methodSymbol.isPublic()) {
reportIssue(methodTree.simpleName(), "Make this method \"public\".");
}
}
private static boolean isClassController(Symbol.MethodSymbol methodSymbol) {
return CONTROLLER_ANNOTATIONS.stream().anyMatch(methodSymbol.owner().metadata()::isAnnotatedWith);
}
private static boolean isRequestMappingAnnotated(Symbol.MethodSymbol methodSymbol) {
return REQUEST_ANNOTATIONS.stream().anyMatch(methodSymbol.metadata()::isAnnotatedWith);
}
}
该规则的核心是visitNode,通过重写该方法,来遍历被扫描Java文件的语法树,该方法会在扫描任务运行时自动被调用。
该规则的运行流程:
- 扫描器加载插件进行扫描,解析被扫描文件的语法树
- visitNode被调用,并将解析好的语法树传入
- 运行自定义方法isClassController和isRequestMappingAnnotated
- 使用reportIssue上报问题
但是只有一个规则文件是无法被扫描器正常加载的,还需要一个规则定义文件和一个规则详情描述文件:
规则定义文件 S3751_java.json
S3751为该扫描规则的Key,在规则文件里由@Rule(key = "S3751")定义
{
"title": "\"@RequestMapping\" methods should be \"public\"",
"type": "VULNERABILITY",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "2min"
},
"tags": [
"spring",
"owasp-a6"
],
"standards": [
"OWASP Top Ten"
],
"defaultSeverity": "Blocker",
"ruleSpecification": "RSPEC-3751",
"sqKey": "S3751",
"scope": "Main",
"securityStandards": {
"OWASP": [
"A6"
]
}
}
type:为该规则的类型,包括VULNERABILITY、BUG SECURITY_HOTSPOT、CODE_SMELL等。
constantCost:代表要解决该问题大概需要花费多长时间。
scope:定义要被扫描项目的范文,包括All、Main、Test。
规则详情描述文件 S3751_java.html
规则详细描述,会在SonarQube Web端规则详情页面里展示出来
<p>A method with a <code>@RequestMapping</code> annotation part of a class annotated with <code>@Controller</code> (directly or indirectly through a
meta annotation - <code>@RestController</code> from Spring Boot is a good example) will be called to handle matching web requests. That will happen
even if the method is <code>private</code>, because Spring invokes such methods via reflection, without checking visibility. </p>
<p>So marking a sensitive method <code>private</code> may seem like a good way to control how such code is called. Unfortunately, not all Spring
frameworks ignore visibility in this way. For instance, if you've tried to control web access to your sensitive, <code>private</code>,
<code>@RequestMapping</code> method by marking it <code>@Secured</code> ... it will still be called, whether or not the user is authorized to access
it. That's because AOP proxies are not applied to non-public methods.</p>
<p>In addition to <code>@RequestMapping</code>, this rule also considers the annotations introduced in Spring Framework 4.3: <code>@GetMapping</code>,
<code>@PostMapping</code>, <code>@PutMapping</code>, <code>@DeleteMapping</code>, <code>@PatchMapping</code>.</p>
<h2>Noncompliant Code Example</h2>
<pre>
@RequestMapping("/greet", method = GET)
private String greet(String greetee) { // Noncompliant
</pre>
<h2>Compliant Solution</h2>
<pre>
@RequestMapping("/greet", method = GET)
public String greet(String greetee) {
</pre>
<h2>See</h2>
<ul>
<li> OWASP Top 10 2017 Category A6 - Security Misconfiguration </li>
</ul>
这两个文件都位于
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/目录下,
我们自定义的规则json和html文件也要放在该目录下,文件名为KEY_java.json、KEY_java.html。KEY在规则中使用@Rule注解定义。
测试文件
编写一条好的规则往往需要很多次测试,通过TDD(测试驱动开发)的方式来帮助我们写出一条好的规则。
该示例规则的测试文件:
org.sonar.java.checks.spring.RequestMappingMethodPublicCheckTest
package org.sonar.java.checks.spring;
import org.junit.Test;
import org.sonar.java.checks.verifier.JavaCheckVerifier;
public class RequestMappingMethodPublicCheckTest {
@Test
public void test() {
JavaCheckVerifier.verify("src/test/files/checks/spring/RequestMappingMethodPublicCheck.java", new RequestMappingMethodPublicCheck());
JavaCheckVerifier.verifyNoIssueWithoutSemantic("src/test/files/checks/spring/RequestMappingMethodPublicCheck.java",
new RequestMappingMethodPublicCheck());
}
}
通过JavaCheckVerifier类中提供的方法,来启动我们的规则扫描文件。
注册规则
在org.sonar.java.checks.CheckList中进行注册,使用add方法添加需要注册的规则
自定义示例分享
Struts2 S2-057检查规则
说明:扫描项目pom.xml中是否使用包含S2-057漏洞版本的struts2依赖
package org.sonar.java.checks.xml.maven;
import org.sonar.java.checks.xml.maven.helpers.MavenDependencyCollector;
import org.sonar.java.xml.maven.PomCheck;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.java.xml.maven.PomCheckContext;
import org.sonar.maven.model.LocatedAttribute;
import org.sonar.maven.model.maven2.Dependency;
import javax.annotation.Nullable;
import java.util.List;
@Rule(key = "Struts2_S2_057Check")
public class Struts2_S2_057Check implements PomCheck {
@Override
public void scanFile(PomCheckContext context) {
List<Dependency> dependencies = new MavenDependencyCollector(context.getMavenProject()).allDependencies();
for (Dependency dependency : dependencies) {
LocatedAttribute artifactId = dependency.getArtifactId();
LocatedAttribute version = dependency.getVersion();
if (version != null && artifactId != null && "struts2-core".equalsIgnoreCase(artifactId.getValue()) && !strutsVerCompare(version.getValue())) {
String message = "此版本Struts2包含高危漏洞";
List<PomCheckContext.Location> secondaries = getSecondary(version);
int line = version.startLocation().line();
context.reportIssue(this, line, message, secondaries);
}
}
}
private static List<PomCheckContext.Location> getSecondary(@Nullable LocatedAttribute systemPath) {
if (systemPath != null && StringUtils.isNotBlank(systemPath.getValue())) {
return Lists.newArrayList(new PomCheckContext.Location("configure check", systemPath));
}
return ImmutableList.of();
}
private static boolean strutsVerCompare(String version){
String StrutsVersion1 = "2.3.35";
String StrutsVersion2 = "2.5.17";
String[] versionArray1 = version.split("\\.");
if(versionArray1[1].equalsIgnoreCase("3")){
if(compareVersion(StrutsVersion1, version) > 0){
return false;
}
}
if(versionArray1[1].equalsIgnoreCase("5")){
if(compareVersion(StrutsVersion2, version) > 0){
return false;
}
}
return true;
}
private static int compareVersion(String version1, String version2){
String[] versionArray1 = version1.split("\\.");
String[] versionArray2 = version2.split("\\.");
int idx = 0;
int minLength = Math.min(versionArray1.length, versionArray2.length);
int diff = 0;
while (idx < minLength
&& (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
&& (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
++idx;
}
diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
return diff;
}
}
规则详细说明:
先看几个关键点:
-
@Rule(key = "Struts2_S2_057Check")
该注解声明本条规则的Key -
implements PomCheck
public void scanFile(PomCheckContext context)
本条规则实现PomCheck类,重写scanFile,这样插件和扫描器会自动解析Pom.xml文件,并将解析完后的pom文件语法树传递进来 -
strutsVerCompare
用来定义哪些版本的Struts2依赖存在漏洞
之后在
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/
中新建Struts2_S2_057Check_java.json和Struts2_S2_057Check_java.html两个文件,大家可以参考其他规则来编写。最后在CheckList中进行注册。
插件编译
mvn clean package -Dlicense.skip=true
-Dlicense.skip=true 跳过签名检查
可能遇到的问题
编译时提示找不到maven2相关类,在IDE中将sonar-java/java-maven-model/target/generated-sources目录设置为Generated Sources Root
2020年3月更新:
有同学在使用自己插件时,sonarqube会报如下错误:
java.lang.IllegalStateException: Name of rule [repository=squid, key=${YourRuleName}] is empty
解决方法:
将 java-checks/src/main/resources/org/sonar/l10n/java/rules/squid 目录下和自己规则对应的html以及json文件更名为${YourRuleName}_java.html和${YourRuleName}_java.json