diff --git a/java/change-notes/2021-07-01-url-classloader-reactive-webclient.md b/java/change-notes/2021-07-01-url-classloader-reactive-webclient.md new file mode 100644 index 00000000000..5cbd0cd0609 --- /dev/null +++ b/java/change-notes/2021-07-01-url-classloader-reactive-webclient.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added support for two new APIs susceptible to server-side request forgery (SSRF): using a `URLClassLoader`, and using Spring Web Reactive's `WebClient`. diff --git a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll index 39b79ca1ea1..647968e9bc6 100644 --- a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll @@ -218,6 +218,12 @@ private predicate sinkModelCsv(string row) { "java.net;URL;false;openStream;;;Argument[-1];open-url", "java.net.http;HttpRequest;false;newBuilder;;;Argument[0];open-url", "java.net.http;HttpRequest$Builder;false;uri;;;Argument[0];open-url", + "java.net;URLClassLoader;false;URLClassLoader;(URL[]);;Argument[0];open-url", + "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader);;Argument[0];open-url", + "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader,URLStreamHandlerFactory);;Argument[0];open-url", + "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader);;Argument[1];open-url", + "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader,URLStreamHandlerFactory);;Argument[1];open-url", + "java.net;URLClassLoader;false;newInstance;;;Argument[0];open-url", // Create file "java.io;FileOutputStream;false;FileOutputStream;;;Argument[0];create-file", "java.io;RandomAccessFile;false;RandomAccessFile;;;Argument[0];create-file", diff --git a/java/ql/src/semmle/code/java/frameworks/spring/SpringWebClient.qll b/java/ql/src/semmle/code/java/frameworks/spring/SpringWebClient.qll index cb5391257d8..fa8a2c7a1c1 100644 --- a/java/ql/src/semmle/code/java/frameworks/spring/SpringWebClient.qll +++ b/java/ql/src/semmle/code/java/frameworks/spring/SpringWebClient.qll @@ -45,7 +45,9 @@ private class UrlOpenSink extends SinkModelCsv { "org.springframework.web.client;RestTemplate;false;postForEntity;;;Argument[0];open-url", "org.springframework.web.client;RestTemplate;false;postForLocation;;;Argument[0];open-url", "org.springframework.web.client;RestTemplate;false;postForObject;;;Argument[0];open-url", - "org.springframework.web.client;RestTemplate;false;put;;;Argument[0];open-url" + "org.springframework.web.client;RestTemplate;false;put;;;Argument[0];open-url", + "org.springframework.web.reactive.function.client;WebClient;false;create;;;Argument[0];open-url", + "org.springframework.web.reactive.function.client;WebClient$Builder;false;baseUrl;;;Argument[0];open-url" ] } } diff --git a/java/ql/test/query-tests/security/CWE-918/ReactiveWebClientSSRF.java b/java/ql/test/query-tests/security/CWE-918/ReactiveWebClientSSRF.java new file mode 100644 index 00000000000..00d707f71e4 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-918/ReactiveWebClientSSRF.java @@ -0,0 +1,49 @@ +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ReactiveWebClientSSRF extends HttpServlet { + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + WebClient webClient = WebClient.create(url); // $ SSRF + + Mono result = webClient.get() + .uri("/") + .retrieve() + .bodyToMono(String.class); + + result.block(); + } catch (Exception e) { + // Ignore + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + WebClient webClient = WebClient.builder() + .defaultHeader("User-Agent", "Java") + .baseUrl(url) // $ SSRF + .build(); + + + Mono result = webClient.get() + .uri("/") + .retrieve() + .bodyToMono(String.class); + + result.block(); + } catch (Exception e) { + // Ignore + } + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java b/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java new file mode 100644 index 00000000000..84d53f797be --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java @@ -0,0 +1,99 @@ +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLStreamHandlerFactory; + +public class URLClassLoaderSSRF extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{uri.toURL()}); // $ SSRF + Class test = urlClassLoader.loadClass("test"); + } catch (Exception e) { + // Ignore + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{uri.toURL()}, URLClassLoaderSSRF.class.getClassLoader()); // $ SSRF + Class test = urlClassLoader.loadClass("test"); + } catch (Exception e) { + // Ignore + } + } + + protected void doPut(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + + URLStreamHandlerFactory urlStreamHandlerFactory = null; + URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{uri.toURL()}, URLClassLoaderSSRF.class.getClassLoader(), urlStreamHandlerFactory); // $ SSRF + urlClassLoader.findResource("test"); + } catch (Exception e) { + // Ignore + } + } + + protected void doDelete(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + URLClassLoader urlClassLoader = URLClassLoader.newInstance(new URL[]{uri.toURL()}); // $ SSRF + urlClassLoader.getResourceAsStream("test"); + } catch (Exception e) { + // Ignore + } + } + + protected void doOptions(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + URLClassLoader urlClassLoader = + new URLClassLoader("testClassLoader", + new URL[]{uri.toURL()}, // $ SSRF + URLClassLoaderSSRF.class.getClassLoader() + ); + + Class rceTest = urlClassLoader.loadClass("RCETest"); + } catch (Exception e) { + // Ignore + } + } + + protected void doTrace(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + String url = request.getParameter("uri"); + URI uri = new URI(url); + URLStreamHandlerFactory urlStreamHandlerFactory = null; + + URLClassLoader urlClassLoader = + new URLClassLoader("testClassLoader", + new URL[]{uri.toURL()}, // $ SSRF + URLClassLoaderSSRF.class.getClassLoader(), + urlStreamHandlerFactory + ); + + Class rceTest = urlClassLoader.loadClass("RCETest"); + } catch (Exception e) { + // Ignore + } + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-918/options b/java/ql/test/query-tests/security/CWE-918/options index b1fc2007fe7..87db9eacec3 100644 --- a/java/ql/test/query-tests/security/CWE-918/options +++ b/java/ql/test/query-tests/security/CWE-918/options @@ -1 +1,2 @@ -//semmle-extractor-options: --javac-args -source 11 -target 11 -cp ${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/javax-ws-rs-api-2.1.1:${testdir}/../../../stubs/javax-ws-rs-api-3.0.0:${testdir}/../../../stubs/apache-http-4.4.13/:${testdir}/../../../stubs/servlet-api-2.4/ +//semmle-extractor-options: --javac-args -source 11 -target 11 -cp ${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/javax-ws-rs-api-2.1.1:${testdir}/../../../stubs/javax-ws-rs-api-3.0.0:${testdir}/../../../stubs/apache-http-4.4.13/:${testdir}/../../../stubs/servlet-api-2.4/:${testdir}/../../../stubs/projectreactor-3.4.3/ + diff --git a/java/ql/test/stubs/projectreactor-3.4.3/reactor/core/publisher/Mono.java b/java/ql/test/stubs/projectreactor-3.4.3/reactor/core/publisher/Mono.java new file mode 100644 index 00000000000..89d6bf8673b --- /dev/null +++ b/java/ql/test/stubs/projectreactor-3.4.3/reactor/core/publisher/Mono.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2011-Present VMware Inc. or its affiliates, All Rights Reserved. + * + * 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 reactor.core.publisher; + +public abstract class Mono { + public T block() { + return null; + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java new file mode 100644 index 00000000000..2342f36c43e --- /dev/null +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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.springframework.web.reactive.function.client; + +final class DefaultWebClientBuilder implements WebClient.Builder { + + public DefaultWebClientBuilder() { + } + + @Override + public WebClient.Builder baseUrl(String baseUrl) { + return this; + } + + @Override + public WebClient.Builder defaultHeader(String header, String... values) { + return this; + } + + + @Override + public WebClient build() { + return null; + } + +} \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/WebClient.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/WebClient.java new file mode 100644 index 00000000000..13f207dd711 --- /dev/null +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/WebClient.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2020 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.springframework.web.reactive.function.client; + +import reactor.core.publisher.Mono; + +/** + Spring Reactor WebClient interface stub + */ +public interface WebClient { + + RequestHeadersUriSpec get(); + RequestHeadersUriSpec head(); + RequestBodyUriSpec post(); + RequestBodyUriSpec put(); + RequestBodyUriSpec patch(); + RequestHeadersUriSpec delete(); + RequestHeadersUriSpec options(); + + static WebClient create(String baseUrl) { + return null; + } + + static WebClient create() { + return null; + } + + static WebClient.Builder builder() { + return null; + } + + interface Builder { + Builder baseUrl(String baseUrl); + Builder defaultHeader(String header, String... values); + WebClient build(); + } + + interface UriSpec> { + S uri(String uri, Object... uriVariables); + } + + interface RequestBodySpec extends RequestHeadersSpec { + } + + interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec { + } + + interface ResponseSpec { + Mono bodyToMono(Class elementClass); + } + + interface RequestHeadersUriSpec> + extends UriSpec, RequestHeadersSpec { + } + + interface RequestHeadersSpec> { + ResponseSpec retrieve(); + } +} \ No newline at end of file