背景
當(dāng)今的大部分公司都有對(duì)自己的業(yè)務(wù)代碼進(jìn)行安全性審計(jì)的需求删铃,來(lái)保證業(yè)務(wù)代碼的安全性概荷,同時(shí)代碼審計(jì)作為SDL中重要的一環(huán)螺戳,可有效保證業(yè)務(wù)的CIA搁宾。但是人工審計(jì)存在嚴(yán)重的性能瓶頸,單純的使用代碼掃描器效果也不盡如意倔幼,誤報(bào)問(wèn)題較多凉倚。目前較好的方法:結(jié)合業(yè)務(wù)缓窜,自定義規(guī)則咧最,結(jié)合兩者優(yōu)勢(shì)慈鸠。但是網(wǎng)上關(guān)于這方面的介紹較少,希望本文章能幫助到有需求的同學(xué)膏燃。選擇的掃描為SonarQube茂卦,這款掃描器是開源掃描器中較為出色的一款,有豐富的圖像化界面和強(qiáng)大的語(yǔ)法解析能力组哩。
準(zhǔn)備工作
- 下載并運(yùn)行SonarQube等龙,具體步驟請(qǐng)參考官網(wǎng)教程。
- 下載sonar-java插件源代碼伶贰,這也是Java掃描規(guī)則集蛛砰,我們會(huì)基于這個(gè)規(guī)則集編寫我們自己的規(guī)則,下載地址:
https://github.com/SonarSource/sonar-java
sonar-java插件關(guān)鍵結(jié)構(gòu)
java-checks模塊:該模塊包含最重要的JAVA掃描規(guī)則集
java-frontend模塊:該模塊提供JAVA語(yǔ)法解析類黍衙,是該插件的基礎(chǔ)
一條規(guī)則的必要構(gòu)成
- java-check中添加一條規(guī)則
- java-check test模塊中添加測(cè)試用例
- java-check resource模塊中添加規(guī)則描述泥畅,包括一個(gè)html和一個(gè)json文件
- 在org.sonar.java.checks.CheckList中注冊(cè)規(guī)則
示例解析
我們先使用java-check中的一條掃描規(guī)則作為示例,先了解下如何編寫和注冊(cè)規(guī)則琅翻,規(guī)則路徑如下:
org.sonar.java.checks.spring.RequestMappingMethodPublicCheck
先看規(guī)則本體:
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);
}
}
該規(guī)則的核心是visitNode涯捻,通過(guò)重寫該方法,來(lái)遍歷被掃描Java文件的語(yǔ)法樹望迎,該方法會(huì)在掃描任務(wù)運(yùn)行時(shí)自動(dòng)被調(diào)用。
該規(guī)則的運(yùn)行流程:
- 掃描器加載插件進(jìn)行掃描凌外,解析被掃描文件的語(yǔ)法樹
- visitNode被調(diào)用辩尊,并將解析好的語(yǔ)法樹傳入
- 運(yùn)行自定義方法isClassController和isRequestMappingAnnotated
- 使用reportIssue上報(bào)問(wèn)題
但是只有一個(gè)規(guī)則文件是無(wú)法被掃描器正常加載的,還需要一個(gè)規(guī)則定義文件和一個(gè)規(guī)則詳情描述文件:
規(guī)則定義文件 S3751_java.json
S3751為該掃描規(guī)則的Key康辑,在規(guī)則文件里由@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:為該規(guī)則的類型摄欲,包括VULNERABILITY轿亮、BUG SECURITY_HOTSPOT、CODE_SMELL等胸墙。
constantCost:代表要解決該問(wèn)題大概需要花費(fèi)多長(zhǎng)時(shí)間我注。
scope:定義要被掃描項(xiàng)目的范文,包括All迟隅、Main但骨、Test。
規(guī)則詳情描述文件 S3751_java.html
規(guī)則詳細(xì)描述智袭,會(huì)在SonarQube Web端規(guī)則詳情頁(yè)面里展示出來(lái)
<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>
這兩個(gè)文件都位于
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/目錄下奔缠,
我們自定義的規(guī)則json和html文件也要放在該目錄下,文件名為KEY_java.json吼野、KEY_java.html校哎。KEY在規(guī)則中使用@Rule注解定義。
測(cè)試文件
編寫一條好的規(guī)則往往需要很多次測(cè)試瞳步,通過(guò)TDD(測(cè)試驅(qū)動(dòng)開發(fā))的方式來(lái)幫助我們寫出一條好的規(guī)則闷哆。
該示例規(guī)則的測(cè)試文件:
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());
}
}
通過(guò)JavaCheckVerifier類中提供的方法,來(lái)啟動(dòng)我們的規(guī)則掃描文件单起。
注冊(cè)規(guī)則
在org.sonar.java.checks.CheckList中進(jìn)行注冊(cè)抱怔,使用add方法添加需要注冊(cè)的規(guī)則
自定義示例分享
Struts2 S2-057檢查規(guī)則
說(shuō)明:掃描項(xiàng)目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;
}
}
規(guī)則詳細(xì)說(shuō)明:
先看幾個(gè)關(guān)鍵點(diǎn):
-
@Rule(key = "Struts2_S2_057Check")
該注解聲明本條規(guī)則的Key -
implements PomCheck
public void scanFile(PomCheckContext context)
本條規(guī)則實(shí)現(xiàn)PomCheck類,重寫scanFile馏臭,這樣插件和掃描器會(huì)自動(dòng)解析Pom.xml文件野蝇,并將解析完后的pom文件語(yǔ)法樹傳遞進(jìn)來(lái) -
strutsVerCompare
用來(lái)定義哪些版本的Struts2依賴存在漏洞
之后在
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/
中新建Struts2_S2_057Check_java.json和Struts2_S2_057Check_java.html兩個(gè)文件,大家可以參考其他規(guī)則來(lái)編寫括儒。最后在CheckList中進(jìn)行注冊(cè)绕沈。
插件編譯
mvn clean package -Dlicense.skip=true
-Dlicense.skip=true 跳過(guò)簽名檢查
可能遇到的問(wèn)題
編譯時(shí)提示找不到maven2相關(guān)類,在IDE中將sonar-java/java-maven-model/target/generated-sources目錄設(shè)置為Generated Sources Root
2020年3月更新:
有同學(xué)在使用自己插件時(shí)帮寻,sonarqube會(huì)報(bào)如下錯(cuò)誤:
java.lang.IllegalStateException: Name of rule [repository=squid, key=${YourRuleName}] is empty
解決方法:
將 java-checks/src/main/resources/org/sonar/l10n/java/rules/squid 目錄下和自己規(guī)則對(duì)應(yīng)的html以及json文件更名為${YourRuleName}_java.html和${YourRuleName}_java.json