Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b5a450b

Browse files
committed
SSRF query: add sanitizer looking for a variety of ways of prepending a sanitizing prefix, such as one that restricts the hostname a URI will refer to.
1 parent 487c1db commit b5a450b

5 files changed

Lines changed: 194 additions & 0 deletions

File tree

java/ql/src/Security/CWE/CWE-918/RequestForgery.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class RequestForgeryConfiguration extends TaintTracking::Configuration {
2525
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
2626
requestForgeryStep(pred, succ)
2727
}
28+
29+
override predicate isSanitizer(DataFlow::Node node) { node instanceof RequestForgerySanitizer }
2830
}
2931

3032
from DataFlow::PathNode source, DataFlow::PathNode sink, RequestForgeryConfiguration conf

java/ql/src/Security/CWE/CWE-918/RequestForgery.qll

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import semmle.code.java.frameworks.spring.Spring
55
import semmle.code.java.frameworks.JaxWS
66
import semmle.code.java.frameworks.javase.Http
77
import semmle.code.java.dataflow.DataFlow
8+
import semmle.code.java.dataflow.TaintTracking
9+
private import semmle.code.java.StringFormat
810

911
predicate requestForgeryStep(DataFlow::Node pred, DataFlow::Node succ) {
1012
// propagate to a URI when its host is assigned to
@@ -190,3 +192,83 @@ private class SpringRestTemplateUrlMethods extends Method {
190192
result = ma.getArgument(0)
191193
}
192194
}
195+
196+
/** A sanitizer for request forgery vulnerabilities. */
197+
abstract class RequestForgerySanitizer extends DataFlow::Node { }
198+
199+
private class HostnameSanitzingPrefix extends CompileTimeConstantExpr {
200+
int offset;
201+
202+
HostnameSanitzingPrefix() {
203+
exists(
204+
this.getStringValue().regexpFind(".*([?#]|[^?#:/\\\\][/\\\\]).*|[/\\\\][^/\\\\].*", 0, offset)
205+
)
206+
}
207+
208+
int getOffset() { result = offset }
209+
}
210+
211+
private AddExpr getParentAdd(AddExpr e) { result = e.getParent() }
212+
213+
private AddExpr getAnAddContainingHostnameSanitizingPrefix() {
214+
result = getParentAdd*(any(HostnameSanitzingPrefix p).getParent())
215+
}
216+
217+
private Expr getASanitizedAddOperand() {
218+
exists(AddExpr e |
219+
e = getAnAddContainingHostnameSanitizingPrefix() and
220+
(
221+
e.getLeftOperand() = getAnAddContainingHostnameSanitizingPrefix() or
222+
e.getLeftOperand() instanceof HostnameSanitzingPrefix
223+
) and
224+
result = e.getRightOperand()
225+
)
226+
}
227+
228+
private MethodAccess getNextAppend(MethodAccess append) {
229+
result = any(StringBuilderVar sbv).getNextAppend(append)
230+
}
231+
232+
class HostnameSanitizedExpr extends Expr {
233+
HostnameSanitizedExpr() {
234+
// Sanitize expressions that come after a sanitizing prefix in a tree of string additions:
235+
this = getASanitizedAddOperand()
236+
or
237+
// Sanitize expressions that come after a sanitizing prefix in a sequence of StringBuilder operations:
238+
exists(MethodAccess appendSanitizingConstant, MethodAccess subsequentAppend |
239+
appendSanitizingConstant.getArgument(0) instanceof HostnameSanitzingPrefix and
240+
getNextAppend*(appendSanitizingConstant) = subsequentAppend and
241+
this = subsequentAppend.getArgument(0)
242+
)
243+
or
244+
// Sanitize expressions that come after a sanitizing prefix in the args to a format call:
245+
exists(
246+
FormattingCall formatCall, FormatString formatString, HostnameSanitzingPrefix prefix,
247+
int sanitizedFromOffset, int laterOffset, int sanitizedArg
248+
|
249+
formatString = unique(FormatString fs | fs = formatCall.getAFormatString()) and
250+
(
251+
// An argument that sanitizes will be come before this:
252+
exists(int argIdx |
253+
formatCall.getArgumentToBeFormatted(argIdx) = prefix and
254+
sanitizedFromOffset = formatString.getAnArgUsageOffset(argIdx)
255+
)
256+
or
257+
// The format string itself sanitizes subsequent arguments:
258+
formatString = prefix.getStringValue() and
259+
sanitizedFromOffset = prefix.getOffset()
260+
) and
261+
laterOffset > sanitizedFromOffset and
262+
laterOffset = formatString.getAnArgUsageOffset(sanitizedArg) and
263+
this = formatCall.getArgumentToBeFormatted(sanitizedArg)
264+
)
265+
}
266+
}
267+
268+
/**
269+
* A value that is the result of prepending a string that prevents any value from controlling the
270+
* host of a URL.
271+
*/
272+
class HostnameSantizer extends RequestForgerySanitizer {
273+
HostnameSantizer() { this.asExpr() instanceof HostnameSanitizedExpr }
274+
}

