diff --git a/ruby/ql/lib/change-notes/2025-07-21-nethttprequest-improvements.md b/ruby/ql/lib/change-notes/2025-07-21-nethttprequest-improvements.md new file mode 100644 index 000000000000..7de3ed050e2f --- /dev/null +++ b/ruby/ql/lib/change-notes/2025-07-21-nethttprequest-improvements.md @@ -0,0 +1,7 @@ +--- +category: fix +--- +* Made the following changes to `NetHttpRequest` + * Adds `connectionNode`, like other Ruby HTTP clients + * Makes `requestNode` and `connectionNode` public so subclasses can use them + * Adds detection of `Net::HTTP.start`, a common way to make HTTP requests in Ruby diff --git a/ruby/ql/lib/codeql/ruby/frameworks/http_clients/NetHttp.qll b/ruby/ql/lib/codeql/ruby/frameworks/http_clients/NetHttp.qll index 3a0b484e5465..0b4156ff813d 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/http_clients/NetHttp.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/http_clients/NetHttp.qll @@ -12,15 +12,22 @@ private import codeql.ruby.DataFlow /** * A `Net::HTTP` call which initiates an HTTP request. * ```ruby + * # one-off request * Net::HTTP.get("http://example.com/") * Net::HTTP.post("http://example.com/", "some_data") * req = Net::HTTP.new("example.com") * response = req.get("/") + * + * # connection re-use + * Net::HTTP.start("http://example.com") do |http| + * http.get("/") + * end * ``` */ class NetHttpRequest extends Http::Client::Request::Range instanceof DataFlow::CallNode { private DataFlow::CallNode request; - private API::Node requestNode; + API::Node requestNode; + API::Node connectionNode; private boolean returnsResponseBody; NetHttpRequest() { @@ -30,20 +37,27 @@ class NetHttpRequest extends Http::Client::Request::Range instanceof DataFlow::C | // Net::HTTP.get(...) method in ["get", "get_response"] and - requestNode = API::getTopLevelMember("Net").getMember("HTTP").getReturn(method) and + connectionNode = API::getTopLevelMember("Net").getMember("HTTP") and + requestNode = connectionNode.getReturn(method) and returnsResponseBody = true or // Net::HTTP.post(...).body method in ["post", "post_form"] and - requestNode = API::getTopLevelMember("Net").getMember("HTTP").getReturn(method) and + connectionNode = API::getTopLevelMember("Net").getMember("HTTP") and + requestNode = connectionNode.getReturn(method) and returnsResponseBody = false or // Net::HTTP.new(..).get(..).body + // Net::HTTP.start(..) do |http| http.get(..) end method in [ "get", "get2", "request_get", "head", "head2", "request_head", "delete", "put", "patch", "post", "post2", "request_post", "request" ] and - requestNode = API::getTopLevelMember("Net").getMember("HTTP").getInstance().getReturn(method) and + connectionNode = [ + API::getTopLevelMember("Net").getMember("HTTP").getInstance(), + API::getTopLevelMember("Net").getMember("HTTP").getMethod("start").getBlock().getParameter(0) + ] and + requestNode = connectionNode.getReturn(method) and returnsResponseBody = false ) } diff --git a/ruby/ql/test/library-tests/frameworks/http_clients/HttpClients.expected b/ruby/ql/test/library-tests/frameworks/http_clients/HttpClients.expected index f6275da34ace..e3a36c04819d 100644 --- a/ruby/ql/test/library-tests/frameworks/http_clients/HttpClients.expected +++ b/ruby/ql/test/library-tests/frameworks/http_clients/HttpClients.expected @@ -46,6 +46,8 @@ httpRequests | NetHttp.rb:16:6:16:19 | call to patch | | NetHttp.rb:24:3:24:33 | call to get | | NetHttp.rb:29:1:29:32 | call to post | +| NetHttp.rb:33:1:33:22 | call to request | +| NetHttp.rb:36:3:36:15 | call to get | | OpenURI.rb:3:9:3:41 | call to open | | OpenURI.rb:6:9:6:34 | call to open | | OpenURI.rb:9:9:9:38 | call to open | @@ -123,6 +125,8 @@ getFramework | NetHttp.rb:16:6:16:19 | call to patch | Net::HTTP | | NetHttp.rb:24:3:24:33 | call to get | Net::HTTP | | NetHttp.rb:29:1:29:32 | call to post | Net::HTTP | +| NetHttp.rb:33:1:33:22 | call to request | Net::HTTP | +| NetHttp.rb:36:3:36:15 | call to get | Net::HTTP | | OpenURI.rb:3:9:3:41 | call to open | OpenURI | | OpenURI.rb:6:9:6:34 | call to open | OpenURI | | OpenURI.rb:9:9:9:38 | call to open | OpenURI | @@ -292,6 +296,9 @@ getAUrlPart | NetHttp.rb:24:3:24:33 | call to get | NetHttp.rb:24:17:24:22 | domain | | NetHttp.rb:24:3:24:33 | call to get | NetHttp.rb:24:29:24:32 | path | | NetHttp.rb:29:1:29:32 | call to post | NetHttp.rb:29:16:29:18 | uri | +| NetHttp.rb:33:1:33:22 | call to request | NetHttp.rb:31:22:31:42 | "https://example.com" | +| NetHttp.rb:33:1:33:22 | call to request | NetHttp.rb:33:14:33:21 | root_get | +| NetHttp.rb:36:3:36:15 | call to get | NetHttp.rb:36:12:36:14 | "/" | | OpenURI.rb:3:9:3:41 | call to open | OpenURI.rb:3:21:3:40 | "http://example.com" | | OpenURI.rb:6:9:6:34 | call to open | OpenURI.rb:6:14:6:33 | "http://example.com" | | OpenURI.rb:9:9:9:38 | call to open | OpenURI.rb:9:18:9:37 | "http://example.com" | diff --git a/ruby/ql/test/library-tests/frameworks/http_clients/NetHttp.rb b/ruby/ql/test/library-tests/frameworks/http_clients/NetHttp.rb index 608b46ece9aa..63f1522dfad4 100644 --- a/ruby/ql/test/library-tests/frameworks/http_clients/NetHttp.rb +++ b/ruby/ql/test/library-tests/frameworks/http_clients/NetHttp.rb @@ -27,3 +27,11 @@ def get(domain, path) get("example.com", "/").body Net::HTTP.post(uri, "some_body") # note: response body not accessed + +http = Net::HTTP.new("https://example.com") +root_get = Net::HTTP::Get.new("/") +http.request(root_get) + +Net::HTTP.start("https://example.com") do |http| + http.get("/") +end