diff --git a/build.gradle.kts b/build.gradle.kts index 61a3bcd74..a4f84239e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -205,6 +205,7 @@ dependencies { testRuntimeOnly("org.springframework:spring-beans:latest.release") testRuntimeOnly("org.springframework:spring-context:latest.release") testRuntimeOnly("org.springframework:spring-web:latest.release") + testRuntimeOnly("org.springframework:spring-webmvc:latest.release") testRuntimeOnly("org.springframework:spring-test:latest.release") testRuntimeOnly("org.springframework.boot:spring-boot-test:latest.release") testRuntimeOnly("org.springframework.boot:spring-boot-test-autoconfigure:latest.release") diff --git a/src/main/java/org/openrewrite/java/spring/cve/Spring4Shell.java b/src/main/java/org/openrewrite/java/spring/cve/Spring4Shell.java new file mode 100644 index 000000000..274b99edc --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/cve/Spring4Shell.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.spring.cve; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.FindTypes; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; + +import java.util.Set; +import java.util.function.Supplier; + +public class Spring4Shell extends Recipe { + @Override + public String getDisplayName() { + return "Spring4Shell fix"; + } + + @Override + public String getDescription() { + return "See the [blog post](https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement#status) on the issue. This recipe can be further refined as more information becomes available."; + } + + @Override + protected JavaVisitor getSingleSourceApplicableTest() { + return new UsesType<>("org.springframework.boot.autoconfigure.SpringBootApplication"); + } + +// @Override +// protected TreeVisitor getApplicableTest() { +// // TODO add other applicable tests around presence of dependencies below a certain version, WAR packaging, etc. +// return new JavaVisitor() { +// @Override +// public J visitJavaSourceFile(JavaSourceFile cu, ExecutionContext ctx) { +// JavaVersion javaVersion = cu.getMarkers().findFirst(JavaVersion.class).orElse(null); +// if (javaVersion != null && javaVersion.getMajorVersion() >= 9) { +// return cu.withMarkers(cu.getMarkers().searchResult()); +// } +// return cu; +// } +// }; +// } + + @Override + protected JavaVisitor getVisitor() { + return new JavaIsoVisitor() { + final Supplier javaParser = () -> JavaParser.fromJavaVersion() + .classpath("spring-boot-autoconfigure", "spring-beans", "spring-web", "spring-webmvc") + .build(); + + final JavaTemplate mvcRegistration = JavaTemplate + .builder(this::getCursor, "" + + "@Bean " + + "public WebMvcRegistrations mvcRegistrations() {" + + " return new WebMvcRegistrations() {" + + " @Override" + + " public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {" + + " return null;" + + " }" + + " };" + + "}") + .javaParser(javaParser) + .imports( + "org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations", + "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter") + .build(); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + J.ClassDeclaration c = super.visitClassDeclaration(classDecl, executionContext); + Set springBootApps = FindAnnotations.find(c, "@org.springframework.boot.autoconfigure.SpringBootApplication"); + if (!springBootApps.isEmpty() && FindTypes.find(c, "org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations").isEmpty()) { + // add @Bean method + c = c.withTemplate(mvcRegistration, c.getBody().getCoordinates().addMethodDeclaration((m1, m2) -> { + if (m1.getSimpleName().equals("mvcRegistrations")) return 1; + if (m2.getSimpleName().equals("mvcRegistrations")) return -1; + return m1.getSimpleName().compareTo(m2.getSimpleName()); + })); + + maybeAddImport("org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations"); + maybeAddImport("org.springframework.context.annotation.Bean"); + maybeAddImport("org.springframework.web.bind.ServletRequestDataBinder"); + maybeAddImport("org.springframework.web.context.request.NativeWebRequest"); + maybeAddImport("org.springframework.web.method.annotation.InitBinderDataBinderFactory"); + maybeAddImport("org.springframework.web.method.support.InvocableHandlerMethod"); + maybeAddImport("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"); + maybeAddImport("org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory"); + } + return c; + } + }; + } +} diff --git a/src/main/java/org/openrewrite/java/spring/cve/package-info.java b/src/main/java/org/openrewrite/java/spring/cve/package-info.java new file mode 100644 index 000000000..11f3fd3d0 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/cve/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi @NonNullFields +package org.openrewrite.java.spring.cve; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/src/main/resources/Spring4Shell.java b/src/main/resources/Spring4Shell.java new file mode 100644 index 000000000..c2ad92736 --- /dev/null +++ b/src/main/resources/Spring4Shell.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.InitBinderDataBinderFactory; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory; + +class Fix { + @Bean + public WebMvcRegistrations mvcRegistrations() { + return new WebMvcRegistrations() { + @Override + public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { + return new ExtendedRequestMappingHandlerAdapter(); + } + }; + } + +private static class ExtendedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter { + @Override + protected InitBinderDataBinderFactory createDataBinderFactory(List methods) { + return new ServletRequestDataBinderFactory(methods, getWebBindingInitializer()) { + @Override + protected ServletRequestDataBinder createBinderInstance( + Object target, String name, NativeWebRequest request) throws Exception { + + ServletRequestDataBinder binder = super.createBinderInstance(target, name, request); + String[] fields = binder.getDisallowedFields(); + List fieldList = new ArrayList<>(fields != null ? Arrays.asList(fields) : Collections.emptyList()); + fieldList.addAll(Arrays.asList("class.*", "Class.*", "*.class.*", "*.Class.*")); + binder.setDisallowedFields(fieldList.toArray(new String[]{})); + return binder; + } + }; + } +} +} diff --git a/src/test/kotlin/org/openrewrite/java/spring/cve/Spring4ShellTest.kt b/src/test/kotlin/org/openrewrite/java/spring/cve/Spring4ShellTest.kt new file mode 100644 index 000000000..9003d2be7 --- /dev/null +++ b/src/test/kotlin/org/openrewrite/java/spring/cve/Spring4ShellTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2021 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.spring.org.openrewrite.java.spring.cve + +import org.junit.jupiter.api.Test +import org.openrewrite.Recipe +import org.openrewrite.java.JavaParser +import org.openrewrite.java.JavaRecipeTest +import org.openrewrite.java.spring.cve.Spring4Shell + +class Spring4ShellTest : JavaRecipeTest { + override val parser: JavaParser + get() = JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpath("spring-beans", "spring-boot", "spring-context") + .build() + + override val recipe: Recipe + get() = Spring4Shell() + + @Test + fun spring4Shell() = assertChanged( + before = """ + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.context.annotation.Bean; + + @SpringBootApplication + class Test { + @Bean + String existingBean() { + return "hi"; + } + } + """, + after = """ + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; + import org.springframework.context.annotation.Bean; + import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + + @SpringBootApplication + class Test { + @Bean + String existingBean() { + return "hi"; + } + + @Bean + public WebMvcRegistrations mvcRegistrations() { + return new WebMvcRegistrations() { + @Override + public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { + return null; + } + }; + } + } + """, + ) +}