Skip to content

Commit be9509c

Browse files
authored
Merge pull request #9199 from luchua-bc/java/unsafe-url-forward-dispatch-load
Java: CWE-552 Query to detect unsafe resource loading in Java Spring applications
2 parents 162edd6 + 7ff82bb commit be9509c

File tree

10 files changed

+316
-5
lines changed

10 files changed

+316
-5
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//BAD: no path validation in Spring resource loading
2+
@GetMapping("/file")
3+
public String getFileContent(@RequestParam(name="fileName") String fileName) {
4+
ClassPathResource clr = new ClassPathResource(fileName);
5+
6+
File file = ResourceUtils.getFile(fileName);
7+
8+
Resource resource = resourceLoader.getResource(fileName);
9+
}
10+
11+
//GOOD: check for a trusted prefix, ensuring path traversal is not used to erase that prefix in Spring resource loading:
12+
@GetMapping("/file")
13+
public String getFileContent(@RequestParam(name="fileName") String fileName) {
14+
if (!fileName.contains("..") && fileName.hasPrefix("/public-content")) {
15+
ClassPathResource clr = new ClassPathResource(fileName);
16+
17+
File file = ResourceUtils.getFile(fileName);
18+
19+
Resource resource = resourceLoader.getResource(fileName);
20+
}
21+
}

java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qhelp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ file exposure attacks. It also shows how to remedy the problem by validating the
4343

4444
<sample src="UnsafeResourceGet.java" />
4545

46+
<p>The following examples show an HTTP request parameter being used directly to retrieve a resource
47+
of a Java Spring application without validating the input, which allows sensitive file exposure
48+
attacks. It also shows how to remedy the problem by validating the user input.
49+
</p>
50+
51+
<sample src="UnsafeLoadSpringResource.java" />
4652
</example>
4753
<references>
4854
<li>File Disclosure:
@@ -57,5 +63,8 @@ file exposure attacks. It also shows how to remedy the problem by validating the
5763
<li>CVE-2015-5174:
5864
<a href="https://vuldb.com/?id.81084">Apache Tomcat 6.0/7.0/8.0/9.0 Servletcontext getResource/getResourceAsStream/getResourcePaths Path Traversal</a>
5965
</li>
66+
<li>CVE-2019-3799:
67+
<a href="https://github.com/mpgn/CVE-2019-3799">CVE-2019-3799 - Spring-Cloud-Config-Server Directory Traversal &lt; 2.1.2, 2.0.4, 1.4.6</a>
68+
</li>
6069
</references>
6170
</qhelp>

java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* @name Unsafe URL forward or dispatch from remote source
3-
* @description URL forward or dispatch based on unvalidated user-input
2+
* @name Unsafe URL forward, dispatch, or load from remote source
3+
* @description URL forward, dispatch, or load based on unvalidated user-input
44
* may cause file information disclosure.
55
* @kind path-problem
66
* @problem.severity error
77
* @precision high
8-
* @id java/unsafe-url-forward-dispatch
8+
* @id java/unsafe-url-forward-dispatch-load
99
* @tags security
1010
* external/cwe-552
1111
*/

java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ private import semmle.code.java.dataflow.ExternalFlow
44
private import semmle.code.java.dataflow.FlowSources
55
private import semmle.code.java.dataflow.StringPrefixes
66
private import semmle.code.java.frameworks.javaee.ejb.EJBRestrictions
7+
private import experimental.semmle.code.java.frameworks.SpringResource
78

