parent
0dabc96154
commit
89eb2d44af
@ -0,0 +1,168 @@ |
|||||||
|
package com.keyware.sonar.java.rules.checkers; |
||||||
|
|
||||||
|
import org.sonar.check.Rule; |
||||||
|
import org.sonar.java.ast.visitors.SubscriptionVisitor; |
||||||
|
import org.sonar.plugins.java.api.JavaFileScannerContext; |
||||||
|
import org.sonar.plugins.java.api.tree.*; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* 在重定向前对输入数据进行验证,确保仅重定向至允许的URL。或者在重定向至未知站点时向用户发出明确警告。 |
||||||
|
* |
||||||
|
* @author GuoXin |
||||||
|
* @date 2024/1/9 |
||||||
|
*/ |
||||||
|
@Rule(key = "RedirectUrlChecker") |
||||||
|
public class RedirectUrlChecker extends SubscriptionVisitor { |
||||||
|
@Override |
||||||
|
public List<Tree.Kind> nodesToVisit() { |
||||||
|
var nodeType = new Tree.Kind[]{Tree.Kind.METHOD}; |
||||||
|
return Arrays.asList(nodeType); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void visitNode(Tree tree) { |
||||||
|
MethodTree methodTree = (MethodTree) tree; |
||||||
|
BlockTree block = methodTree.block(); |
||||||
|
// 方法的参数列表
|
||||||
|
List<? extends VariableTree> parameters = methodTree.parameters(); |
||||||
|
if (block != null && !parameters.isEmpty() && isHttpRequestHandlerMethod(methodTree)) { |
||||||
|
// 判断方法的返回节点的类型为RedirectView 或 String 类型
|
||||||
|
if ("RedirectView".equals(methodTree.returnType().toString())) { |
||||||
|
// 传递上下文,和方法的参数列表
|
||||||
|
new RedirectViewCheckVisitor(this, parameters).check(block); |
||||||
|
} else if ("String".equals(methodTree.returnType().toString())) { |
||||||
|
checkByStringType(block, parameters); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void checkByStringType(BlockTree block, List<? extends VariableTree> methodParameters) { |
||||||
|
for (StatementTree statementTree : block.body()){ |
||||||
|
if(statementTree.kind() == Tree.Kind.RETURN_STATEMENT){ |
||||||
|
ReturnStatementTree rs = (ReturnStatementTree) statementTree; |
||||||
|
ExpressionTree exprTree = rs.expression(); |
||||||
|
if(exprTree != null && !exprTree.is(Tree.Kind.STRING_LITERAL)){ |
||||||
|
if(exprTree instanceof BinaryExpressionTree){ |
||||||
|
BinaryExpressionTree bExprTree = (BinaryExpressionTree) exprTree; |
||||||
|
if(bExprTree.is(Tree.Kind.PLUS) && bExprTree.leftOperand().is(Tree.Kind.STRING_LITERAL) && bExprTree.rightOperand().is(Tree.Kind.IDENTIFIER)){ |
||||||
|
var identifierTree = (IdentifierTree) bExprTree.rightOperand(); |
||||||
|
String argName = identifierTree.name(); |
||||||
|
if (methodParameters.stream().anyMatch(parameter -> parameter.simpleName().name().equals(argName))) { |
||||||
|
// 说明该变量是方法传递进来的,抛出问题
|
||||||
|
context.reportIssue(this, identifierTree, "在重定向前对输入数据进行验证"); |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (exprTree.is(Tree.Kind.IDENTIFIER)) { |
||||||
|
var identifierTree = (IdentifierTree) exprTree; |
||||||
|
String argName = identifierTree.name(); |
||||||
|
if (methodParameters.stream().anyMatch(parameter -> parameter.simpleName().name().equals(argName))) { |
||||||
|
// 说明该变量是方法传递进来的,抛出问题
|
||||||
|
context.reportIssue(this, identifierTree, "在重定向前对输入数据进行验证"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 判断方法是否为public方法 |
||||||
|
* |
||||||
|
* @param methodTree 方法树 |
||||||
|
* @return true: 是; false: 否 |
||||||
|
*/ |
||||||
|
private boolean isHttpRequestHandlerMethod(MethodTree methodTree) { |
||||||
|
var isPublic = false; |
||||||
|
var hasMappingAnnotation = false; |
||||||
|
for (ModifierTree modifier : methodTree.modifiers()) { |
||||||
|
// 判断是否为公共方法
|
||||||
|
if (!isPublic && modifier instanceof ModifierKeywordTree) { |
||||||
|
if (((ModifierKeywordTree) modifier).modifier() == Modifier.PUBLIC) { |
||||||
|
isPublic = true; |
||||||
|
} |
||||||
|
} |
||||||
|
// 判断是否包含Mapping注解
|
||||||
|
if (!hasMappingAnnotation && modifier instanceof AnnotationTree) { |
||||||
|
AnnotationTree annotationTree = (AnnotationTree) modifier; |
||||||
|
if (annotationTree.annotationType() instanceof IdentifierTree) { |
||||||
|
IdentifierTree identifierTree = (IdentifierTree) annotationTree.annotationType(); |
||||||
|
String name = identifierTree.name(); |
||||||
|
if (name.endsWith("Mapping")) { |
||||||
|
hasMappingAnnotation = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (isPublic && hasMappingAnnotation) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
static class RedirectViewCheckVisitor extends SubscriptionVisitor { |
||||||
|
private final RedirectUrlChecker checker; |
||||||
|
// 方法的参数列表
|
||||||
|
private final List<? extends VariableTree> methodParameters; |
||||||
|
|
||||||
|
public RedirectViewCheckVisitor(RedirectUrlChecker checker, List<? extends VariableTree> parameters) { |
||||||
|
this.checker = checker; |
||||||
|
this.methodParameters = parameters; |
||||||
|
} |
||||||
|
|
||||||
|
public void check(Tree block) { |
||||||
|
this.scanTree(block); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Tree.Kind> nodesToVisit() { |
||||||
|
// 订阅new class和 函数调用的节点
|
||||||
|
var nodeType = new Tree.Kind[]{Tree.Kind.NEW_CLASS, Tree.Kind.METHOD_INVOCATION}; |
||||||
|
return Arrays.asList(nodeType); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void visitNode(Tree tree) { |
||||||
|
if (tree.is(Tree.Kind.NEW_CLASS)) { |
||||||
|
NewClassTree classTree = (NewClassTree) tree; |
||||||
|
// 判断是否为RedirectView,如果是,则判断是否有参数,如果有参数,则判断参数的类型是否由方法传递进来的
|
||||||
|
String name = classTree.identifier().toString(); |
||||||
|
if ("RedirectView".equals(name)) { |
||||||
|
if (classTree.arguments().size() > 0) { |
||||||
|
// 获取第一个参数语法树节点
|
||||||
|
ExpressionTree argNode = classTree.arguments().get(0); |
||||||
|
checkArgs(argNode, tree); |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
MethodInvocationTree invocationTree = (MethodInvocationTree) tree; |
||||||
|
ExpressionTree expressionTree = invocationTree.methodSelect(); |
||||||
|
if (expressionTree instanceof MemberSelectExpressionTree) { |
||||||
|
MemberSelectExpressionTree member = (MemberSelectExpressionTree) expressionTree; |
||||||
|
if (member.expression().symbolType().is("RedirectView") |
||||||
|
&& "setUrl".equals(member.identifier().name())) { |
||||||
|
ExpressionTree argNode = invocationTree.arguments().get(0); |
||||||
|
checkArgs(argNode, tree); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void checkArgs(ExpressionTree argNode, Tree tree) { |
||||||
|
// 判断该语法树节点是否为IdentifierTree,如果是,则说明语法树节点为变量,然后判断该变量是否是包含在方法的参数列表中
|
||||||
|
if (argNode instanceof IdentifierTree) { |
||||||
|
IdentifierTree identifierTree = (IdentifierTree) argNode; |
||||||
|
String argName = identifierTree.name(); |
||||||
|
if (methodParameters.stream().anyMatch(parameter -> parameter.simpleName().name().equals(argName))) { |
||||||
|
// 说明该变量是方法传递进来的,抛出问题
|
||||||
|
checker.context.reportIssue(checker, tree, "在重定向前对输入数据进行验证"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
<h2>在重定向前对输入数据进行验证</h2> |
||||||
|
<p>在重定向前对输入数据进行验证,确保仅重定向至允许的URL。或者在重定向至未知站点时向用户发出明确警告。</p> |
||||||
|
<pre> |
||||||
|
|
||||||
|
</pre> |
||||||
|
<h2>合规解决方案</h2> |
||||||
|
<pre> |
||||||
|
|
||||||
|
</pre> |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"title": "在重定向前对输入数据进行验证", |
||||||
|
"type": "CODE_SMELL", |
||||||
|
"status": "ready", |
||||||
|
"remediation": { |
||||||
|
"func": "Constant\/Issue", |
||||||
|
"constantCost": "5min" |
||||||
|
}, |
||||||
|
"tags": [ |
||||||
|
"28suo" |
||||||
|
], |
||||||
|
"defaultSeverity": "Minor" |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
|
||||||
|
@Controller |
||||||
|
public class MyController { |
||||||
|
|
||||||
|
@GetMapping("/old-url") |
||||||
|
public RedirectView redirectOldUrl(String url) { // Compliant,因为重定向的路径不是由方法传递进来的
|
||||||
|
RedirectView redirectView = new RedirectView(); |
||||||
|
redirectView.setUrl(url);// Noncompliant {{在重定向前对输入数据进行验证}}
|
||||||
|
return redirectView; |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping("/old-url2") |
||||||
|
public String redirectOldUrl2() { // Compliant,因为重定向的路径不是由方法传递进来的
|
||||||
|
// 302临时重定向到新的URL
|
||||||
|
return "redirect:/new-url"; |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping("/old-url3") |
||||||
|
public RedirectView redirectOldUrl3(String url) { |
||||||
|
RedirectView redirectView = new RedirectView(url); // Noncompliant {{在重定向前对输入数据进行验证}}
|
||||||
|
redirectView.setUrl(url); // Noncompliant {{在重定向前对输入数据进行验证}}
|
||||||
|
return redirectView; |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping("/old-url4") |
||||||
|
public String redirectOldUrl4(String url) { |
||||||
|
// 302临时重定向到新的URL
|
||||||
|
return "redirect:" + url; // Noncompliant {{在重定向前对输入数据进行验证}}
|
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping("/old-url5") |
||||||
|
public String redirectOldUrl4(String url) { |
||||||
|
// 302临时重定向到新的URL
|
||||||
|
return url; // Noncompliant {{在重定向前对输入数据进行验证}}
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package com.keyware.sonar.java.rules.checkers; |
||||||
|
|
||||||
|
import com.keyware.sonar.java.utils.FilesUtils; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.sonar.java.checks.verifier.CheckVerifier; |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO RedirectUrlCheckerTest |
||||||
|
* |
||||||
|
* @author GuoXin |
||||||
|
* @date 2024/1/9 |
||||||
|
*/ |
||||||
|
public class RedirectUrlCheckerTest { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void test(){ |
||||||
|
CheckVerifier.newVerifier() |
||||||
|
.onFile("src/test/files/RedirectUrlChecker.java") |
||||||
|
.withCheck(new RedirectUrlChecker()) |
||||||
|
.withClassPath(FilesUtils.getClassPath("target/test-jars")) |
||||||
|
.verifyIssues(); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue