diff --git a/rules/python/lang/http_url_using_user_input.yml b/rules/python/lang/http_url_using_user_input.yml new file mode 100644 index 00000000..41765ee6 --- /dev/null +++ b/rules/python/lang/http_url_using_user_input.yml @@ -0,0 +1,632 @@ +languages: + - python +imports: + - python_shared_common_user_input + - python_shared_lang_instance + - python_shared_lang_import1 + - python_shared_lang_import2 +patterns: + - pattern: $($<...>$$<...>) + filters: + - variable: CONNECTION_CLASS + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [http] + - variable: MODULE2 + values: [client] + - variable: NAME + values: + - HTTPConnection + - HTTPSConnection + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($<...>$$<...>) + filters: + - variable: CONNECTION + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [http] + - variable: MODULE2 + values: [client] + - variable: NAME + values: + - HTTPConnection + - HTTPSConnection + - variable: METHOD + values: + - request + - set_tunnel + - putrequest + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: URLOPEN + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib] + - variable: MODULE2 + values: [request] + - variable: NAME + values: [urlopen] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: REQUEST_CLASS + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib] + - variable: MODULE2 + values: [request] + - variable: NAME + values: [Request] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$ = $ + filters: + - variable: REQUEST + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib] + - variable: MODULE2 + values: [request] + - variable: NAME + values: [Request] + - variable: ATTRIBUTE + values: + - full_url + - type + - host + - selector + - method + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [requests] + - variable: NAME + values: + - get + - post + - put + - delete + - head + - options + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: REQUEST + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [requests] + - variable: NAME + values: [request, Request] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($$<...>) + filters: + - variable: SESSION + detection: python_lang_http_url_using_user_input_requests_session + scope: cursor + - variable: METHOD + values: + - get + - post + - put + - delete + - head + - options + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.request($<_>, $$<...>) + filters: + - variable: SESSION + detection: python_lang_http_url_using_user_input_requests_session + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [httpx] + - variable: NAME + values: + - get + - options + - head + - post + - put + - delete + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [httpx] + - variable: NAME + values: + - request + - stream + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<...>base_url=$$<...>) + filters: + - variable: CLIENT_CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [httpx] + - variable: NAME + values: [Client] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($$<...>) + filters: + - variable: CLIENT + detection: python_lang_http_url_using_user_input_httpx_client + scope: cursor + - variable: METHOD + values: + - get + - options + - head + - post + - put + - delete + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($<_>, $$<...>) + filters: + - variable: CLIENT + detection: python_lang_http_url_using_user_input_httpx_client + scope: cursor + - variable: METHOD + values: + - request + - stream + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [aiohttp] + - variable: NAME + values: [request] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: SESSION_CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [aiohttp] + - variable: NAME + values: [ClientSession] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($$<...>) + filters: + - variable: SESSION + detection: python_lang_http_url_using_user_input_aiohttp_session + scope: cursor + - variable: METHOD + values: + - get + - put + - post + - delete + - head + - options + - patch + - ws_connect + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.request($<_>, $$<...>) + filters: + - variable: SESSION + detection: python_lang_http_url_using_user_input_aiohttp_session + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.request($$<...>) + filters: + - variable: HTTP + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [httplib2] + - variable: NAME + values: [Http] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: REQUEST + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: NAME + values: [request] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: PROXY_CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: NAME + values: [ProxyManager] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($$<...>) + filters: + - variable: MANAGER + detection: python_lang_http_url_using_user_input_urllib3_manager + scope: cursor + - variable: METHOD + values: + - connection_from_host + - connection_from_url + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($<_>, $$<...>) + filters: + - variable: MANAGER + detection: python_lang_http_url_using_user_input_urllib3_manager + scope: cursor + - variable: METHOD + values: + - connection_from_host + - request + - request_encode_body + - request_encode_url + - urlopen + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.proxy = $ + filters: + - variable: MANAGER + detection: python_lang_http_url_using_user_input_urllib3_manager + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: POOL_CLASS + detection: python_lang_http_url_using_user_input_urllib3_pool_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: POOL_CLASS + detection: python_lang_http_url_using_user_input_urllib3_pool_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: MODULE2 + values: [connectionpool] + - variable: NAME + values: [connection_from_url] + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.urlopen($<_>, $$<...>) + filters: + - variable: POOL + detection: python_lang_http_url_using_user_input_urllib3_pool + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($$<...>) + filters: + - variable: CONNECTION_CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<_>, $$<...>) + filters: + - variable: CONNECTION_CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $($<...>proxy=$$<...>) + filters: + - variable: CONNECTION_CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.$($<_>, $$<...>) + filters: + - variable: CONNECTION + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: METHOD + values: + - request + - request_chunked + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.set_tunnel($$<...>) + filters: + - variable: CONNECTION + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result + - pattern: $.set_tunnel($<_>, $$<...>) + filters: + - variable: CONNECTION + detection: python_shared_lang_instance + scope: cursor + filters: + - variable: CLASS + detection: python_lang_http_url_using_user_input_urllib3_connection_class + scope: cursor + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: result +auxiliary: + - id: python_lang_http_url_using_user_input_requests_session + patterns: + - pattern: $ + filters: + - variable: SESSION + detection: python_shared_lang_instance + scope: cursor_strict + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [requests] + - variable: NAME + values: [Session] + - id: python_lang_http_url_using_user_input_httpx_client + patterns: + - pattern: $ + filters: + - variable: CLIENT + detection: python_shared_lang_instance + scope: cursor_strict + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [httpx] + - variable: NAME + values: [Client] + - id: python_lang_http_url_using_user_input_aiohttp_session + patterns: + - pattern: $ + filters: + - variable: SESSION + detection: python_shared_lang_instance + scope: cursor_strict + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [aiohttp] + - variable: NAME + values: [ClientSession] + - id: python_lang_http_url_using_user_input_urllib3_manager + patterns: + - pattern: $ + filters: + - variable: MANAGER + detection: python_shared_lang_instance + scope: cursor_strict + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: NAME + values: + - PoolManager + - ProxyManager + - id: python_lang_http_url_using_user_input_urllib3_pool + patterns: + - pattern: $.$($<...>) + filters: + - variable: MANAGER + detection: python_lang_http_url_using_user_input_urllib3_manager + scope: cursor + - variable: METHOD + values: + - connection_from_context + - connection_from_host + - connection_from_pool_key + - connection_from_url + - pattern: $ + filters: + - variable: POOL + detection: python_shared_lang_instance + scope: cursor_strict + filters: + - variable: CLASS + detection: python_lang_http_url_using_user_input_urllib3_pool_class + scope: cursor + - pattern: $($<...>) + filters: + - variable: FUNCTION + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: MODULE2 + values: [connectionpool] + - variable: NAME + values: [connection_from_url] + - id: python_lang_http_url_using_user_input_urllib3_pool_class + patterns: + - pattern: $ + filters: + - variable: CLASS + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: NAME + values: + - HTTPConnectionPool + - HTTPSConnectionPool + - id: python_lang_http_url_using_user_input_urllib3_connection_class + patterns: + - pattern: $ + filters: + - variable: CLASS + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [urllib3] + - variable: MODULE2 + values: [connection] + - variable: NAME + values: + - HTTPConnection + - HTTPSConnection +severity: high +metadata: + description: "Unsanitized user input in HTTP request (SSRF)" + remediation_message: |- + ## Description + + Your application is vulnerable to Server-Side Request Forgery (SSRF) attacks when it connects to URLs that include user-supplied data. This vulnerability occurs because attackers can manipulate these URLs to force your application to make unintended requests to internal or external resources. + + ## Remediations + + - **Do not** directly include user input in HTTP URLs. This practice can lead to SSRF vulnerabilities, where attackers exploit the application to send requests to unintended destinations. + ```python + host = request.GET["host"] + + urllib.request.urlopen(f"https://{host}") # unsafe + ``` + - **Do** validate or map user input against a predefined list of safe values before using it to form URLs. This approach ensures that the application only connects to intended and safe locations. + ```python + host = "api1.com" if request.GET["host"] == "option1" else "api2.com" + + urllib.request.urlopen(f"https://{host}") + ``` + + ## References + + - [OWASP - Server-Side Request Forgery (SSRF) prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) + cwe_id: + - 918 + id: python_lang_http_url_using_user_input + documentation_url: https://docs.bearer.com/reference/rules/python_lang_http_url_using_user_input diff --git a/rules/python/shared/common/html_user_input.yaml b/rules/python/shared/common/html_user_input.yaml deleted file mode 100644 index 7c80acfd..00000000 --- a/rules/python/shared/common/html_user_input.yaml +++ /dev/null @@ -1,40 +0,0 @@ -type: shared -languages: - - python -imports: - - python_shared_lang_import1 - - python_shared_lang_instance - - python_shared_common_user_input - - python_shared_django_html_sanitizer -sanitizer: python_shared_common_html_user_input_sanitizer -patterns: - - pattern: $ - filters: - - variable: USER_INPUT - detection: python_shared_common_user_input - scope: cursor -auxiliary: - - id: python_shared_common_html_user_input_sanitizer - patterns: - - pattern: $ - filters: - - variable: DJANGO_SANITIZER - detection: python_shared_django_html_sanitizer - scope: cursor_strict - - pattern: $.sanitize($<_>) - filters: - - variable: SANITIZER - detection: python_shared_lang_instance - scope: cursor - filters: - - variable: CLASS - detection: python_shared_lang_import1 - scope: cursor - filters: - - variable: MODULE1 - values: [html_sanitizer] - - variable: NAME - values: [Sanitizer] -metadata: - description: "Python HTML user input." - id: python_shared_common_html_user_input diff --git a/rules/python/shared/lang/user_input.yml b/rules/python/shared/lang/user_input.yml index c4751f61..dd85a141 100644 --- a/rules/python/shared/lang/user_input.yml +++ b/rules/python/shared/lang/user_input.yml @@ -1,8 +1,6 @@ type: shared languages: - python -imports: - - python_shared_lang_instance patterns: - input($<...>) metadata: diff --git a/tests/python/lang/http_url_using_user_input/test.js b/tests/python/lang/http_url_using_user_input/test.js new file mode 100644 index 00000000..90e7dba9 --- /dev/null +++ b/tests/python/lang/http_url_using_user_input/test.js @@ -0,0 +1,20 @@ +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.py" + + const results = invoke(testCase) + + expect(results).toEqual({ + Missing: [], + Extra: [] + }) + }) +}) \ No newline at end of file diff --git a/tests/python/lang/http_url_using_user_input/testdata/main.py b/tests/python/lang/http_url_using_user_input/testdata/main.py new file mode 100644 index 00000000..d3108d82 --- /dev/null +++ b/tests/python/lang/http_url_using_user_input/testdata/main.py @@ -0,0 +1,192 @@ +user_input = input() + +def http_client(): + import http.client + + conn = http.client.HTTPSConnection("ok") + # bearer:expected python_lang_http_url_using_user_input + conn = http.client.HTTPSConnection(user_input) + + conn.request("GET", "/") + # bearer:expected python_lang_http_url_using_user_input + conn.request("GET", user_input) + + conn.set_tunnel("ok", 42) + # bearer:expected python_lang_http_url_using_user_input + conn.set_tunnel(user_input, 42) + + conn.putrequest("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + conn.putrequest("GET", user_input) + + +def urllib(): + from urllib import request as ur + import urllib.request + + ur.urlopen("ok", data) + # bearer:expected python_lang_http_url_using_user_input + ur.urlopen(user_input, data) + + req = urllib.request.Request("ok", data) + # bearer:expected python_lang_http_url_using_user_input + req = urllib.request.Request(user_input, data) + + req.full_url = "ok" + # bearer:expected python_lang_http_url_using_user_input + req.full_url = user_input + + +def requests(): + from requests import get, post + import requests + + r = requests.get("ok") + # bearer:expected python_lang_http_url_using_user_input + r = requests.get(user_input) + + r = requests.post("ok", data={'key': 'value'}) + # bearer:expected python_lang_http_url_using_user_input + r = requests.post(user_input, data={'key': 'value'}) + + r = requests.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + r = requests.request("GET", user_input) + + req = requests.Request('GET', "ok") + # bearer:expected python_lang_http_url_using_user_input + req = requests.Request('GET', user_input) + + with requests.Session() as s: + s.get("ok") + # bearer:expected python_lang_http_url_using_user_input + s.get(user_input) + + s.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + s.request("GET", user_input) + + +def httpx(): + import httpx + + response = httpx.get("ok") + # bearer:expected python_lang_http_url_using_user_input + response = httpx.get(user_input) + + httpx.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + httpx.request("GET", user_input) + + client = httpx.Client(base_url="ok") + # bearer:expected python_lang_http_url_using_user_input + client = httpx.Client(base_url=user_input) + + client.get("ok") + # bearer:expected python_lang_http_url_using_user_input + client.get(user_input) + + client.stream("POST", "ok") + # bearer:expected python_lang_http_url_using_user_input + client.stream("POST", user_input) + + +async def aiohttp(url): + import aiohttp + + aiohttp.ClientSession("ok", cookies={}) + # bearer:expected python_lang_http_url_using_user_input + aiohttp.ClientSession(user_input, cookies={}) + + async with aiohttp.ClientSession() as session: + await session.get("ok") + # bearer:expected python_lang_http_url_using_user_input + await session.get(user_input) + + await session.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + await session.request("GET", user_input) + + await aiohttp.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + await aiohttp.request("GET", user_input) + + +def httplib2(): + import httplib2 + h = httplib2.Http(".cache") + + h.request("ok", "GET") + # bearer:expected python_lang_http_url_using_user_input + h.request(user_input, "GET") + + +def urllib3(): + import urllib3 + + urllib3.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + urllib3.request("GET", user_input) + + http = urllib3.PoolManager() + http.request('GET', "ok") + # bearer:expected python_lang_http_url_using_user_input + http.request('GET', user_input) + + http2 = urllib3.ProxyManager("ok", 42) + # bearer:expected python_lang_http_url_using_user_input + http2 = urllib3.ProxyManager(user_input, 42) + + pool = http2.connection_from_host("ok", 42) + # bearer:expected python_lang_http_url_using_user_input + pool = http2.connection_from_host(user_input, 42) + # bearer:expected python_lang_http_url_using_user_input + pool = http2.connection_from_host("ok", user_input) + + http.connection_from_url("ok", {}) + # bearer:expected python_lang_http_url_using_user_input + http.connection_from_url(user_input, {}) + + http.proxy = "ok" + # bearer:expected python_lang_http_url_using_user_input + http.proxy = user_input + + pool2 = urllib3.HTTPConnectionPool("ok", 42) #HTTPSConnectionPool + # bearer:expected python_lang_http_url_using_user_input + pool2 = urllib3.HTTPConnectionPool(user_input, 42) + # bearer:expected python_lang_http_url_using_user_input + pool2 = urllib3.HTTPConnectionPool("ok", user_input) + + pool3 = urllib3.connectionpool.connection_from_url("ok", {}) + # bearer:expected python_lang_http_url_using_user_input + pool3 = urllib3.connectionpool.connection_from_url(user_input, {}) + + pool.urlopen('GET', "ok") + # bearer:expected python_lang_http_url_using_user_input + pool.urlopen('GET', user_input) + # bearer:expected python_lang_http_url_using_user_input + pool2.urlopen('GET', user_input) + # bearer:expected python_lang_http_url_using_user_input + pool3.urlopen('GET', user_input) + + connection = urllib3.connection.HTTPConnection("ok", 42) + # bearer:expected python_lang_http_url_using_user_input + connection = urllib3.connection.HTTPConnection(user_input, 42) + # bearer:expected python_lang_http_url_using_user_input + connection = urllib3.connection.HTTPConnection("ok", user_input) + # bearer:expected python_lang_http_url_using_user_input + connection = urllib3.connection.HTTPConnection("ok", proxy=user_input) + + connection.request("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + connection.request("GET", user_input) + + connection.request_chunked("GET", "ok") + # bearer:expected python_lang_http_url_using_user_input + connection.request_chunked("GET", user_input) + + connection.set_tunnel("ok", 42, {}) + # bearer:expected python_lang_http_url_using_user_input + connection.set_tunnel(user_input, 42, {}) + # bearer:expected python_lang_http_url_using_user_input + connection.set_tunnel("ok", user_input, {}) \ No newline at end of file