java/ql/src/semmle/code/java/StringFormat.qll

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ class FormattingCall extends Call {
175175
)
176176
}
177177

178+
/** Gets the `i`th argument to be formatted. */
179+
Expr getArgumentToBeFormatted(int i) {
180+
i >= 0 and
181+
if this.hasExplicitVarargsArray()
182+
then
183+
result =
184+
this.getArgument(1 + this.getFormatStringIndex()).(ArrayCreationExpr).getInit().getInit(i)
185+
else result = this.getArgument(this.getFormatStringIndex() + 1 + i)
186+
}
187+
178188
/** Holds if the varargs argument is given as an explicit array. */
179189
private predicate hasExplicitVarargsArray() {
180190
this.getNumArgument() = this.getFormatStringIndex() + 2 and
@@ -353,6 +363,11 @@ class FormatString extends string {
353363
* is not referred by any format specifier.
354364
*/
355365
/*abstract*/ int getASkippedFmtSpecIndex() { none() }
366+
367+
/**
368+
* Gets an offset in this format string where argument `argNo` will be interpolated, if any.
369+
*/
370+
int getAnArgUsageOffset(int argNo) { none() }
356371
}
357372

358373
private class PrintfFormatString extends FormatString {
@@ -425,6 +440,16 @@ private class PrintfFormatString extends FormatString {
425440
result > count(int i | fmtSpecRefersToSequentialIndex(i)) and
426441
not result = fmtSpecRefersToSpecificIndex(_)
427442
}
443+
444+
override int getAnArgUsageOffset(int argNo) {
445+
argNo = fmtSpecRefersToSpecificIndex(result)
446+
or
447+
fmtSpecRefersToSequentialIndex(result) and
448+
argNo = count(int i | i < result and fmtSpecRefersToSequentialIndex(i))
449+
or
450+
fmtSpecRefersToPrevious(result) and
451+
argNo = count(int i | i < result and fmtSpecRefersToSequentialIndex(i)) - 1
452+
}
428453
}
429454

430455
private class LoggerFormatString extends FormatString {
@@ -449,4 +474,9 @@ private class LoggerFormatString extends FormatString {
449474
}
450475

451476
override int getMaxFmtSpecIndex() { result = count(int i | fmtPlaceholder(i)) }
477+
478+
override int getAnArgUsageOffset(int argNo) {
479+
fmtPlaceholder(result) and
480+
argNo = count(int i | fmtPlaceholder(i) and i < result)
481+
}
452482
}

java/ql/test/query-tests/security/CWE-918/RequestForgery.expected

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ edges
1616
| RequestForgery.java:19:31:19:57 | getParameter(...) : String | RequestForgery.java:19:23:19:58 | new URI(...) : URI |
1717
| RequestForgery.java:19:31:19:57 | getParameter(...) : String | RequestForgery.java:22:52:22:54 | uri |
1818
| RequestForgery.java:19:31:19:57 | getParameter(...) : String | RequestForgery.java:27:57:27:59 | uri |
19+
| RequestForgery.java:61:33:61:63 | getParameter(...) : String | RequestForgery.java:62:59:62:77 | new URI(...) |
20+
| RequestForgery.java:65:49:65:79 | getParameter(...) : String | RequestForgery.java:66:59:66:77 | new URI(...) |
21+
| RequestForgery.java:70:31:70:61 | getParameter(...) : String | RequestForgery.java:71:59:71:88 | new URI(...) |
22+
| RequestForgery.java:74:73:74:103 | getParameter(...) : String | RequestForgery.java:75:59:75:77 | new URI(...) |
23+
| RequestForgery.java:78:56:78:86 | getParameter(...) : String | RequestForgery.java:79:59:79:77 | new URI(...) |
24+
| RequestForgery.java:82:55:82:85 | getParameter(...) : String | RequestForgery.java:83:59:83:77 | new URI(...) |
1925
| SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:32:47:32:67 | ... + ... |
2026
| SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:37:43:37:56 | fooResourceUrl |
2127
| SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:41:42:41:55 | fooResourceUrl |
@@ -44,6 +50,18 @@ nodes
4450
| RequestForgery.java:19:31:19:57 | getParameter(...) : String | semmle.label | getParameter(...) : String |
4551
| RequestForgery.java:22:52:22:54 | uri | semmle.label | uri |
4652
| RequestForgery.java:27:57:27:59 | uri | semmle.label | uri |
53+
| RequestForgery.java:61:33:61:63 | getParameter(...) : String | semmle.label | getParameter(...) : String |
54+
| RequestForgery.java:62:59:62:77 | new URI(...) | semmle.label | new URI(...) |
55+
| RequestForgery.java:65:49:65:79 | getParameter(...) : String | semmle.label | getParameter(...) : String |
56+
| RequestForgery.java:66:59:66:77 | new URI(...) | semmle.label | new URI(...) |
57+
| RequestForgery.java:70:31:70:61 | getParameter(...) : String | semmle.label | getParameter(...) : String |
58+
| RequestForgery.java:71:59:71:88 | new URI(...) | semmle.label | new URI(...) |
59+
| RequestForgery.java:74:73:74:103 | getParameter(...) : String | semmle.label | getParameter(...) : String |
60+
| RequestForgery.java:75:59:75:77 | new URI(...) | semmle.label | new URI(...) |
61+
| RequestForgery.java:78:56:78:86 | getParameter(...) : String | semmle.label | getParameter(...) : String |
62+
| RequestForgery.java:79:59:79:77 | new URI(...) | semmle.label | new URI(...) |
63+
| RequestForgery.java:82:55:82:85 | getParameter(...) : String | semmle.label | getParameter(...) : String |
64+
| RequestForgery.java:83:59:83:77 | new URI(...) | semmle.label | new URI(...) |
4765
| SpringSSRF.java:26:33:26:60 | getParameter(...) : String | semmle.label | getParameter(...) : String |
4866
| SpringSSRF.java:32:47:32:67 | ... + ... | semmle.label | ... + ... |
4967
| SpringSSRF.java:37:43:37:56 | fooResourceUrl | semmle.label | fooResourceUrl |
@@ -66,6 +84,12 @@ nodes
6684
| RequestForgery2.java:69:29:69:32 | uri2 | RequestForgery2.java:23:27:23:53 | getParameter(...) : String | RequestForgery2.java:69:29:69:32 | uri2 | Potential server side request forgery due to $@. | RequestForgery2.java:23:27:23:53 | getParameter(...) | a user-provided value |
6785
| RequestForgery.java:22:52:22:54 | uri | RequestForgery.java:19:31:19:57 | getParameter(...) : String | RequestForgery.java:22:52:22:54 | uri | Potential server side request forgery due to $@. | RequestForgery.java:19:31:19:57 | getParameter(...) | a user-provided value |
6886
| RequestForgery.java:27:57:27:59 | uri | RequestForgery.java:19:31:19:57 | getParameter(...) : String | RequestForgery.java:27:57:27:59 | uri | Potential server side request forgery due to $@. | RequestForgery.java:19:31:19:57 | getParameter(...) | a user-provided value |
87+
| RequestForgery.java:62:59:62:77 | new URI(...) | RequestForgery.java:61:33:61:63 | getParameter(...) : String | RequestForgery.java:62:59:62:77 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:61:33:61:63 | getParameter(...) | a user-provided value |
88+
| RequestForgery.java:66:59:66:77 | new URI(...) | RequestForgery.java:65:49:65:79 | getParameter(...) : String | RequestForgery.java:66:59:66:77 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:65:49:65:79 | getParameter(...) | a user-provided value |
89+
| RequestForgery.java:71:59:71:88 | new URI(...) | RequestForgery.java:70:31:70:61 | getParameter(...) : String | RequestForgery.java:71:59:71:88 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:70:31:70:61 | getParameter(...) | a user-provided value |
90+
| RequestForgery.java:75:59:75:77 | new URI(...) | RequestForgery.java:74:73:74:103 | getParameter(...) : String | RequestForgery.java:75:59:75:77 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:74:73:74:103 | getParameter(...) | a user-provided value |
91+
| RequestForgery.java:79:59:79:77 | new URI(...) | RequestForgery.java:78:56:78:86 | getParameter(...) : String | RequestForgery.java:79:59:79:77 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:78:56:78:86 | getParameter(...) | a user-provided value |
92+
| RequestForgery.java:83:59:83:77 | new URI(...) | RequestForgery.java:82:55:82:85 | getParameter(...) : String | RequestForgery.java:83:59:83:77 | new URI(...) | Potential server side request forgery due to $@. | RequestForgery.java:82:55:82:85 | getParameter(...) | a user-provided value |
6993
| SpringSSRF.java:32:47:32:67 | ... + ... | SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:32:47:32:67 | ... + ... | Potential server side request forgery due to $@. | SpringSSRF.java:26:33:26:60 | getParameter(...) | a user-provided value |
7094
| SpringSSRF.java:37:43:37:56 | fooResourceUrl | SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:37:43:37:56 | fooResourceUrl | Potential server side request forgery due to $@. | SpringSSRF.java:26:33:26:60 | getParameter(...) | a user-provided value |
7195
| SpringSSRF.java:41:42:41:55 | fooResourceUrl | SpringSSRF.java:26:33:26:60 | getParameter(...) : String | SpringSSRF.java:41:42:41:55 | fooResourceUrl | Potential server side request forgery due to $@. | SpringSSRF.java:26:33:26:60 | getParameter(...) | a user-provided value |

java/ql/test/query-tests/security/CWE-918/RequestForgery.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,62 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
2727
HttpRequest r2 = HttpRequest.newBuilder(uri).build();
2828
client.send(r2, null);
2929
}
30+
31+
// GOOD: sanitisation by concatenation with a prefix that prevents targeting an arbitrary host.
32+
// We test a few different ways of sanitisation: via string conctentation (perhaps nested),
33+
// via a stringbuilder and via String.format.
34+
String safeUri3 = "https://example.com/" + request.getParameter("uri3");
35+
HttpRequest r3 = HttpRequest.newBuilder(new URI(safeUri3)).build();
36+
client.send(r3, null);
37+
38+
String safeUri4 = "https://example.com/" + ("someprefix" + request.getParameter("uri4"));
39+
HttpRequest r4 = HttpRequest.newBuilder(new URI(safeUri4)).build();
40+
client.send(r4, null);
41+
42+
StringBuilder safeUri5 = new StringBuilder();
43+
safeUri5.append("https://example.com/").append(request.getParameter("uri5"));
44+
HttpRequest r5 = HttpRequest.newBuilder(new URI(safeUri5.toString())).build();
45+
client.send(r5, null);
46+
47+
String safeUri6 = String.format("https://example.com/%s", request.getParameter("uri6"));
48+
HttpRequest r6 = HttpRequest.newBuilder(new URI(safeUri6)).build();
49+
client.send(r6, null);
50+
51+
String safeUri7 = String.format("%s/%s", "https://example.com", request.getParameter("uri7"));
52+
HttpRequest r7 = HttpRequest.newBuilder(new URI(safeUri7)).build();
53+
client.send(r7, null);
54+
55+
String safeUri8 = String.format("%s%s", "https://example.com/", request.getParameter("uri8"));
56+
HttpRequest r8 = HttpRequest.newBuilder(new URI(safeUri8)).build();
57+
client.send(r8, null);
58+
59+
// BAD: cases where a string that would sanitise is used, but occurs in the wrong
60+
// place to sanitise user input:
61+
String unsafeUri3 = request.getParameter("baduri3") + "https://example.com/";
62+
HttpRequest unsafer3 = HttpRequest.newBuilder(new URI(unsafeUri3)).build();
63+
client.send(unsafer3, null);
64+
65+
String unsafeUri4 = ("someprefix" + request.getParameter("baduri4")) + "https://example.com/";
66+
HttpRequest unsafer4 = HttpRequest.newBuilder(new URI(unsafeUri4)).build();
67+
client.send(unsafer4, null);
68+
69+
StringBuilder unsafeUri5 = new StringBuilder();
70+
unsafeUri5.append(request.getParameter("baduri5")).append("https://example.com/");
71+
HttpRequest unsafer5 = HttpRequest.newBuilder(new URI(unsafeUri5.toString())).build();
72+
client.send(unsafer5, null);
73+
74+
String unsafeUri6 = String.format("%shttps://example.com/", request.getParameter("baduri6"));
75+
HttpRequest unsafer6 = HttpRequest.newBuilder(new URI(unsafeUri6)).build();
76+
client.send(unsafer6, null);
77+
78+
String unsafeUri7 = String.format("%s/%s", request.getParameter("baduri7"), "https://example.com");
79+
HttpRequest unsafer7 = HttpRequest.newBuilder(new URI(unsafeUri7)).build();
80+
client.send(unsafer7, null);
81+
82+
String unsafeUri8 = String.format("%s%s", request.getParameter("baduri8"), "https://example.com/");
83+
HttpRequest unsafer8 = HttpRequest.newBuilder(new URI(unsafeUri8)).build();
84+
client.send(unsafer8, null);
85+
3086
} catch (Exception e) {
3187
// TODO: handle exception
3288
}

0 commit comments

Comments
 (0)