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