From 0db74966174107024967d014817612ee17bea74f Mon Sep 17 00:00:00 2001 From: p0wn4j Date: Sun, 27 Jun 2021 21:52:10 +0400 Subject: [PATCH 1/3] Add URLClassLoader and Spring WebClient SSRF sinks --- .../code/java/dataflow/ExternalFlow.qll | 6 ++ .../frameworks/spring/SpringWebClient.qll | 4 +- .../CWE-918/ReactiveWebClientSSRF.java | 49 ++++++++++ .../security/CWE-918/URLClassLoaderSSRF.java | 98 +++++++++++++++++++ .../test/query-tests/security/CWE-918/options | 3 +- .../reactor/core/publisher/Mono.java | 22 +++++ .../client/DefaultWebClientBuilder.java | 40 ++++++++ .../reactive/function/client/WebClient.java | 73 ++++++++++++++ 8 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 java/ql/test/query-tests/security/CWE-918/ReactiveWebClientSSRF.java create mode 100644 java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java create mode 100644 java/ql/test/stubs/projectreactor-3.4.3/reactor/core/publisher/Mono.java create mode 100644 java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java create mode 100644 java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/WebClient.java diff --git a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll index f1a17c05723..ed51f62fa6a 100644 --- a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll @@ -217,6 +217,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..73a68cc4d05 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java @@ -0,0 +1,98 @@ +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; + +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 = TomcatURLStreamHandlerFactory.getInstance(); + 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[]{new URL[]{uri.toURL()}}, + URLClassLoaderSSRF.class.getClassLoader() + ); // $ SSRF + + 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 = TomcatURLStreamHandlerFactory.getInstance(); + + URLClassLoader urlClassLoader = + new URLClassLoader("testClassLoader", + new URL[]{uri.toURL()}, + URLClassLoaderSSRF.class.getClassLoader(), + urlStreamHandlerFactory + ); // $ SSRF + + 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 From 44e8dd9ec56802efc0558ad8aad205d0863a4eca Mon Sep 17 00:00:00 2001 From: Chris Smowton Date: Thu, 1 Jul 2021 13:34:41 +0100 Subject: [PATCH 2/3] Add change note --- .../2021-07-01-url-classloader-reactive-webclient.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 java/change-notes/2021-07-01-url-classloader-reactive-webclient.md 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`. From e0a7f6e14f50a7da984969a6adb79adc8a891ef9 Mon Sep 17 00:00:00 2001 From: Chris Smowton Date: Thu, 1 Jul 2021 15:03:38 +0100 Subject: [PATCH 3/3] Fix URLClassLoader test --- .../security/CWE-918/URLClassLoaderSSRF.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java b/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java index 73a68cc4d05..84d53f797be 100644 --- a/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java +++ b/java/ql/test/query-tests/security/CWE-918/URLClassLoaderSSRF.java @@ -6,6 +6,7 @@ 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 { @@ -39,7 +40,7 @@ public class URLClassLoaderSSRF extends HttpServlet { String url = request.getParameter("uri"); URI uri = new URI(url); - URLStreamHandlerFactory urlStreamHandlerFactory = TomcatURLStreamHandlerFactory.getInstance(); + URLStreamHandlerFactory urlStreamHandlerFactory = null; URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{uri.toURL()}, URLClassLoaderSSRF.class.getClassLoader(), urlStreamHandlerFactory); // $ SSRF urlClassLoader.findResource("test"); } catch (Exception e) { @@ -64,11 +65,11 @@ public class URLClassLoaderSSRF extends HttpServlet { try { String url = request.getParameter("uri"); URI uri = new URI(url); - URLClassLoader urlClassLoader = - new URLClassLoader("testClassLoader", - new URL[]{new URL[]{uri.toURL()}}, + URLClassLoader urlClassLoader = + new URLClassLoader("testClassLoader", + new URL[]{uri.toURL()}, // $ SSRF URLClassLoaderSSRF.class.getClassLoader() - ); // $ SSRF + ); Class rceTest = urlClassLoader.loadClass("RCETest"); } catch (Exception e) { @@ -81,14 +82,14 @@ public class URLClassLoaderSSRF extends HttpServlet { try { String url = request.getParameter("uri"); URI uri = new URI(url); - URLStreamHandlerFactory urlStreamHandlerFactory = TomcatURLStreamHandlerFactory.getInstance(); + URLStreamHandlerFactory urlStreamHandlerFactory = null; URLClassLoader urlClassLoader = new URLClassLoader("testClassLoader", - new URL[]{uri.toURL()}, - URLClassLoaderSSRF.class.getClassLoader(), + new URL[]{uri.toURL()}, // $ SSRF + URLClassLoaderSSRF.class.getClassLoader(), urlStreamHandlerFactory - ); // $ SSRF + ); Class rceTest = urlClassLoader.loadClass("RCETest"); } catch (Exception e) {