89
/** A sink for unsafe URL forward vulnerabilities. */
910
abstract class UnsafeUrlForwardSink extends DataFlow::Node { }
@@ -86,6 +87,8 @@ private class GetResourceSink extends UnsafeUrlForwardSink {
8687
GetResourceSink() {
8788
sinkNode(this, "open-url")
8889
or
90+
sinkNode(this, "get-resource")
91+
or
8992
exists(MethodAccess ma |
9093
(
9194
ma.getMethod() instanceof GetServletResourceAsStreamMethod or
@@ -99,6 +102,16 @@ private class GetResourceSink extends UnsafeUrlForwardSink {
99102
}
100103
}
101104

105+
/** A sink for methods that load Spring resources. */
106+
private class SpringResourceSink extends UnsafeUrlForwardSink {
107+
SpringResourceSink() {
108+
exists(MethodAccess ma |
109+
ma.getMethod() instanceof GetResourceUtilsMethod and
110+
ma.getArgument(0) = this.asExpr()
111+
)
112+
}
113+
}
114+
102115
/** An argument to `new ModelAndView` or `ModelAndView.setViewName`. */
103116
private class SpringModelAndViewSink extends UnsafeUrlForwardSink {
104117
SpringModelAndViewSink() {
@@ -175,3 +188,25 @@ private class FilePathFlowStep extends SummaryModelCsv {
175188
]
176189
}
177190
}
191+
192+
/** Taint models related to resource loading in Spring. */
193+
private class LoadSpringResourceFlowStep extends SummaryModelCsv {
194+
override predicate row(string row) {
195+
row =
196+
[
197+
"org.springframework.core.io;ClassPathResource;false;ClassPathResource;;;Argument[0];Argument[-1];taint;manual",
198+
"org.springframework.core.io;ResourceLoader;true;getResource;;;Argument[0];ReturnValue;taint;manual",
199+
"org.springframework.core.io;Resource;true;createRelative;;;Argument[0];ReturnValue;taint;manual"
200+
]
201+
}
202+
}
203+
204+
/** Sink models for methods that load Spring resources. */
205+
private class SpringResourceCsvSink extends SinkModelCsv {
206+
override predicate row(string row) {
207+
row =
208+
// Get spring resource
209+
"org.springframework.core.io;ClassPathResource;true;" +
210+
["getFilename", "getPath", "getURL", "resolveURL"] + ";;;Argument[-1];get-resource;manual"
211+
}
212+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Provides classes for working with resource loading in Spring.
3+
*/
4+
5+
import java
6+
private import semmle.code.java.dataflow.ExternalFlow
7+
private import semmle.code.java.dataflow.FlowSources
8+
9+
/** A utility class for resolving resource locations to files in the file system in the Spring framework. */
10+
class ResourceUtils extends Class {
11+
ResourceUtils() { this.hasQualifiedName("org.springframework.util", "ResourceUtils") }
12+
}
13+
14+
/**
15+
* A method declared in `org.springframework.util.ResourceUtils` that loads Spring resources.
16+
*/
17+
class GetResourceUtilsMethod extends Method {
18+
GetResourceUtilsMethod() {
19+
this.getDeclaringType().getASupertype*() instanceof ResourceUtils and
20+
this.hasName(["extractArchiveURL", "extractJarFileURL", "getFile", "getURL"])
21+
}
22+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.example;
2+
3+
import java.io.File;
4+
import java.io.FileReader;
5+
import java.io.InputStreamReader;
6+
import java.io.IOException;
7+
import java.io.Reader;
8+
import java.io.UnsupportedEncodingException;
9+
import java.net.URLDecoder;
10+
import java.nio.file.Files;
11+
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.core.io.ClassPathResource;
14+
import org.springframework.core.io.Resource;
15+
import org.springframework.core.io.ResourceLoader;
16+
import org.springframework.util.ResourceUtils;
17+
import org.springframework.util.StringUtils;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.RequestParam;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
/** Sample class of Spring RestController */
23+
@RestController
24+
public class UnsafeLoadSpringResource {
25+
@GetMapping("/file1")
26+
//BAD: Get resource from ClassPathResource without input validation
27+
public String getFileContent1(@RequestParam(name="fileName") String fileName) {
28+
// A request such as the following can disclose source code and application configuration
29+
// fileName=/../../WEB-INF/views/page.jsp
30+
// fileName=/com/example/package/SampleController.class
31+
ClassPathResource clr = new ClassPathResource(fileName);
32+
char[] buffer = new char[4096];
33+
StringBuilder out = new StringBuilder();
34+
try {
35+
Reader in = new FileReader(clr.getFilename());
36+
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
37+
out.append(buffer, 0, numRead);
38+
}
39+
} catch (IOException ie) {
40+
ie.printStackTrace();
41+
}
42+
return out.toString();
43+
}
44+
45+
@GetMapping("/file1a")
46+
//GOOD: Get resource from ClassPathResource with input path validation
47+
public String getFileContent1a(@RequestParam(name="fileName") String fileName) {
48+
String result = null;
49+
if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
50+
ClassPathResource clr = new ClassPathResource(fileName);
51+
char[] buffer = new char[4096];
52+
StringBuilder out = new StringBuilder();
53+
try {
54+
Reader in = new InputStreamReader(clr.getInputStream(), "UTF-8");
55+
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
56+
out.append(buffer, 0, numRead);
57+
}
58+
} catch (IOException ie) {
59+
ie.printStackTrace();
60+
}
61+
result = out.toString();
62+
}
63+
return result;
64+
}
65+
66+
@GetMapping("/file2")
67+
//BAD: Get resource from ResourceUtils without input validation
68+
public String getFileContent2(@RequestParam(name="fileName") String fileName) {
69+
String content = null;
70+
71+
try {
72+
// A request such as the following can disclose source code and system configuration
73+
// fileName=/etc/hosts
74+
// fileName=file:/etc/hosts
75+
// fileName=/opt/appdir/WEB-INF/views/page.jsp
76+
File file = ResourceUtils.getFile(fileName);
77+
//Read File Content
78+
content = new String(Files.readAllBytes(file.toPath()));
79+
} catch (IOException ie) {
80+
ie.printStackTrace();
81+
}
82+
return content;
83+
}
84+
85+
@GetMapping("/file2a")
86+
//GOOD: Get resource from ResourceUtils with input path validation
87+
public String getFileContent2a(@RequestParam(name="fileName") String fileName) {
88+
String content = null;
89+
90+
if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
91+
try {
92+
File file = ResourceUtils.getFile(fileName);
93+
//Read File Content
94+
content = new String(Files.readAllBytes(file.toPath()));
95+
} catch (IOException ie) {
96+
ie.printStackTrace();
97+
}
98+
}
99+
return content;
100+
}
101+
102+
@Autowired
103+
ResourceLoader resourceLoader;
104+
105+
@GetMapping("/file3")
106+
//BAD: Get resource from ResourceLoader (same as application context) without input validation
107+
// Note it is not detected without the generic `resource.getInputStream()` check
108+
public String getFileContent3(@RequestParam(name="fileName") String fileName) {
109+
String content = null;
110+
111+
try {
112+
// A request such as the following can disclose source code and system configuration
113+
// fileName=/WEB-INF/views/page.jsp
114+
// fileName=/WEB-INF/classes/com/example/package/SampleController.class
115+
// fileName=file:/etc/hosts
116+
Resource resource = resourceLoader.getResource(fileName);
117+
118+
char[] buffer = new char[4096];
119+
StringBuilder out = new StringBuilder();
120+
121+
Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
122+
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
123+
out.append(buffer, 0, numRead);
124+
}
125+
content = out.toString();
126+
} catch (IOException ie) {
127+
ie.printStackTrace();
128+
}
129+
return content;
130+
}
131+
132+
@GetMapping("/file3a")
133+
//GOOD: Get resource from ResourceLoader (same as application context) with input path validation
134+
public String getFileContent3a(@RequestParam(name="fileName") String fileName) {
135+
String content = null;
136+
137+
if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
138+
try {
139+
Resource resource = resourceLoader.getResource(fileName);
140+
141+
char[] buffer = new char[4096];
142+
StringBuilder out = new StringBuilder();
143+
144+
Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
145+
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
146+
out.append(buffer, 0, numRead);
147+
}
148+
content = out.toString();
149+
} catch (IOException ie) {
150+
ie.printStackTrace();
151+
}
152+
}
153+
return content;
154+
}
155+
}

