From a93cb6c48e030f493936e9e54872d25201920622 Mon Sep 17 00:00:00 2001 From: elsapet Date: Wed, 7 Feb 2024 09:51:56 +0200 Subject: [PATCH] feat(java): add SSRF rule (CWE-918) (#235) --- rules/java/lang/http_url_using_user_input.yml | 86 +++++++++++++++++++ .../lang/http_url_using_user_input/test.js | 18 ++++ .../testdata/main.java | 41 +++++++++ 3 files changed, 145 insertions(+) create mode 100644 rules/java/lang/http_url_using_user_input.yml create mode 100644 tests/java/lang/http_url_using_user_input/test.js create mode 100644 tests/java/lang/http_url_using_user_input/testdata/main.java diff --git a/rules/java/lang/http_url_using_user_input.yml b/rules/java/lang/http_url_using_user_input.yml new file mode 100644 index 000000000..28f267654 --- /dev/null +++ b/rules/java/lang/http_url_using_user_input.yml @@ -0,0 +1,86 @@ +imports: + - java_shared_lang_user_input +patterns: + - pattern: | + $.$() + filters: + - variable: USER_INPUT_URL + detection: java_lang_http_url_using_user_input_url + scope: cursor + - variable: METHOD + values: + - connect + - GetContent + - openConnection + - openStream + - getContent + - pattern: | + new $($$<...>); + filters: + - variable: INET_SOCKET_ADDRESS + regex: \A(java\.net\.)?InetSocketAddress\z + - variable: USER_INPUT + detection: java_lang_http_url_using_user_input_user_input + scope: result +auxiliary: + - id: java_lang_http_url_using_user_input_url + patterns: + - pattern: new $($); + filters: + - variable: URI + regex: \A(java\.net\.)?(URL|URI)\z + - variable: USER_INPUT + detection: java_lang_http_url_using_user_input_user_input + scope: result + - id: java_lang_http_url_using_user_input_user_input + sanitizer: java_lang_http_url_using_user_input_sanitizer + patterns: + - pattern: $; + filters: + - variable: USER_INPUT + detection: java_shared_lang_user_input + scope: cursor + - id: java_lang_http_url_using_user_input_sanitizer + patterns: + - pattern: $.encode($$<_>$<...>); + filters: + - variable: CLASS + regex: \A(java\.net\.)?URLEncoder\z + - pattern: $.escape($$<_>); + filters: + - variable: PATH_SEGMENT_ESCAPER + regex: \A((com\.google\.common\.net\.)?UrlEscapers\.)?urlPathSegmentEscaper\(\)\z +languages: + - java +metadata: + description: "Unsanitized user input in HTTP request (SSRF)" + remediation_message: | + ## Description + + Applications should not connect to locations formed from user input. This is bad security practice because it can lead to Server-Side-Request-Forgery (SSRF) attacks. + This rule checks for URLs containing user-supplied data. + + ## Remediations + + ❌ Avoid using user input in HTTP URLs: + + ```java + new URL(request.getParameter("someRandomUrl")).getContent(); + ``` + + ✅ Use user input indirectly to form a URL: + + ```java + String url; + if (request.getParameter("selectedUrl") == "option1") { + url = "api1.com"; + } else { + url = "api2.com"; + } + + new URL(url).getContent(); + ``` + cwe_id: + - 918 + id: java_lang_http_url_using_user_input + documentation_url: https://docs.bearer.com/reference/rules/java_lang_http_url_using_user_input diff --git a/tests/java/lang/http_url_using_user_input/test.js b/tests/java/lang/http_url_using_user_input/test.js new file mode 100644 index 000000000..f2bc09776 --- /dev/null +++ b/tests/java/lang/http_url_using_user_input/test.js @@ -0,0 +1,18 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("http_url_using_user_input", () => { + const testCase = "main.java" + + const results = invoke(testCase) + + expect(results.Missing).toEqual([]) + expect(results.Extra).toEqual([]) + }) +}) \ No newline at end of file diff --git a/tests/java/lang/http_url_using_user_input/testdata/main.java b/tests/java/lang/http_url_using_user_input/testdata/main.java new file mode 100644 index 000000000..90c047683 --- /dev/null +++ b/tests/java/lang/http_url_using_user_input/testdata/main.java @@ -0,0 +1,41 @@ +import java.net.*; + +public class Foo { + private static final int TIMEOUT_IN_SECONDS = 20; + + public void bad(HttpServletRequest req, HttpServletResponse res) { + String dangerous = req.getParameter("someRandomUrl"); + // bearer:expected java_lang_http_url_using_user_input + new URL(dangerous).openConnection().getInputStream(); + // bearer:expected java_lang_http_url_using_user_input + new URL(dangerous).openConnection().getLastModified(); + // bearer:expected java_lang_http_url_using_user_input + new URL(dangerous).openStream(); + // bearer:expected java_lang_http_url_using_user_input + new URL(dangerous).getContent(); + // bearer:expected java_lang_http_url_using_user_input + new URL(dangerous).getContent(new Class[0]); + URL bad = new URL(dangerous); + // bearer:expected java_lang_http_url_using_user_input + bad.openConnection().connect(); + } + + public void bad2(HttpServletRequest req, HttpServletResponse res) { + // bearer:expected java_lang_http_url_using_user_input + new URL("http://safe.com").openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(req.getParameter("someRandomUrl"), 8080))).connect(); + + int port = url.getPort(); + port = port > 0 ? port : 443; + try (Socket s = ctx.getSocketFactory().createSocket()) { + // bearer:expected java_lang_http_url_using_user_input + InetSocketAddress socketAddress = new InetSocketAddress(req.getRequestURI(), port); + } + } + + public static void good(HttpServletRequest req, HttpServletResponse res) { + String encodedString = URLEncoder.encode(req.getRequestURI(), StandardCharsets.UTF_8); + new URL(encodedString).openConnection(); + + new URL("http://safe.com").openConnection(); + } +}