diff --git a/sonar-keyware-plugins-java/src/main/java/com/keyware/sonar/java/rules/checkers/RedirectUrlChecker.java b/sonar-keyware-plugins-java/src/main/java/com/keyware/sonar/java/rules/checkers/RedirectUrlChecker.java new file mode 100644 index 0000000..9069403 --- /dev/null +++ b/sonar-keyware-plugins-java/src/main/java/com/keyware/sonar/java/rules/checkers/RedirectUrlChecker.java @@ -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 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 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 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 methodParameters; + + public RedirectViewCheckVisitor(RedirectUrlChecker checker, List parameters) { + this.checker = checker; + this.methodParameters = parameters; + } + + public void check(Tree block) { + this.scanTree(block); + } + + @Override + public List 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, "在重定向前对输入数据进行验证"); + } + } + } + } + +} diff --git a/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.html b/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.html new file mode 100644 index 0000000..789c34b --- /dev/null +++ b/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.html @@ -0,0 +1,9 @@ +

在重定向前对输入数据进行验证

+

在重定向前对输入数据进行验证,确保仅重定向至允许的URL。或者在重定向至未知站点时向用户发出明确警告。

+
+
+
+

合规解决方案

+
+
+
diff --git a/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.json b/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.json new file mode 100644 index 0000000..1a51800 --- /dev/null +++ b/sonar-keyware-plugins-java/src/main/resources/org/sonar/l10n/java/rules/java/RedirectUrlChecker.json @@ -0,0 +1,13 @@ +{ + "title": "在重定向前对输入数据进行验证", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "28suo" + ], + "defaultSeverity": "Minor" +} \ No newline at end of file diff --git a/sonar-keyware-plugins-java/src/test/files/RedirectUrlChecker.java b/sonar-keyware-plugins-java/src/test/files/RedirectUrlChecker.java new file mode 100644 index 0000000..f17d8b9 --- /dev/null +++ b/sonar-keyware-plugins-java/src/test/files/RedirectUrlChecker.java @@ -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 {{在重定向前对输入数据进行验证}} + } +} \ No newline at end of file diff --git a/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/ABCVarNameCheckerTest.java b/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/ABCVarNameCheckerTest.java index 6a83980..accc754 100644 --- a/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/ABCVarNameCheckerTest.java +++ b/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/ABCVarNameCheckerTest.java @@ -24,9 +24,8 @@ public class ABCVarNameCheckerTest { ABCVarNameChecker rule = new ABCVarNameChecker(); - // Verifies that the check will raise the adequate issues with the expected message. - // In the test file, lines which should raise an issue have been commented out - // by using the following syntax: "// Noncompliant {{EXPECTED_MESSAGE}}" + // 验证检查是否会按预期检测到样例代码中预定的问题 + // 在测试样例文件中,问题所在行必须使用单行注释声明,如: "// Noncompliant {{EXPECTED_MESSAGE}}" CheckVerifier.newVerifier() .onFile("src/test/files/ABCVarNameRule.java") .withCheck(rule) diff --git a/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/RedirectUrlCheckerTest.java b/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/RedirectUrlCheckerTest.java new file mode 100644 index 0000000..c448e5e --- /dev/null +++ b/sonar-keyware-plugins-java/src/test/java/com/keyware/sonar/java/rules/checkers/RedirectUrlCheckerTest.java @@ -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(); + } +}