java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
edges
2+
| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String |
3+
| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | UnsafeLoadSpringResource.java:35:31:35:33 | clr |
4+
| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource |
5+
| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName |
26
| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path |
37
| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:17:20:17:25 | params : Map |
48
| UnsafeResourceGet2.java:17:20:17:25 | params : Map | UnsafeResourceGet2.java:17:20:17:40 | get(...) : Object |
@@ -30,6 +34,12 @@ edges
3034
| UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... |
3135
| UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... |
3236
nodes
37+
| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | semmle.label | fileName : String |
38+
| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | semmle.label | new ClassPathResource(...) : ClassPathResource |
39+
| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | semmle.label | fileName : String |
40+
| UnsafeLoadSpringResource.java:35:31:35:33 | clr | semmle.label | clr |
41+
| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | semmle.label | fileName : String |
42+
| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | semmle.label | fileName |
3343
| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | semmle.label | getServletPath(...) : String |
3444
| UnsafeRequestPath.java:23:33:23:36 | path | semmle.label | path |
3545
| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
@@ -82,6 +92,8 @@ nodes
8292
| UnsafeUrlForward.java:60:33:60:62 | ... + ... | semmle.label | ... + ... |
8393
subpaths
8494
#select
95+
| UnsafeLoadSpringResource.java:35:31:35:33 | clr | UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:35:31:35:33 | clr | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:27:32:27:77 | fileName | user-provided value |
96+
| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:68:32:68:77 | fileName | user-provided value |
8597
| UnsafeRequestPath.java:23:33:23:36 | path | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path | Potentially untrusted URL forward due to $@. | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) | user-provided value |
8698
| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:19:93:19:99 | loadUrl | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) | user-provided value |
8799
| UnsafeResourceGet2.java:37:20:37:22 | url | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:37:20:37:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) | user-provided value |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/:${testdir}/../../../../stubs/javax-faces-2.3/:${testdir}/../../../../stubs/undertow-io-2.2/:${testdir}/../../../../stubs/jboss-vfs-3.2/
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/:${testdir}/../../../../stubs/javax-faces-2.3/:${testdir}/../../../../stubs/undertow-io-2.2/:${testdir}/../../../../stubs/jboss-vfs-3.2/:${testdir}/../../../../stubs/springframework-5.3.8/

java/ql/test/stubs/springframework-5.3.8/org/springframework/core/io/ClassPathResource.java

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

java/ql/test/stubs/springframework-5.3.8/org/springframework/core/io/ResourceLoader.java

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)