From d729ab501b0d7ed30e2a6e3ea9c14dcd96245973 Mon Sep 17 00:00:00 2001
From: Asger F
Date: Fri, 12 Sep 2025 08:45:18 +0200
Subject: [PATCH 01/90] JS: Add test that calls .json or .jsonp
---
.../frameworks/Express/src/json.js | 10 +++
.../frameworks/Express/tests.expected | 73 +++++++++++++++++++
2 files changed, 83 insertions(+)
create mode 100644 javascript/ql/test/library-tests/frameworks/Express/src/json.js
diff --git a/javascript/ql/test/library-tests/frameworks/Express/src/json.js b/javascript/ql/test/library-tests/frameworks/Express/src/json.js
new file mode 100644
index 00000000000..6b87cad68cf
--- /dev/null
+++ b/javascript/ql/test/library-tests/frameworks/Express/src/json.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const app = express();
+
+app.get('/test/json', function(req, res) {
+ res.json(req.query.data);
+});
+
+app.get('/test/jsonp', function(req, res) {
+ res.jsonp(req.query.data);
+});
diff --git a/javascript/ql/test/library-tests/frameworks/Express/tests.expected b/javascript/ql/test/library-tests/frameworks/Express/tests.expected
index ec4253740f7..0abc2dbdf95 100644
--- a/javascript/ql/test/library-tests/frameworks/Express/tests.expected
+++ b/javascript/ql/test/library-tests/frameworks/Express/tests.expected
@@ -131,6 +131,12 @@ test_isRequest
| src/inheritedFromNode.js:4:24:4:26 | req |
| src/inheritedFromNode.js:4:24:4:26 | req |
| src/inheritedFromNode.js:7:2:7:4 | req |
+| src/json.js:4:32:4:34 | req |
+| src/json.js:4:32:4:34 | req |
+| src/json.js:5:14:5:16 | req |
+| src/json.js:8:33:8:35 | req |
+| src/json.js:8:33:8:35 | req |
+| src/json.js:9:15:9:17 | req |
| src/middleware-flow.js:5:20:5:22 | req |
| src/middleware-flow.js:5:20:5:22 | req |
| src/middleware-flow.js:6:5:6:7 | req |
@@ -201,6 +207,8 @@ test_RouteSetup
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() | false |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() | false |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:2:11:2:19 | express() | false |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() | false |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() | false |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:2:13:2:21 | express() | true |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:2:13:2:21 | express() | false |
| src/middleware-flow.js:39:1:43:2 | unrelat ... .db;\\n}) | src/middleware-flow.js:37:22:37:30 | express() | false |
@@ -345,6 +353,14 @@ test_isResponse
| src/inheritedFromNode.js:4:29:4:31 | res |
| src/inheritedFromNode.js:5:2:5:4 | res |
| src/inheritedFromNode.js:6:2:6:4 | res |
+| src/json.js:4:37:4:39 | res |
+| src/json.js:4:37:4:39 | res |
+| src/json.js:5:5:5:7 | res |
+| src/json.js:5:5:5:28 | res.jso ... y.data) |
+| src/json.js:8:38:8:40 | res |
+| src/json.js:8:38:8:40 | res |
+| src/json.js:9:5:9:7 | res |
+| src/json.js:9:5:9:29 | res.jso ... y.data) |
| src/middleware-flow.js:5:25:5:27 | res |
| src/middleware-flow.js:17:30:17:32 | res |
| src/middleware-flow.js:23:23:23:25 | res |
@@ -575,6 +591,12 @@ test_RequestExpr
| src/inheritedFromNode.js:4:24:4:26 | req | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
| src/inheritedFromNode.js:4:24:4:26 | req | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
| src/inheritedFromNode.js:7:2:7:4 | req | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:32:4:34 | req | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:4:32:4:34 | req | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:5:14:5:16 | req | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:33:8:35 | req | src/json.js:8:24:10:1 | functio ... ata);\\n} |
+| src/json.js:8:33:8:35 | req | src/json.js:8:24:10:1 | functio ... ata);\\n} |
+| src/json.js:9:15:9:17 | req | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:5:20:5:22 | req | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:5:20:5:22 | req | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:6:5:6:7 | req | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
@@ -627,6 +649,7 @@ test_appCreation
| src/express4.js:2:11:2:19 | express() |
| src/express.js:2:11:2:19 | express() |
| src/inheritedFromNode.js:2:11:2:19 | express() |
+| src/json.js:2:13:2:21 | express() |
| src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:37:22:37:30 | express() |
| src/params.js:2:11:2:19 | express() |
@@ -820,6 +843,14 @@ test_ResponseExpr
| src/inheritedFromNode.js:4:29:4:31 | res | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
| src/inheritedFromNode.js:5:2:5:4 | res | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
| src/inheritedFromNode.js:6:2:6:4 | res | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:37:4:39 | res | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:4:37:4:39 | res | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:5:5:5:7 | res | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:5:5:5:28 | res.jso ... y.data) | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:38:8:40 | res | src/json.js:8:24:10:1 | functio ... ata);\\n} |
+| src/json.js:8:38:8:40 | res | src/json.js:8:24:10:1 | functio ... ata);\\n} |
+| src/json.js:9:5:9:7 | res | src/json.js:8:24:10:1 | functio ... ata);\\n} |
+| src/json.js:9:5:9:29 | res.jso ... y.data) | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:5:25:5:27 | res | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:17:30:17:32 | res | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:23:23:23:25 | res | src/middleware-flow.js:23:17:23:41 | (req, r ... q.db; } |
@@ -940,6 +971,8 @@ test_RouteHandler
| src/express.js:65:27:69:1 | functio ... res);\\n} | src/express.js:65:36:65:38 | req | src/express.js:65:41:65:43 | res |
| src/express.js:71:23:75:1 | functio ... res);\\n} | src/express.js:71:32:71:34 | req | src/express.js:71:37:71:39 | res |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:24:4:26 | req | src/inheritedFromNode.js:4:29:4:31 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:32:4:34 | req | src/json.js:4:37:4:39 | res |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:33:8:35 | req | src/json.js:8:38:8:40 | res |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:5:20:5:22 | req | src/middleware-flow.js:5:25:5:27 | res |
| src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } | src/middleware-flow.js:17:25:17:27 | req | src/middleware-flow.js:17:30:17:32 | res |
| src/middleware-flow.js:23:17:23:41 | (req, r ... q.db; } | src/middleware-flow.js:23:18:23:20 | req | src/middleware-flow.js:23:23:23:25 | res |
@@ -1036,6 +1069,8 @@ test_RouteHandlerExpr
| src/express.js:65:27:69:1 | functio ... res);\\n} | src/express.js:65:1:69:2 | app.get ... es);\\n}) | true |
| src/express.js:71:23:75:1 | functio ... res);\\n} | src/express.js:71:1:75:2 | app.get ... es);\\n}) | true |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | true |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:1:6:2 | app.get ... ta);\\n}) | true |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:1:10:2 | app.get ... ta);\\n}) | true |
| src/middleware-flow.js:13:16:13:24 | installDb | src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | false |
| src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } | src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | true |
| src/middleware-flow.js:27:23:27:32 | routers[p] | src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | true |
@@ -1068,6 +1103,7 @@ test_isRouterCreation
| src/express4.js:2:11:2:19 | express() |
| src/express.js:2:11:2:19 | express() |
| src/inheritedFromNode.js:2:11:2:19 | express() |
+| src/json.js:2:13:2:21 | express() |
| src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:37:22:37:30 | express() |
| src/params.js:2:11:2:19 | express() |
@@ -1111,6 +1147,8 @@ test_RequestInputAccess
| src/express.js:67:12:67:25 | req.params.foo | parameter | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:73:12:73:19 | req.path | url | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:7:2:7:8 | req.url | url | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:5:14:5:27 | req.query.data | parameter | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:9:15:9:28 | req.query.data | parameter | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/params.js:4:35:4:39 | value | parameter | src/params.js:4:18:12:1 | (req, r ... }\\n} |
| src/params.js:5:17:5:28 | req.query.xx | parameter | src/params.js:4:18:12:1 | (req, r ... }\\n} |
| src/params.js:6:17:6:24 | req.body | body | src/params.js:4:18:12:1 | (req, r ... }\\n} |
@@ -1182,6 +1220,8 @@ test_RouteSetup_getRouter
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:2:11:2:19 | express() |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | src/middleware-flow.js:2:13:2:21 | express() |
@@ -1226,6 +1266,8 @@ test_RouteSetup_getServer
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:2:11:2:19 | express() |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:2:11:2:19 | express() |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:2:13:2:21 | express() |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:39:1:43:2 | unrelat ... .db;\\n}) | src/middleware-flow.js:37:22:37:30 | express() |
@@ -1266,6 +1308,8 @@ test_StandardRouteHandler
| src/express.js:65:27:69:1 | functio ... res);\\n} | src/express.js:2:11:2:19 | express() | src/express.js:65:36:65:38 | req | src/express.js:65:41:65:43 | res |
| src/express.js:71:23:75:1 | functio ... res);\\n} | src/express.js:2:11:2:19 | express() | src/express.js:71:32:71:34 | req | src/express.js:71:37:71:39 | res |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:2:11:2:19 | express() | src/inheritedFromNode.js:4:24:4:26 | req | src/inheritedFromNode.js:4:29:4:31 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:2:13:2:21 | express() | src/json.js:4:32:4:34 | req | src/json.js:4:37:4:39 | res |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:2:13:2:21 | express() | src/json.js:8:33:8:35 | req | src/json.js:8:38:8:40 | res |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:2:13:2:21 | express() | src/middleware-flow.js:5:20:5:22 | req | src/middleware-flow.js:5:25:5:27 | res |
| src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } | src/middleware-flow.js:2:13:2:21 | express() | src/middleware-flow.js:17:25:17:27 | req | src/middleware-flow.js:17:30:17:32 | res |
| src/middleware-flow.js:39:23:43:1 | (req, r ... s.db;\\n} | src/middleware-flow.js:37:22:37:30 | express() | src/middleware-flow.js:39:24:39:26 | req | src/middleware-flow.js:39:29:39:31 | res |
@@ -1346,6 +1390,8 @@ test_RouteHandlerExpr_getBody
| src/express.js:65:27:69:1 | functio ... res);\\n} | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:71:23:75:1 | functio ... res);\\n} | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:13:16:13:24 | installDb | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:39:23:43:1 | (req, r ... s.db;\\n} | src/middleware-flow.js:39:23:43:1 | (req, r ... s.db;\\n} |
@@ -1466,6 +1512,8 @@ test_RouteSetup_getARouteHandler
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | src/middleware-flow.js:23:17:23:41 | (req, r ... q.db; } |
@@ -1526,6 +1574,8 @@ test_RouteSetup_getRequestMethod
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | GET |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | GET |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | POST |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | GET |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | GET |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | GET |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | GET |
| src/middleware-flow.js:39:1:43:2 | unrelat ... .db;\\n}) | GET |
@@ -1699,6 +1749,12 @@ test_RouteHandler_getARequestExpr
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:24:4:26 | req |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:24:4:26 | req |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:7:2:7:4 | req |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:32:4:34 | req |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:32:4:34 | req |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:5:14:5:16 | req |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:33:8:35 | req |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:33:8:35 | req |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:9:15:9:17 | req |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:5:20:5:22 | req |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:5:20:5:22 | req |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:6:5:6:7 | req |
@@ -1909,6 +1965,14 @@ test_RouteHandler_getAResponseExpr
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:4:29:4:31 | res |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:5:2:5:4 | res |
| src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} | src/inheritedFromNode.js:6:2:6:4 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:37:4:39 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:4:37:4:39 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:5:5:5:7 | res |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | src/json.js:5:5:5:28 | res.jso ... y.data) |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:38:8:40 | res |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:8:38:8:40 | res |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:9:5:9:7 | res |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | src/json.js:9:5:9:29 | res.jso ... y.data) |
| src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} | src/middleware-flow.js:5:25:5:27 | res |
| src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } | src/middleware-flow.js:17:30:17:32 | res |
| src/middleware-flow.js:23:17:23:41 | (req, r ... q.db; } | src/middleware-flow.js:23:23:23:25 | res |
@@ -2041,6 +2105,8 @@ test_RouteSetup_getRouteHandlerExpr
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | 0 | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | 0 | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | 0 | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | 0 | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | 0 | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | 0 | src/middleware-flow.js:13:16:13:24 | installDb |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | 0 | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | 0 | src/middleware-flow.js:27:23:27:32 | routers[p] |
@@ -2149,6 +2215,8 @@ test_RouteSetup_getARouteHandlerExpr
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:13:16:13:24 | installDb |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | src/middleware-flow.js:27:23:27:32 | routers[p] |
@@ -2181,6 +2249,7 @@ test_RouterDefinition_RouterDefinition
| src/express4.js:2:11:2:19 | express() |
| src/express.js:2:11:2:19 | express() |
| src/inheritedFromNode.js:2:11:2:19 | express() |
+| src/json.js:2:13:2:21 | express() |
| src/middleware-flow.js:2:13:2:21 | express() |
| src/middleware-flow.js:37:22:37:30 | express() |
| src/params.js:2:11:2:19 | express() |
@@ -2216,6 +2285,8 @@ test_RouterDefinition_getARouteHandler
| src/express.js:2:11:2:19 | express() | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:2:11:2:19 | express() | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:2:11:2:19 | express() | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:2:13:2:21 | express() | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:2:13:2:21 | express() | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:2:13:2:21 | express() | src/middleware-flow.js:5:1:10:1 | functio ... xt();\\n} |
| src/middleware-flow.js:2:13:2:21 | express() | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:37:22:37:30 | express() | src/middleware-flow.js:39:23:43:1 | (req, r ... s.db;\\n} |
@@ -2334,6 +2405,8 @@ test_RouteSetup_getLastRouteHandlerExpr
| src/express.js:65:1:69:2 | app.get ... es);\\n}) | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:71:1:75:2 | app.get ... es);\\n}) | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:4:1:6:2 | app.get ... ta);\\n}) | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:8:1:10:2 | app.get ... ta);\\n}) | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/middleware-flow.js:13:5:13:25 | router. ... tallDb) | src/middleware-flow.js:13:16:13:24 | installDb |
| src/middleware-flow.js:17:5:21:6 | router. ... \\n }) | src/middleware-flow.js:17:24:21:5 | (req, r ... ;\\n } |
| src/middleware-flow.js:27:9:27:33 | router. ... ers[p]) | src/middleware-flow.js:27:23:27:32 | routers[p] |
From 132a8b8b53bd8ce115d0641c8b11d47a0e0648e2 Mon Sep 17 00:00:00 2001
From: Asger F
Date: Fri, 12 Sep 2025 08:46:21 +0200
Subject: [PATCH 02/90] JS: Model json and jsonp methods
---
.../semmle/javascript/frameworks/Express.qll | 34 +++++++++++++++++++
.../frameworks/Express/tests.expected | 12 +++++++
2 files changed, 46 insertions(+)
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Express.qll b/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
index 8c016b3afe9..41e4d1c860c 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
@@ -781,6 +781,40 @@ module Express {
override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
}
+ /**
+ * A call to `res.json()` or `res.jsonp()`.
+ *
+ * This sets the `content-type` header.
+ */
+ private class ResponseJsonCall extends DataFlow::MethodCallNode, Http::HeaderDefinition {
+ private ResponseSource response;
+
+ ResponseJsonCall() { this = response.ref().getAMethodCall(["json", "jsonp"]) }
+
+ override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
+
+ override string getAHeaderName() { result = "content-type" }
+
+ override predicate defines(string headerName, string headerValue) {
+ // Note: for `jsonp` the actual content-type header will be `text/javascript` or similar, but to avoid
+ // generating a spurious HTML injection sink, we treat it as `application/json` here.
+ headerName = "content-type" and headerValue = "application/json"
+ }
+ }
+
+ /**
+ * An argument passed to the `json` or `json` method of an HTTP response object.
+ */
+ private class ResponseJsonCallArgument extends Http::ResponseSendArgument {
+ ResponseJsonCall call;
+
+ ResponseJsonCallArgument() { this = call.getArgument(0) }
+
+ override RouteHandler getRouteHandler() { result = call.getRouteHandler() }
+
+ override HeaderDefinition getAnAssociatedHeaderDefinition() { result = call }
+ }
+
/**
* An invocation of the `cookie` method on an HTTP response object.
*/
diff --git a/javascript/ql/test/library-tests/frameworks/Express/tests.expected b/javascript/ql/test/library-tests/frameworks/Express/tests.expected
index 0abc2dbdf95..9007a1ed984 100644
--- a/javascript/ql/test/library-tests/frameworks/Express/tests.expected
+++ b/javascript/ql/test/library-tests/frameworks/Express/tests.expected
@@ -674,6 +674,8 @@ test_ResponseBody
| src/express.js:61:12:61:25 | req.params.foo | src/express.js:59:23:63:1 | functio ... res);\\n} |
| src/express.js:67:12:67:25 | req.params.foo | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:73:12:73:19 | req.path | src/express.js:71:23:75:1 | functio ... res);\\n} |
+| src/json.js:5:14:5:27 | req.query.data | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:9:15:9:28 | req.query.data | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/params.js:8:18:8:22 | value | src/params.js:4:18:12:1 | (req, r ... }\\n} |
| src/params.js:15:12:15:18 | "Hello" | src/params.js:14:24:16:1 | functio ... lo");\\n} |
test_ResponseExpr
@@ -1005,6 +1007,8 @@ test_HeaderDefinition
| src/express.js:66:3:66:42 | res.hea ... plain") | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:72:3:72:41 | res.hea ... /html") | src/express.js:71:23:75:1 | functio ... res);\\n} |
| src/inheritedFromNode.js:6:2:6:16 | res.setHeader() | src/inheritedFromNode.js:4:15:8:1 | functio ... .url;\\n} |
+| src/json.js:5:5:5:28 | res.jso ... y.data) | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:9:5:9:29 | res.jso ... y.data) | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/responseExprs.js:19:5:19:16 | res.append() | src/responseExprs.js:16:30:42:1 | functio ... }\\n} |
| src/responseExprs.js:37:5:37:28 | f(res.a ... ppend() | src/responseExprs.js:16:30:42:1 | functio ... }\\n} |
| src/responseExprs.js:37:7:37:18 | res.append() | src/responseExprs.js:16:30:42:1 | functio ... }\\n} |
@@ -1163,6 +1167,8 @@ test_ResponseSendArgument
| src/express.js:61:12:61:25 | req.params.foo | src/express.js:59:23:63:1 | functio ... res);\\n} |
| src/express.js:67:12:67:25 | req.params.foo | src/express.js:65:27:69:1 | functio ... res);\\n} |
| src/express.js:73:12:73:19 | req.path | src/express.js:71:23:75:1 | functio ... res);\\n} |
+| src/json.js:5:14:5:27 | req.query.data | src/json.js:4:23:6:1 | functio ... ata);\\n} |
+| src/json.js:9:15:9:28 | req.query.data | src/json.js:8:24:10:1 | functio ... ata);\\n} |
| src/params.js:8:18:8:22 | value | src/params.js:4:18:12:1 | (req, r ... }\\n} |
| src/params.js:15:12:15:18 | "Hello" | src/params.js:14:24:16:1 | functio ... lo");\\n} |
test_RouteSetup_getRouter
@@ -1366,6 +1372,8 @@ test_HeaderDefinition_defines
| src/express.js:60:3:60:47 | res.hea ... n/xml") | content-type | application/xml |
| src/express.js:66:3:66:42 | res.hea ... plain") | content-type | text/plain |
| src/express.js:72:3:72:41 | res.hea ... /html") | content-type | text/html |
+| src/json.js:5:5:5:28 | res.jso ... y.data) | content-type | application/json |
+| src/json.js:9:5:9:29 | res.jso ... y.data) | content-type | application/json |
test_RouteHandlerExpr_getBody
| src/advanced-routehandler-registration.js:51:9:51:60 | (req, r ... tever") | src/advanced-routehandler-registration.js:51:9:51:60 | (req, r ... tever") |
| src/advanced-routehandler-registration.js:64:9:64:53 | (req, r ... q, res) | src/advanced-routehandler-registration.js:64:9:64:53 | (req, r ... q, res) |
@@ -2139,6 +2147,8 @@ test_HeaderDefinition_getAHeaderName
| src/express.js:60:3:60:47 | res.hea ... n/xml") | content-type |
| src/express.js:66:3:66:42 | res.hea ... plain") | content-type |
| src/express.js:72:3:72:41 | res.hea ... /html") | content-type |
+| src/json.js:5:5:5:28 | res.jso ... y.data) | content-type |
+| src/json.js:9:5:9:29 | res.jso ... y.data) | content-type |
test_RouteHandlerExpr_getAsSubRouter
| src/csurf-example.js:13:17:13:19 | api | src/csurf-example.js:30:16:30:35 | new express.Router() |
| src/express2.js:6:9:6:14 | router | src/express2.js:2:14:2:23 | e.Router() |
@@ -2155,6 +2165,8 @@ test_RouteHandler_getAResponseHeader
| src/express.js:65:27:69:1 | functio ... res);\\n} | content-type | src/express.js:66:3:66:42 | res.hea ... plain") |
| src/express.js:71:23:75:1 | functio ... res);\\n} | access-control-allow-credentials | src/express.js:12:3:12:54 | arg.hea ... , true) |
| src/express.js:71:23:75:1 | functio ... res);\\n} | content-type | src/express.js:72:3:72:41 | res.hea ... /html") |
+| src/json.js:4:23:6:1 | functio ... ata);\\n} | content-type | src/json.js:5:5:5:28 | res.jso ... y.data) |
+| src/json.js:8:24:10:1 | functio ... ata);\\n} | content-type | src/json.js:9:5:9:29 | res.jso ... y.data) |
test_RouteSetup_getARouteHandlerExpr
| src/advanced-routehandler-registration.js:10:3:10:24 | app.get ... es0[p]) | src/advanced-routehandler-registration.js:10:14:10:23 | routes0[p] |
| src/advanced-routehandler-registration.js:19:3:19:18 | app.use(handler) | src/advanced-routehandler-registration.js:19:11:19:17 | handler |
From d295acc3c33268fe10fa5aaf20fde43ef900c59d Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 12 Sep 2025 19:22:05 -0400
Subject: [PATCH 03/90] Add initial support for Ruby Grape
---
ruby/ql/lib/codeql/ruby/Frameworks.qll | 1 +
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 198 ++++++++++++++++++
.../frameworks/grape/Grape.expected | 25 +++
.../library-tests/frameworks/grape/Grape.ql | 18 ++
.../library-tests/frameworks/grape/app.rb | 54 +++++
.../security/cwe-089/ArelInjection.rb | 9 +
.../security/cwe-089/SqlInjection.expected | 11 +
7 files changed, 316 insertions(+)
create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Grape.expected
create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Grape.ql
create mode 100644 ruby/ql/test/library-tests/frameworks/grape/app.rb
diff --git a/ruby/ql/lib/codeql/ruby/Frameworks.qll b/ruby/ql/lib/codeql/ruby/Frameworks.qll
index 9bc01874710..e8009c91b7d 100644
--- a/ruby/ql/lib/codeql/ruby/Frameworks.qll
+++ b/ruby/ql/lib/codeql/ruby/Frameworks.qll
@@ -21,6 +21,7 @@ private import codeql.ruby.frameworks.Rails
private import codeql.ruby.frameworks.Railties
private import codeql.ruby.frameworks.Stdlib
private import codeql.ruby.frameworks.Files
+private import codeql.ruby.frameworks.Grape
private import codeql.ruby.frameworks.HttpClients
private import codeql.ruby.frameworks.XmlParsing
private import codeql.ruby.frameworks.ActionDispatch
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
new file mode 100644
index 00000000000..8e9a062dc9a
--- /dev/null
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -0,0 +1,198 @@
+/**
+ * Provides modeling for the `Grape` API framework.
+ */
+
+private import codeql.ruby.AST
+private import codeql.ruby.Concepts
+private import codeql.ruby.controlflow.CfgNodes
+private import codeql.ruby.DataFlow
+private import codeql.ruby.dataflow.RemoteFlowSources
+private import codeql.ruby.ApiGraphs
+private import codeql.ruby.typetracking.TypeTracking
+private import codeql.ruby.frameworks.Rails
+private import codeql.ruby.frameworks.internal.Rails
+private import codeql.ruby.dataflow.internal.DataFlowDispatch
+
+/**
+ * Provides modeling for Grape, a REST-like API framework for Ruby.
+ * Grape allows you to build RESTful APIs in Ruby with minimal effort.
+ */
+module Grape {
+ /**
+ * A Grape API class which sits at the top of the class hierarchy.
+ * In other words, it does not subclass any other Grape API class in source code.
+ */
+ class RootAPI extends GrapeAPIClass {
+ RootAPI() {
+ not exists(GrapeAPIClass parent | this != parent and this = parent.getADescendent())
+ }
+ }
+}
+
+/**
+ * A class that extends `Grape::API`.
+ * For example,
+ *
+ * ```rb
+ * class FooAPI < Grape::API
+ * get '/users' do
+ * name = params[:name]
+ * User.where("name = #{name}")
+ * end
+ * end
+ * ```
+ */
+class GrapeAPIClass extends DataFlow::ClassNode {
+ GrapeAPIClass() {
+ this = grapeAPIBaseClass().getADescendentModule() and
+ not exists(DataFlow::ModuleNode m | m = grapeAPIBaseClass().asModule() | this = m)
+ }
+
+ /**
+ * Gets a `GrapeEndpoint` defined in this class.
+ */
+ GrapeEndpoint getAnEndpoint() {
+ result.getAPIClass() = this
+ }
+
+ /**
+ * Gets a `self` that possibly refers to an instance of this class.
+ */
+ DataFlow::LocalSourceNode getSelf() {
+ result = this.getAnInstanceSelf()
+ or
+ // Include the module-level `self` to recover some cases where a block at the module level
+ // is invoked with an instance as the `self`.
+ result = this.getModuleLevelSelf()
+ }
+}
+
+private DataFlow::ConstRef grapeAPIBaseClass() {
+ result = DataFlow::getConstant("Grape").getConstant("API")
+}
+
+private API::Node grapeAPIInstance() {
+ result = any(GrapeAPIClass cls).getSelf().track()
+}
+
+/**
+ * A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
+ */
+class GrapeEndpoint extends DataFlow::CallNode {
+ private GrapeAPIClass apiClass;
+
+ GrapeEndpoint() {
+ this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
+ }
+
+ /**
+ * Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
+ */
+ string getHttpMethod() {
+ result = this.getMethodName().toUpperCase()
+ }
+
+ /**
+ * Gets the API class containing this endpoint.
+ */
+ GrapeAPIClass getAPIClass() { result = apiClass }
+
+ /**
+ * Gets the block containing the endpoint logic.
+ */
+ DataFlow::BlockNode getBody() { result = this.getBlock() }
+
+ /**
+ * Gets the path pattern for this endpoint, if specified.
+ */
+ string getPath() {
+ result = this.getArgument(0).getConstantValue().getString()
+ }
+}
+
+/**
+ * A `RemoteFlowSource::Range` to represent accessing the
+ * Grape parameters available via the `params` method within an endpoint.
+ */
+class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
+ GrapeParamsSource() {
+ this.asExpr().getExpr() instanceof GrapeParamsCall
+ }
+
+ override string getSourceType() { result = "Grape::API#params" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
+}
+
+/**
+ * A call to `params` from within a Grape API endpoint.
+ */
+private class GrapeParamsCall extends ParamsCallImpl {
+ GrapeParamsCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "params"
+ )
+ or
+ // Also handle cases where params is called on an instance of a Grape API class
+ this = grapeAPIInstance().getAMethodCall("params").asExpr().getExpr()
+ }
+}
+
+/**
+ * A call to `headers` from within a Grape API endpoint.
+ * Headers can also be a source of user input.
+ */
+class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
+ GrapeHeadersSource() {
+ this.asExpr().getExpr() instanceof GrapeHeadersCall
+ }
+
+ override string getSourceType() { result = "Grape::API#headers" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
+}
+
+/**
+ * A call to `headers` from within a Grape API endpoint.
+ */
+private class GrapeHeadersCall extends MethodCall {
+ GrapeHeadersCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "headers"
+ )
+ or
+ // Also handle cases where headers is called on an instance of a Grape API class
+ this = grapeAPIInstance().getAMethodCall("headers").asExpr().getExpr()
+ }
+}
+
+/**
+ * A call to `request` from within a Grape API endpoint.
+ * The request object can contain user input.
+ */
+class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
+ GrapeRequestSource() {
+ this.asExpr().getExpr() instanceof GrapeRequestCall
+ }
+
+ override string getSourceType() { result = "Grape::API#request" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
+}
+
+/**
+ * A call to `request` from within a Grape API endpoint.
+ */
+private class GrapeRequestCall extends MethodCall {
+ GrapeRequestCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "request"
+ )
+ or
+ // Also handle cases where request is called on an instance of a Grape API class
+ this = grapeAPIInstance().getAMethodCall("request").asExpr().getExpr()
+ }
+}
\ No newline at end of file
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
new file mode 100644
index 00000000000..904cb36333a
--- /dev/null
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
@@ -0,0 +1,25 @@
+grapeAPIClasses
+| app.rb:1:1:48:3 | MyAPI |
+| app.rb:50:1:54:3 | AdminAPI |
+grapeEndpoints
+| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
+| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
+| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
+| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
+| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
+| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
+| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
+| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin |
+grapeParams
+| app.rb:8:12:8:17 | call to params |
+| app.rb:14:3:16:5 | call to params |
+| app.rb:18:11:18:16 | call to params |
+| app.rb:24:10:24:15 | call to params |
+| app.rb:31:5:31:10 | call to params |
+| app.rb:36:5:36:10 | call to params |
+| app.rb:52:5:52:10 | call to params |
+grapeHeaders
+| app.rb:9:18:9:24 | call to headers |
+| app.rb:46:5:46:11 | call to headers |
+grapeRequest
+| app.rb:25:12:25:18 | call to request |
\ No newline at end of file
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
new file mode 100644
index 00000000000..a35c639d9ad
--- /dev/null
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -0,0 +1,18 @@
+import ruby
+import codeql.ruby.frameworks.Grape
+import codeql.ruby.Concepts
+import codeql.ruby.AST
+
+query predicate grapeAPIClasses(GrapeAPIClass api) { any() }
+
+query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) {
+ endpoint = api.getAnEndpoint() and
+ method = endpoint.getHttpMethod() and
+ path = endpoint.getPath()
+}
+
+query predicate grapeParams(GrapeParamsSource params) { any() }
+
+query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
+
+query predicate grapeRequest(GrapeRequestSource request) { any() }
\ No newline at end of file
diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb
new file mode 100644
index 00000000000..3e33caa85e9
--- /dev/null
+++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb
@@ -0,0 +1,54 @@
+class MyAPI < Grape::API
+ version 'v1', using: :header, vendor: 'myapi'
+ format :json
+ prefix :api
+
+ desc 'Simple get endpoint'
+ get '/hello/:name' do
+ name = params[:name]
+ user_agent = headers['User-Agent']
+ "Hello #{name}!"
+ end
+
+ desc 'Post endpoint with params'
+ params do
+ requires :message, type: String
+ end
+ post '/messages' do
+ msg = params[:message]
+ { status: 'received', message: msg }
+ end
+
+ desc 'Put endpoint accessing request'
+ put '/update/:id' do
+ id = params[:id]
+ body = request.body.read
+ { id: id, body: body }
+ end
+
+ desc 'Delete endpoint'
+ delete '/items/:id' do
+ params[:id]
+ end
+
+ desc 'Patch endpoint'
+ patch '/items/:id' do
+ params[:id]
+ end
+
+ desc 'Head endpoint'
+ head '/status' do
+ # Just return status
+ end
+
+ desc 'Options endpoint'
+ options '/info' do
+ headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
+ end
+end
+
+class AdminAPI < Grape::API
+ get '/admin' do
+ params[:token]
+ end
+end
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
index 1cd6782b241..30832894b9e 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
+++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
@@ -6,4 +6,13 @@ class PotatoController < ActionController::Base
sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
end
+end
+
+class PotatoAPI < Grape::API
+ get '/unsafe_endpoint' do
+ name = params[:user_name]
+ # BAD: SQL statement constructed from user input
+ sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
+ sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
+ end
end
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
index 069cb34810f..b8b1350882d 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
+++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
@@ -81,6 +81,10 @@ edges
| ArelInjection.rb:4:5:4:8 | name | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:4:12:4:29 | ...[...] | provenance | |
| ArelInjection.rb:4:12:4:29 | ...[...] | ArelInjection.rb:4:5:4:8 | name | provenance | |
+| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | |
+| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -209,6 +213,11 @@ nodes
| ArelInjection.rb:4:12:4:29 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
+| ArelInjection.rb:13:5:13:8 | name | semmle.label | name |
+| ArelInjection.rb:13:12:13:17 | call to params | semmle.label | call to params |
+| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
+| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -266,6 +275,8 @@ subpaths
| ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | ActiveRecordInjection.rb:222:29:222:34 | call to params | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | This SQL query depends on a $@. | ActiveRecordInjection.rb:222:29:222:34 | call to params | user-provided value |
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
+| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
+| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
| PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
From 738ab6fba7ff64b97c60cf7f2593f136c5bf0f04 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 12 Sep 2025 19:23:15 -0400
Subject: [PATCH 04/90] Refactor Grape framework code for improved readability
and consistency
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +-
.../test/library-tests/frameworks/grape/Grape.ql | 2 +-
.../test/library-tests/frameworks/grape/app.rb | 16 ++++++++--------
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 8e9a062dc9a..857b849f425 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -115,7 +115,7 @@ class GrapeEndpoint extends DataFlow::CallNode {
* Grape parameters available via the `params` method within an endpoint.
*/
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
- GrapeParamsSource() {
+ GrapeParamsSource() {
this.asExpr().getExpr() instanceof GrapeParamsCall
}
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
index a35c639d9ad..3dd7c488a49 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -5,7 +5,7 @@ import codeql.ruby.AST
query predicate grapeAPIClasses(GrapeAPIClass api) { any() }
-query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) {
+query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) {
endpoint = api.getAnEndpoint() and
method = endpoint.getHttpMethod() and
path = endpoint.getPath()
diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb
index 3e33caa85e9..6333240debe 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/app.rb
+++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb
@@ -9,7 +9,7 @@ class MyAPI < Grape::API
user_agent = headers['User-Agent']
"Hello #{name}!"
end
-
+
desc 'Post endpoint with params'
params do
requires :message, type: String
@@ -18,36 +18,36 @@ class MyAPI < Grape::API
msg = params[:message]
{ status: 'received', message: msg }
end
-
+
desc 'Put endpoint accessing request'
put '/update/:id' do
id = params[:id]
body = request.body.read
{ id: id, body: body }
end
-
- desc 'Delete endpoint'
+
+ desc 'Delete endpoint'
delete '/items/:id' do
params[:id]
end
-
+
desc 'Patch endpoint'
patch '/items/:id' do
params[:id]
end
-
+
desc 'Head endpoint'
head '/status' do
# Just return status
end
-
+
desc 'Options endpoint'
options '/info' do
headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
end
end
-class AdminAPI < Grape::API
+class AdminAPI < Grape::API
get '/admin' do
params[:token]
end
From 3252bd39d2e671710e94ada3adaae1cefb1deaf1 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 12 Sep 2025 22:13:21 -0400
Subject: [PATCH 05/90] Enhance Grape framework with additional data flow
modeling and helper method support
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 57 +++++++++++++++----
.../frameworks/grape/Grape.expected | 2 +-
.../security/cwe-089/ArelInjection.rb | 25 +++++++-
.../security/cwe-089/SqlInjection.expected | 20 +++++++
4 files changed, 92 insertions(+), 12 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 857b849f425..a3aa2f684c7 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -12,6 +12,7 @@ private import codeql.ruby.typetracking.TypeTracking
private import codeql.ruby.frameworks.Rails
private import codeql.ruby.frameworks.internal.Rails
private import codeql.ruby.dataflow.internal.DataFlowDispatch
+private import codeql.ruby.dataflow.FlowSteps
/**
* Provides modeling for Grape, a REST-like API framework for Ruby.
@@ -125,21 +126,17 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
}
/**
- * A call to `params` from within a Grape API endpoint.
+ * A call to `params` from within a Grape API endpoint or helper method.
*/
private class GrapeParamsCall extends ParamsCallImpl {
GrapeParamsCall() {
- exists(GrapeEndpoint endpoint |
- this.getParent+() = endpoint.getBody().asCallableAstNode() and
- this.getMethodName() = "params"
+ // Simplified approach: find params calls that are descendants of Grape API class methods
+ exists(GrapeAPIClass api |
+ this.getMethodName() = "params" and
+ this.getParent+() = api.getADeclaration()
)
- or
- // Also handle cases where params is called on an instance of a Grape API class
- this = grapeAPIInstance().getAMethodCall("params").asExpr().getExpr()
}
-}
-
-/**
+}/**
* A call to `headers` from within a Grape API endpoint.
* Headers can also be a source of user input.
*/
@@ -195,4 +192,44 @@ private class GrapeRequestCall extends MethodCall {
// Also handle cases where request is called on an instance of a Grape API class
this = grapeAPIInstance().getAMethodCall("request").asExpr().getExpr()
}
+}
+
+/**
+ * A method defined within a `helpers` block in a Grape API class.
+ * These methods become available in endpoint contexts through Grape's DSL.
+ */
+private class GrapeHelperMethod extends Method {
+ private GrapeAPIClass apiClass;
+
+ GrapeHelperMethod() {
+ exists(DataFlow::CallNode helpersCall |
+ helpersCall = apiClass.getAModuleLevelCall("helpers") and
+ this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
+ )
+ }
+
+ /**
+ * Gets the API class that contains this helper method.
+ */
+ GrapeAPIClass getAPIClass() { result = apiClass }
+}
+
+/**
+ * Additional taint step to model dataflow from method arguments to parameters
+ * for Grape helper methods defined in `helpers` blocks.
+ * This bridges the gap where standard dataflow doesn't recognize the Grape DSL semantics.
+ */
+private class GrapeHelperMethodTaintStep extends AdditionalTaintStep {
+ override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
+ exists(GrapeHelperMethod helperMethod, MethodCall call, int i |
+ // Find calls to helper methods from within Grape endpoints
+ call.getMethodName() = helperMethod.getName() and
+ exists(GrapeEndpoint endpoint |
+ call.getParent+() = endpoint.getBody().asExpr().getExpr()
+ ) and
+ // Map argument to parameter
+ nodeFrom.asExpr().getExpr() = call.getArgument(i) and
+ nodeTo.asParameter() = helperMethod.getParameter(i)
+ )
+ }
}
\ No newline at end of file
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
index 904cb36333a..7e792465911 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
@@ -22,4 +22,4 @@ grapeHeaders
| app.rb:9:18:9:24 | call to headers |
| app.rb:46:5:46:11 | call to headers |
grapeRequest
-| app.rb:25:12:25:18 | call to request |
\ No newline at end of file
+| app.rb:25:12:25:18 | call to request |
diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
index 30832894b9e..cf0769c0acd 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
+++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
@@ -15,4 +15,27 @@ class PotatoAPI < Grape::API
sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
end
-end
\ No newline at end of file
+end
+
+class SimpleAPI < Grape::API
+ get '/test' do
+ x = params[:name]
+ Arel.sql("SELECT * FROM users WHERE name = #{x}")
+ end
+end
+
+ # Test helper method pattern in Grape helpers block
+ class TestAPI < Grape::API
+ helpers do
+ def vulnerable_helper(user_id)
+ # BAD: SQL statement constructed from user input passed as parameter
+ Arel.sql("SELECT * FROM users WHERE id = #{user_id}")
+ end
+ end
+
+ get '/helper_test' do
+ # This should be detected as SQL injection via helper method
+ user_id = params[:user_id]
+ vulnerable_helper(user_id)
+ end
+ end
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
index b8b1350882d..0b14504058e 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
+++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
@@ -85,6 +85,14 @@ edges
| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | |
| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | |
+| ArelInjection.rb:22:5:22:5 | x | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | |
+| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | |
+| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:38:7:38:13 | user_id | ArelInjection.rb:39:25:39:31 | user_id | provenance | |
+| ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:38:17:38:32 | ...[...] | provenance | |
+| ArelInjection.rb:38:17:38:32 | ...[...] | ArelInjection.rb:38:7:38:13 | user_id | provenance | |
+| ArelInjection.rb:39:25:39:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -218,6 +226,16 @@ nodes
| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
+| ArelInjection.rb:22:5:22:5 | x | semmle.label | x |
+| ArelInjection.rb:22:9:22:14 | call to params | semmle.label | call to params |
+| ArelInjection.rb:22:9:22:21 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
+| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id |
+| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
+| ArelInjection.rb:38:7:38:13 | user_id | semmle.label | user_id |
+| ArelInjection.rb:38:17:38:22 | call to params | semmle.label | call to params |
+| ArelInjection.rb:38:17:38:32 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:39:25:39:31 | user_id | semmle.label | user_id |
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -277,6 +295,8 @@ subpaths
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
+| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value |
+| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:38:17:38:22 | call to params | user-provided value |
| PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
From 5cfa6e83b390aafe6783eba0e282674a8247db46 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 12 Sep 2025 22:51:47 -0400
Subject: [PATCH 06/90] Add support for route parameters(+ blocks), headers,
and cookies in Grape API
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 92 ++++++++++++++++++-
.../frameworks/grape/Grape.expected | 37 +++++---
.../library-tests/frameworks/grape/Grape.ql | 6 +-
.../library-tests/frameworks/grape/app.rb | 42 +++++++++
.../security/cwe-089/ArelInjection.rb | 32 ++++++-
.../security/cwe-089/SqlInjection.expected | 53 +++++++++--
6 files changed, 239 insertions(+), 23 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index a3aa2f684c7..fbab28180b8 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -137,12 +137,14 @@ private class GrapeParamsCall extends ParamsCallImpl {
)
}
}/**
- * A call to `headers` from within a Grape API endpoint.
+ * A call to `headers` from within a Grape API endpoint or headers block.
* Headers can also be a source of user input.
*/
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
GrapeHeadersSource() {
this.asExpr().getExpr() instanceof GrapeHeadersCall
+ or
+ this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
}
override string getSourceType() { result = "Grape::API#headers" }
@@ -179,6 +181,20 @@ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
}
+/**
+ * A call to `route_param` from within a Grape API endpoint.
+ * Route parameters are extracted from the URL path and can be a source of user input.
+ */
+class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
+ GrapeRouteParamSource() {
+ this.asExpr().getExpr() instanceof GrapeRouteParamCall
+ }
+
+ override string getSourceType() { result = "Grape::API#route_param" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
+}
+
/**
* A call to `request` from within a Grape API endpoint.
*/
@@ -194,6 +210,80 @@ private class GrapeRequestCall extends MethodCall {
}
}
+/**
+ * A call to `route_param` from within a Grape API endpoint.
+ */
+private class GrapeRouteParamCall extends MethodCall {
+ GrapeRouteParamCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asExpr().getExpr() and
+ this.getMethodName() = "route_param"
+ )
+ or
+ // Also handle cases where route_param is called on an instance of a Grape API class
+ this = grapeAPIInstance().getAMethodCall("route_param").asExpr().getExpr()
+ }
+}
+
+/**
+ * A call to `headers` block within a Grape API class.
+ * This is different from the headers() method call - this is the DSL block for defining header requirements.
+ */
+private class GrapeHeadersBlockCall extends MethodCall {
+ GrapeHeadersBlockCall() {
+ exists(GrapeAPIClass api |
+ this.getParent+() = api.getADeclaration() and
+ this.getMethodName() = "headers" and
+ exists(this.getBlock())
+ )
+ }
+}
+
+/**
+ * A call to `cookies` block within a Grape API class.
+ * This DSL block defines cookie requirements and those cookies are user-controlled.
+ */
+private class GrapeCookiesBlockCall extends MethodCall {
+ GrapeCookiesBlockCall() {
+ exists(GrapeAPIClass api |
+ this.getParent+() = api.getADeclaration() and
+ this.getMethodName() = "cookies" and
+ exists(this.getBlock())
+ )
+ }
+}
+
+/**
+ * A call to `cookies` method from within a Grape API endpoint or cookies block.
+ * Similar to headers, cookies can be accessed as a method and are user-controlled input.
+ */
+class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
+ GrapeCookiesSource() {
+ this.asExpr().getExpr() instanceof GrapeCookiesCall
+ or
+ this.asExpr().getExpr() instanceof GrapeCookiesBlockCall
+ }
+
+ override string getSourceType() { result = "Grape::API#cookies" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
+}
+
+/**
+ * A call to `cookies` method from within a Grape API endpoint.
+ */
+private class GrapeCookiesCall extends MethodCall {
+ GrapeCookiesCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "cookies"
+ )
+ or
+ // Also handle cases where cookies is called on an instance of a Grape API class
+ this = grapeAPIInstance().getAMethodCall("cookies").asExpr().getExpr()
+ }
+}
+
/**
* A method defined within a `helpers` block in a Grape API class.
* These methods become available in endpoint contexts through Grape's DSL.
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
index 7e792465911..c0bee75371c 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
@@ -1,15 +1,18 @@
grapeAPIClasses
-| app.rb:1:1:48:3 | MyAPI |
-| app.rb:50:1:54:3 | AdminAPI |
+| app.rb:1:1:90:3 | MyAPI |
+| app.rb:92:1:96:3 | AdminAPI |
grapeEndpoints
-| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
-| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
-| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
-| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
-| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
-| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
-| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
-| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin |
+| app.rb:1:1:90:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
+| app.rb:1:1:90:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
+| app.rb:1:1:90:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
+| app.rb:1:1:90:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
+| app.rb:1:1:90:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
+| app.rb:1:1:90:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
+| app.rb:1:1:90:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
+| app.rb:1:1:90:3 | MyAPI | app.rb:50:3:54:5 | call to get | GET | /users/:user_id/posts/:post_id |
+| app.rb:1:1:90:3 | MyAPI | app.rb:78:3:82:5 | call to get | GET | /cookie_test |
+| app.rb:1:1:90:3 | MyAPI | app.rb:85:3:89:5 | call to get | GET | /header_test |
+| app.rb:92:1:96:3 | AdminAPI | app.rb:93:3:95:5 | call to get | GET | /admin |
grapeParams
| app.rb:8:12:8:17 | call to params |
| app.rb:14:3:16:5 | call to params |
@@ -17,9 +20,21 @@ grapeParams
| app.rb:24:10:24:15 | call to params |
| app.rb:31:5:31:10 | call to params |
| app.rb:36:5:36:10 | call to params |
-| app.rb:52:5:52:10 | call to params |
+| app.rb:60:12:60:17 | call to params |
+| app.rb:94:5:94:10 | call to params |
grapeHeaders
| app.rb:9:18:9:24 | call to headers |
| app.rb:46:5:46:11 | call to headers |
+| app.rb:66:3:69:5 | call to headers |
+| app.rb:86:12:86:18 | call to headers |
+| app.rb:87:14:87:20 | call to headers |
grapeRequest
| app.rb:25:12:25:18 | call to request |
+grapeRouteParam
+| app.rb:51:15:51:35 | call to route_param |
+| app.rb:52:15:52:36 | call to route_param |
+| app.rb:57:3:63:5 | call to route_param |
+grapeCookies
+| app.rb:72:3:75:5 | call to cookies |
+| app.rb:79:15:79:21 | call to cookies |
+| app.rb:80:16:80:22 | call to cookies |
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
index 3dd7c488a49..63d59d0bdd7 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -15,4 +15,8 @@ query predicate grapeParams(GrapeParamsSource params) { any() }
query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
-query predicate grapeRequest(GrapeRequestSource request) { any() }
\ No newline at end of file
+query predicate grapeRequest(GrapeRequestSource request) { any() }
+
+query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() }
+
+query predicate grapeCookies(GrapeCookiesSource cookies) { any() }
\ No newline at end of file
diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb
index 6333240debe..a034f325f7b 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/app.rb
+++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb
@@ -45,6 +45,48 @@ class MyAPI < Grape::API
options '/info' do
headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
end
+
+ desc 'Route param endpoint'
+ get '/users/:user_id/posts/:post_id' do
+ user_id = route_param(:user_id)
+ post_id = route_param('post_id')
+ { user_id: user_id, post_id: post_id }
+ end
+
+ desc 'Route param block pattern'
+ route_param :id do
+ get do
+ # params[:id] is user input from the path parameter
+ id = params[:id]
+ { id: id }
+ end
+ end
+
+ # Headers block for defining expected headers
+ headers do
+ requires :Authorization, type: String
+ optional 'X-Custom-Header', type: String
+ end
+
+ # Cookies block for defining expected cookies
+ cookies do
+ requires :session_id, type: String
+ optional :tracking_id, type: String
+ end
+
+ desc 'Endpoint that uses cookies method'
+ get '/cookie_test' do
+ session = cookies[:session_id]
+ tracking = cookies['tracking_id']
+ { session: session, tracking: tracking }
+ end
+
+ desc 'Endpoint that uses headers method'
+ get '/header_test' do
+ auth = headers[:Authorization]
+ custom = headers['X-Custom-Header']
+ { auth: auth, custom: custom }
+ end
end
class AdminAPI < Grape::API
diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
index cf0769c0acd..8c9c3bff4fb 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
+++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
@@ -33,9 +33,39 @@ end
end
end
+ # Headers and cookies blocks for DSL testing
+ headers do
+ requires :Authorization, type: String
+ end
+
+ cookies do
+ requires :session_id, type: String
+ end
+
+ get '/comprehensive_test/:user_id' do
+ # BAD: Comprehensive test using all Grape input sources in one SQL query
+ user_id = params[:user_id] # params taint source
+ route_id = route_param(:user_id) # route_param taint source
+ auth = headers[:Authorization] # headers taint source
+ session = cookies[:session_id] # cookies taint source
+ body_data = request.body.read # request taint source
+
+ # All sources flow to SQL injection
+ Arel.sql("SELECT * FROM users WHERE id = #{user_id} AND route_id = #{route_id} AND auth = #{auth} AND session = #{session} AND data = #{body_data}")
+ end
+
get '/helper_test' do
- # This should be detected as SQL injection via helper method
+ # BAD: Test helper method dataflow
user_id = params[:user_id]
vulnerable_helper(user_id)
end
+
+ # Test route_param block pattern
+ route_param :id do
+ get do
+ # BAD: params[:id] should be user input from the path
+ user_id = params[:id]
+ Arel.sql("SELECT * FROM users WHERE id = #{user_id}")
+ end
+ end
end
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
index 0b14504058e..34128474cb9 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
+++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
@@ -89,10 +89,24 @@ edges
| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | |
| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | |
| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:38:7:38:13 | user_id | ArelInjection.rb:39:25:39:31 | user_id | provenance | |
-| ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:38:17:38:32 | ...[...] | provenance | |
-| ArelInjection.rb:38:17:38:32 | ...[...] | ArelInjection.rb:38:7:38:13 | user_id | provenance | |
-| ArelInjection.rb:39:25:39:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
+| ArelInjection.rb:47:7:47:13 | user_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:47:17:47:32 | ...[...] | provenance | |
+| ArelInjection.rb:47:17:47:32 | ...[...] | ArelInjection.rb:47:7:47:13 | user_id | provenance | |
+| ArelInjection.rb:48:7:48:14 | route_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:48:7:48:14 | route_id | provenance | |
+| ArelInjection.rb:49:7:49:10 | auth | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:49:14:49:36 | ...[...] | provenance | |
+| ArelInjection.rb:49:14:49:36 | ...[...] | ArelInjection.rb:49:7:49:10 | auth | provenance | |
+| ArelInjection.rb:50:7:50:13 | session | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:50:17:50:36 | ...[...] | provenance | |
+| ArelInjection.rb:50:17:50:36 | ...[...] | ArelInjection.rb:50:7:50:13 | session | provenance | |
+| ArelInjection.rb:59:7:59:13 | user_id | ArelInjection.rb:60:25:60:31 | user_id | provenance | |
+| ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:59:17:59:32 | ...[...] | provenance | |
+| ArelInjection.rb:59:17:59:32 | ...[...] | ArelInjection.rb:59:7:59:13 | user_id | provenance | |
+| ArelInjection.rb:60:25:60:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
+| ArelInjection.rb:67:9:67:15 | user_id | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
+| ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:67:19:67:29 | ...[...] | provenance | |
+| ArelInjection.rb:67:19:67:29 | ...[...] | ArelInjection.rb:67:9:67:15 | user_id | provenance | |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -232,10 +246,26 @@ nodes
| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id |
| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
-| ArelInjection.rb:38:7:38:13 | user_id | semmle.label | user_id |
-| ArelInjection.rb:38:17:38:22 | call to params | semmle.label | call to params |
-| ArelInjection.rb:38:17:38:32 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:39:25:39:31 | user_id | semmle.label | user_id |
+| ArelInjection.rb:47:7:47:13 | user_id | semmle.label | user_id |
+| ArelInjection.rb:47:17:47:22 | call to params | semmle.label | call to params |
+| ArelInjection.rb:47:17:47:32 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:48:7:48:14 | route_id | semmle.label | route_id |
+| ArelInjection.rb:48:18:48:38 | call to route_param | semmle.label | call to route_param |
+| ArelInjection.rb:49:7:49:10 | auth | semmle.label | auth |
+| ArelInjection.rb:49:14:49:20 | call to headers | semmle.label | call to headers |
+| ArelInjection.rb:49:14:49:36 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:50:7:50:13 | session | semmle.label | session |
+| ArelInjection.rb:50:17:50:23 | call to cookies | semmle.label | call to cookies |
+| ArelInjection.rb:50:17:50:36 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
+| ArelInjection.rb:59:7:59:13 | user_id | semmle.label | user_id |
+| ArelInjection.rb:59:17:59:22 | call to params | semmle.label | call to params |
+| ArelInjection.rb:59:17:59:32 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:60:25:60:31 | user_id | semmle.label | user_id |
+| ArelInjection.rb:67:9:67:15 | user_id | semmle.label | user_id |
+| ArelInjection.rb:67:19:67:24 | call to params | semmle.label | call to params |
+| ArelInjection.rb:67:19:67:29 | ...[...] | semmle.label | ...[...] |
+| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -296,7 +326,12 @@ subpaths
| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value |
-| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:38:17:38:22 | call to params | user-provided value |
+| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:59:17:59:22 | call to params | user-provided value |
+| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:47:17:47:22 | call to params | user-provided value |
+| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:48:18:48:38 | call to route_param | user-provided value |
+| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:49:14:49:20 | call to headers | user-provided value |
+| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:50:17:50:23 | call to cookies | user-provided value |
+| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:67:19:67:24 | call to params | user-provided value |
| PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
From 459f00ab415e1263c0756a03516e51e9963adc52 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 15 Sep 2025 11:25:11 +0000
Subject: [PATCH 07/90] Initial plan
From e630bf86bdd0f9a678b234f04f11f4ba883b5571 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 15 Sep 2025 11:44:05 +0000
Subject: [PATCH 08/90] Implement Rust non-HTTPS URL query (CWE-319)
Co-authored-by: geoffw0 <40627776+geoffw0@users.noreply.github.com>
---
.../rust-code-scanning.qls.expected | 1 +
.../rust-security-and-quality.qls.expected | 1 +
.../rust-security-extended.qls.expected | 1 +
.../rust/security/UseOfHttpExtensions.qll | 60 +
.../change-notes/2025-09-15-non-https-url.md | 4 +
.../queries/security/CWE-319/UseOfHttp.qhelp | 48 +
.../src/queries/security/CWE-319/UseOfHttp.ql | 42 +
.../queries/security/CWE-319/UseOfHttpBad.rs | 10 +
.../queries/security/CWE-319/UseOfHttpGood.rs | 10 +
rust/ql/src/queries/summary/Stats.qll | 1 +
.../query-tests/security/CWE-319/Cargo.lock | 1574 +++++++++++++++++
.../security/CWE-319/UseOfHttp.expected | 78 +
.../security/CWE-319/UseOfHttp.qlref | 4 +
.../test/query-tests/security/CWE-319/main.rs | 65 +
.../query-tests/security/CWE-319/options.yml | 3 +
15 files changed, 1902 insertions(+)
create mode 100644 rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
create mode 100644 rust/ql/src/change-notes/2025-09-15-non-https-url.md
create mode 100644 rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
create mode 100644 rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
create mode 100644 rust/ql/src/queries/security/CWE-319/UseOfHttpBad.rs
create mode 100644 rust/ql/src/queries/security/CWE-319/UseOfHttpGood.rs
create mode 100644 rust/ql/test/query-tests/security/CWE-319/Cargo.lock
create mode 100644 rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
create mode 100644 rust/ql/test/query-tests/security/CWE-319/UseOfHttp.qlref
create mode 100644 rust/ql/test/query-tests/security/CWE-319/main.rs
create mode 100644 rust/ql/test/query-tests/security/CWE-319/options.yml
diff --git a/rust/ql/integration-tests/query-suite/rust-code-scanning.qls.expected b/rust/ql/integration-tests/query-suite/rust-code-scanning.qls.expected
index b601905e6a3..1b8e1015a1f 100644
--- a/rust/ql/integration-tests/query-suite/rust-code-scanning.qls.expected
+++ b/rust/ql/integration-tests/query-suite/rust-code-scanning.qls.expected
@@ -14,6 +14,7 @@ ql/rust/ql/src/queries/security/CWE-089/SqlInjection.ql
ql/rust/ql/src/queries/security/CWE-311/CleartextTransmission.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
+ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
diff --git a/rust/ql/integration-tests/query-suite/rust-security-and-quality.qls.expected b/rust/ql/integration-tests/query-suite/rust-security-and-quality.qls.expected
index 074cb2ec888..a2d2e2b820c 100644
--- a/rust/ql/integration-tests/query-suite/rust-security-and-quality.qls.expected
+++ b/rust/ql/integration-tests/query-suite/rust-security-and-quality.qls.expected
@@ -15,6 +15,7 @@ ql/rust/ql/src/queries/security/CWE-117/LogInjection.ql
ql/rust/ql/src/queries/security/CWE-311/CleartextTransmission.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
+ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-696/BadCtorInitialization.ql
diff --git a/rust/ql/integration-tests/query-suite/rust-security-extended.qls.expected b/rust/ql/integration-tests/query-suite/rust-security-extended.qls.expected
index 38846e281eb..9000990ad84 100644
--- a/rust/ql/integration-tests/query-suite/rust-security-extended.qls.expected
+++ b/rust/ql/integration-tests/query-suite/rust-security-extended.qls.expected
@@ -15,6 +15,7 @@ ql/rust/ql/src/queries/security/CWE-117/LogInjection.ql
ql/rust/ql/src/queries/security/CWE-311/CleartextTransmission.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
+ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
diff --git a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
new file mode 100644
index 00000000000..026880785b6
--- /dev/null
+++ b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
@@ -0,0 +1,60 @@
+/**
+ * Provides classes and predicates for reasoning about the use of
+ * non-HTTPS URLs in Rust code.
+ */
+
+import rust
+private import codeql.rust.dataflow.DataFlow
+private import codeql.rust.dataflow.FlowSink
+private import codeql.rust.elements.LiteralExprExt
+private import codeql.rust.Concepts
+
+/**
+ * Provides default sources, sinks and barriers for detecting use of
+ * non-HTTPS URLs, as well as extension points for adding your own.
+ */
+module UseOfHttp {
+ /**
+ * A data flow source for use of non-HTTPS URLs.
+ */
+ abstract class Source extends DataFlow::Node { }
+
+ /**
+ * A data flow sink for use of non-HTTPS URLs.
+ */
+ abstract class Sink extends QuerySink::Range {
+ override string getSinkType() { result = "UseOfHttp" }
+ }
+
+ /**
+ * A barrier for use of non-HTTPS URLs.
+ */
+ abstract class Barrier extends DataFlow::Node { }
+
+ /**
+ * A string containing an HTTP URL.
+ */
+ class HttpStringLiteral extends StringLiteralExpr {
+ HttpStringLiteral() {
+ exists(string s | this.getTextValue() = s |
+ // Match HTTP URLs that are not private/local
+ s.regexpMatch("\"http://.*\"") and
+ not s.regexpMatch("\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.16\\.[0-9]+\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
+ )
+ }
+ }
+
+ /**
+ * An HTTP string literal as a source.
+ */
+ private class HttpStringLiteralAsSource extends Source {
+ HttpStringLiteralAsSource() { this.asExpr().getExpr() instanceof HttpStringLiteral }
+ }
+
+ /**
+ * A sink for use of HTTP URLs from model data.
+ */
+ private class ModelsAsDataSink extends Sink {
+ ModelsAsDataSink() { sinkNode(this, "request-url") }
+ }
+}
diff --git a/rust/ql/src/change-notes/2025-09-15-non-https-url.md b/rust/ql/src/change-notes/2025-09-15-non-https-url.md
new file mode 100644
index 00000000000..c4ab664f732
--- /dev/null
+++ b/rust/ql/src/change-notes/2025-09-15-non-https-url.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* Added a new query, `rust/non-https-url`, for detecting the use of non-HTTPS URLs that can be intercepted by third parties.
\ No newline at end of file
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
new file mode 100644
index 00000000000..a8ca1d9c7c7
--- /dev/null
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
@@ -0,0 +1,48 @@
+
+
+
+
+Constructing URLs with the HTTP protocol can lead to unsecured connections.
+
+Furthermore, constructing URLs with the HTTP protocol can create problems if other parts of the
+code expect HTTPS URLs. A typical pattern is to use libraries that expect secure connections,
+which may fail or fall back to insecure behavior when provided with HTTP URLs instead of HTTPS URLs.
+
+
+
+
+When you construct a URL for network requests, ensure that you use an HTTPS URL rather than an HTTP URL.
+Then, any connections that are made using that URL are secure SSL/TLS connections.
+
+
+
+
+The following example shows two ways of making a network request using a URL. When the request is
+made using an HTTP URL rather than an HTTPS URL, the connection is unsecured and can be intercepted
+by attackers. When the request is made using an HTTPS URL, the connection is a secure SSL/TLS connection.
+
+
+
+A better approach is to use HTTPS:
+
+
+
+
+
+
+
+OWASP:
+Transport Layer Protection Cheat Sheet.
+
+
+OWASP Top 10:
+A08:2021 - Software and Data Integrity Failures.
+
+Rust reqwest documentation:
+reqwest crate.
+
+
+
+
\ No newline at end of file
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql b/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
new file mode 100644
index 00000000000..4a464d90bbe
--- /dev/null
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
@@ -0,0 +1,42 @@
+/**
+ * @name Failure to use HTTPS URLs
+ * @description Non-HTTPS connections can be intercepted by third parties.
+ * @kind path-problem
+ * @problem.severity warning
+ * @security-severity 8.1
+ * @precision high
+ * @id rust/non-https-url
+ * @tags security
+ * external/cwe/cwe-319
+ * external/cwe/cwe-345
+ */
+
+import rust
+import codeql.rust.dataflow.DataFlow
+import codeql.rust.dataflow.TaintTracking
+import codeql.rust.security.UseOfHttpExtensions
+
+/**
+ * A taint configuration for HTTP URL strings that flow to URL-using sinks.
+ */
+module UseOfHttpConfig implements DataFlow::ConfigSig {
+ import UseOfHttp
+
+ predicate isSource(DataFlow::Node node) { node instanceof Source }
+
+ predicate isSink(DataFlow::Node node) { node instanceof Sink }
+
+ predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier }
+
+ predicate observeDiffInformedIncrementalMode() { any() }
+}
+
+module UseOfHttpFlow = TaintTracking::Global;
+
+import UseOfHttpFlow::PathGraph
+
+from UseOfHttpFlow::PathNode sourceNode, UseOfHttpFlow::PathNode sinkNode
+where UseOfHttpFlow::flowPath(sourceNode, sinkNode)
+select sinkNode.getNode(), sourceNode, sinkNode,
+ "This URL may be constructed with the HTTP protocol, from $@.", sourceNode.getNode(),
+ "this HTTP URL"
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttpBad.rs b/rust/ql/src/queries/security/CWE-319/UseOfHttpBad.rs
new file mode 100644
index 00000000000..ada466cae5c
--- /dev/null
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttpBad.rs
@@ -0,0 +1,10 @@
+// BAD: Using HTTP URL which can be intercepted
+use reqwest;
+
+fn main() {
+ let url = "http://example.com/sensitive-data";
+
+ // This makes an insecure HTTP request that can be intercepted
+ let response = reqwest::blocking::get(url).unwrap();
+ println!("Response: {}", response.text().unwrap());
+}
\ No newline at end of file
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttpGood.rs b/rust/ql/src/queries/security/CWE-319/UseOfHttpGood.rs
new file mode 100644
index 00000000000..22b94235fa1
--- /dev/null
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttpGood.rs
@@ -0,0 +1,10 @@
+// GOOD: Using HTTPS URL which provides encryption
+use reqwest;
+
+fn main() {
+ let url = "https://example.com/sensitive-data";
+
+ // This makes a secure HTTPS request that is encrypted
+ let response = reqwest::blocking::get(url).unwrap();
+ println!("Response: {}", response.text().unwrap());
+}
\ No newline at end of file
diff --git a/rust/ql/src/queries/summary/Stats.qll b/rust/ql/src/queries/summary/Stats.qll
index 7a1de4f1314..d49e1fdde5d 100644
--- a/rust/ql/src/queries/summary/Stats.qll
+++ b/rust/ql/src/queries/summary/Stats.qll
@@ -27,6 +27,7 @@ private import codeql.rust.security.LogInjectionExtensions
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.TaintedPathExtensions
private import codeql.rust.security.UncontrolledAllocationSizeExtensions
+private import codeql.rust.security.UseOfHttpExtensions
private import codeql.rust.security.WeakSensitiveDataHashingExtensions
private import codeql.rust.security.HardcodedCryptographicValueExtensions
diff --git a/rust/ql/test/query-tests/security/CWE-319/Cargo.lock b/rust/ql/test/query-tests/security/CWE-319/Cargo.lock
new file mode 100644
index 00000000000..ad4b5cebd22
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-319/Cargo.lock
@@ -0,0 +1,1574 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.5+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "reqwest"
+version = "0.12.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schannel"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+dependencies = [
+ "windows-sys 0.61.0",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.223"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.223"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.223"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.0",
+]
+
+[[package]]
+name = "test"
+version = "0.0.1"
+dependencies = [
+ "reqwest",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "slab",
+ "socket2",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.5+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
+dependencies = [
+ "wasip2",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.0+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
new file mode 100644
index 00000000000..53cc8606cc8
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -0,0 +1,78 @@
+#select
+| main.rs:12:22:12:43 | ...::get | main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:12:45:12:68 | "http://example.com/api" | this HTTP URL |
+| main.rs:13:22:13:43 | ...::get | main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:13:45:13:73 | "http://api.example.com/data" | this HTTP URL |
+| main.rs:25:21:25:42 | ...::get | main.rs:22:20:22:39 | "http://example.com" | main.rs:25:21:25:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:22:20:22:39 | "http://example.com" | this HTTP URL |
+| main.rs:36:30:36:51 | ...::get | main.rs:33:20:33:28 | "http://" | main.rs:36:30:36:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:33:20:33:28 | "http://" | this HTTP URL |
+| main.rs:60:21:60:42 | ...::get | main.rs:59:15:59:49 | "http://example.com/sensitive-... | main.rs:60:21:60:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:59:15:59:49 | "http://example.com/sensitive-... | this HTTP URL |
+edges
+| main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:22:9:22:16 | base_url | main.rs:24:28:24:53 | MacroExpr | provenance | |
+| main.rs:22:20:22:39 | "http://example.com" | main.rs:22:9:22:16 | base_url | provenance | |
+| main.rs:24:9:24:16 | full_url | main.rs:25:45:25:52 | full_url | provenance | |
+| main.rs:24:20:24:26 | res | main.rs:24:28:24:53 | { ... } | provenance | |
+| main.rs:24:28:24:53 | ...::format(...) | main.rs:24:20:24:26 | res | provenance | |
+| main.rs:24:28:24:53 | ...::must_use(...) | main.rs:24:9:24:16 | full_url | provenance | |
+| main.rs:24:28:24:53 | MacroExpr | main.rs:24:28:24:53 | ...::format(...) | provenance | MaD:2 |
+| main.rs:24:28:24:53 | { ... } | main.rs:24:28:24:53 | ...::must_use(...) | provenance | MaD:3 |
+| main.rs:25:44:25:52 | &full_url [&ref] | main.rs:25:21:25:42 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:25:45:25:52 | full_url | main.rs:25:44:25:52 | &full_url [&ref] | provenance | |
+| main.rs:33:9:33:16 | protocol | main.rs:35:32:35:53 | MacroExpr | provenance | |
+| main.rs:33:20:33:28 | "http://" | main.rs:33:9:33:16 | protocol | provenance | |
+| main.rs:35:9:35:20 | insecure_url | main.rs:36:54:36:65 | insecure_url | provenance | |
+| main.rs:35:24:35:30 | res | main.rs:35:32:35:53 | { ... } | provenance | |
+| main.rs:35:32:35:53 | ...::format(...) | main.rs:35:24:35:30 | res | provenance | |
+| main.rs:35:32:35:53 | ...::must_use(...) | main.rs:35:9:35:20 | insecure_url | provenance | |
+| main.rs:35:32:35:53 | MacroExpr | main.rs:35:32:35:53 | ...::format(...) | provenance | MaD:2 |
+| main.rs:35:32:35:53 | { ... } | main.rs:35:32:35:53 | ...::must_use(...) | provenance | MaD:3 |
+| main.rs:36:53:36:65 | &insecure_url [&ref] | main.rs:36:30:36:51 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:36:54:36:65 | insecure_url | main.rs:36:53:36:65 | &insecure_url [&ref] | provenance | |
+| main.rs:59:9:59:11 | url | main.rs:60:44:60:46 | url | provenance | |
+| main.rs:59:15:59:49 | "http://example.com/sensitive-... | main.rs:59:9:59:11 | url | provenance | |
+| main.rs:60:44:60:46 | url | main.rs:60:21:60:42 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+models
+| 1 | Sink: reqwest::blocking::get; Argument[0]; request-url |
+| 2 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
+| 3 | Summary: core::hint::must_use; Argument[0]; ReturnValue; value |
+nodes
+| main.rs:12:22:12:43 | ...::get | semmle.label | ...::get |
+| main.rs:12:45:12:68 | "http://example.com/api" | semmle.label | "http://example.com/api" |
+| main.rs:13:22:13:43 | ...::get | semmle.label | ...::get |
+| main.rs:13:45:13:73 | "http://api.example.com/data" | semmle.label | "http://api.example.com/data" |
+| main.rs:22:9:22:16 | base_url | semmle.label | base_url |
+| main.rs:22:20:22:39 | "http://example.com" | semmle.label | "http://example.com" |
+| main.rs:24:9:24:16 | full_url | semmle.label | full_url |
+| main.rs:24:20:24:26 | res | semmle.label | res |
+| main.rs:24:28:24:53 | ...::format(...) | semmle.label | ...::format(...) |
+| main.rs:24:28:24:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
+| main.rs:24:28:24:53 | MacroExpr | semmle.label | MacroExpr |
+| main.rs:24:28:24:53 | { ... } | semmle.label | { ... } |
+| main.rs:25:21:25:42 | ...::get | semmle.label | ...::get |
+| main.rs:25:44:25:52 | &full_url [&ref] | semmle.label | &full_url [&ref] |
+| main.rs:25:45:25:52 | full_url | semmle.label | full_url |
+| main.rs:33:9:33:16 | protocol | semmle.label | protocol |
+| main.rs:33:20:33:28 | "http://" | semmle.label | "http://" |
+| main.rs:35:9:35:20 | insecure_url | semmle.label | insecure_url |
+| main.rs:35:24:35:30 | res | semmle.label | res |
+| main.rs:35:32:35:53 | ...::format(...) | semmle.label | ...::format(...) |
+| main.rs:35:32:35:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
+| main.rs:35:32:35:53 | MacroExpr | semmle.label | MacroExpr |
+| main.rs:35:32:35:53 | { ... } | semmle.label | { ... } |
+| main.rs:36:30:36:51 | ...::get | semmle.label | ...::get |
+| main.rs:36:53:36:65 | &insecure_url [&ref] | semmle.label | &insecure_url [&ref] |
+| main.rs:36:54:36:65 | insecure_url | semmle.label | insecure_url |
+| main.rs:59:9:59:11 | url | semmle.label | url |
+| main.rs:59:15:59:49 | "http://example.com/sensitive-... | semmle.label | "http://example.com/sensitive-... |
+| main.rs:60:21:60:42 | ...::get | semmle.label | ...::get |
+| main.rs:60:44:60:46 | url | semmle.label | url |
+subpaths
+testFailures
+| main.rs:22:20:22:39 | "http://example.com" | Unexpected result: Source |
+| main.rs:22:42:22:71 | //... | Missing result: Alert[rust/non-https-url] |
+| main.rs:25:21:25:42 | ...::get | Unexpected result: Alert |
+| main.rs:33:20:33:28 | "http://" | Unexpected result: Source |
+| main.rs:33:31:33:60 | //... | Missing result: Alert[rust/non-https-url] |
+| main.rs:36:30:36:51 | ...::get | Unexpected result: Alert |
+| main.rs:59:15:59:49 | "http://example.com/sensitive-... | Unexpected result: Source |
+| main.rs:59:52:59:81 | //... | Missing result: Alert[rust/non-https-url] |
+| main.rs:60:21:60:42 | ...::get | Unexpected result: Alert |
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.qlref b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.qlref
new file mode 100644
index 00000000000..90b53330019
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.qlref
@@ -0,0 +1,4 @@
+query: queries/security/CWE-319/UseOfHttp.ql
+postprocess:
+ - utils/test/PrettyPrintModels.ql
+ - utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
new file mode 100644
index 00000000000..ae58967a49b
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -0,0 +1,65 @@
+use reqwest;
+use std::env;
+
+fn main() {
+ test_direct_literals();
+ test_dynamic_urls();
+ test_localhost_exemptions();
+}
+
+fn test_direct_literals() {
+ // BAD: Direct HTTP URLs that should be flagged
+ let _response1 = reqwest::blocking::get("http://example.com/api").unwrap(); // $ Alert[rust/non-https-url]
+ let _response2 = reqwest::blocking::get("http://api.example.com/data").unwrap(); // $ Alert[rust/non-https-url]
+
+ // GOOD: HTTPS URLs that should not be flagged
+ let _response3 = reqwest::blocking::get("https://example.com/api").unwrap();
+ let _response4 = reqwest::blocking::get("https://api.example.com/data").unwrap();
+}
+
+fn test_dynamic_urls() {
+ // BAD: HTTP URLs constructed dynamically
+ let base_url = "http://example.com"; // $ Alert[rust/non-https-url]
+ let endpoint = "/api/users";
+ let full_url = format!("{}{}", base_url, endpoint);
+ let _response = reqwest::blocking::get(&full_url).unwrap();
+
+ // GOOD: HTTPS URLs constructed dynamically
+ let secure_base = "https://example.com";
+ let secure_full = format!("{}{}", secure_base, endpoint);
+ let _secure_response = reqwest::blocking::get(&secure_full).unwrap();
+
+ // BAD: HTTP protocol string
+ let protocol = "http://"; // $ Alert[rust/non-https-url]
+ let host = "api.example.com";
+ let insecure_url = format!("{}{}", protocol, host);
+ let _insecure_response = reqwest::blocking::get(&insecure_url).unwrap();
+
+ // GOOD: HTTPS protocol string
+ let secure_protocol = "https://";
+ let secure_url = format!("{}{}", secure_protocol, host);
+ let _secure_response2 = reqwest::blocking::get(&secure_url).unwrap();
+}
+
+fn test_localhost_exemptions() {
+ // GOOD: localhost URLs should not be flagged (local development)
+ let _local1 = reqwest::blocking::get("http://localhost:8080/api").unwrap();
+ let _local2 = reqwest::blocking::get("http://127.0.0.1:3000/test").unwrap();
+ let _local3 = reqwest::blocking::get("http://192.168.1.100/internal").unwrap();
+ let _local4 = reqwest::blocking::get("http://10.0.0.1/admin").unwrap();
+
+ // Test IPv6 localhost variants
+ let _local5 = reqwest::blocking::get("http://[::1]:8080/api").unwrap();
+ let _local6 = reqwest::blocking::get("http://[0:0:0:0:0:0:0:1]/test").unwrap();
+}
+
+// Additional test cases that mirror the Bad/Good examples
+fn test_examples() {
+ // From UseOfHttpBad.rs - BAD case
+ let url = "http://example.com/sensitive-data"; // $ Alert[rust/non-https-url]
+ let _response = reqwest::blocking::get(url).unwrap();
+
+ // From UseOfHttpGood.rs - GOOD case
+ let secure_url = "https://example.com/sensitive-data";
+ let _secure_response = reqwest::blocking::get(secure_url).unwrap();
+}
\ No newline at end of file
diff --git a/rust/ql/test/query-tests/security/CWE-319/options.yml b/rust/ql/test/query-tests/security/CWE-319/options.yml
new file mode 100644
index 00000000000..aa57719603d
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-319/options.yml
@@ -0,0 +1,3 @@
+qltest_cargo_check: true
+qltest_dependencies:
+ - reqwest = { version = "0.12.9", features = ["blocking"] }
\ No newline at end of file
From a8d4d6b5630f7bd4804c51aefd8b0f84ef00ffe1 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Mon, 15 Sep 2025 22:02:03 -0400
Subject: [PATCH 09/90] Apply naming standards + changenote
---
.../2025-09-15-grape-framework-support.md | 4 ++
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 44 +++++++++----------
.../frameworks/grape/Grape.expected | 2 +-
.../library-tests/frameworks/grape/Grape.ql | 4 +-
4 files changed, 29 insertions(+), 25 deletions(-)
create mode 100644 ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
diff --git a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
new file mode 100644
index 00000000000..258da40d36c
--- /dev/null
+++ b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
@@ -0,0 +1,4 @@
+---
+category: feature
+---
+* Initial modeling for the Ruby Grape framework in `Grape.qll` have been added to detect API endpoints, parameters, and headers within Grape API classes.
\ No newline at end of file
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index fbab28180b8..72dd1e13b9b 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -23,9 +23,9 @@ module Grape {
* A Grape API class which sits at the top of the class hierarchy.
* In other words, it does not subclass any other Grape API class in source code.
*/
- class RootAPI extends GrapeAPIClass {
- RootAPI() {
- not exists(GrapeAPIClass parent | this != parent and this = parent.getADescendent())
+ class RootApi extends GrapeApiClass {
+ RootApi() {
+ not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent())
}
}
}
@@ -43,17 +43,17 @@ module Grape {
* end
* ```
*/
-class GrapeAPIClass extends DataFlow::ClassNode {
- GrapeAPIClass() {
- this = grapeAPIBaseClass().getADescendentModule() and
- not exists(DataFlow::ModuleNode m | m = grapeAPIBaseClass().asModule() | this = m)
+class GrapeApiClass extends DataFlow::ClassNode {
+ GrapeApiClass() {
+ this = grapeApiBaseClass().getADescendentModule() and
+ not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m)
}
/**
* Gets a `GrapeEndpoint` defined in this class.
*/
GrapeEndpoint getAnEndpoint() {
- result.getAPIClass() = this
+ result.getApiClass() = this
}
/**
@@ -68,19 +68,19 @@ class GrapeAPIClass extends DataFlow::ClassNode {
}
}
-private DataFlow::ConstRef grapeAPIBaseClass() {
+private DataFlow::ConstRef grapeApiBaseClass() {
result = DataFlow::getConstant("Grape").getConstant("API")
}
-private API::Node grapeAPIInstance() {
- result = any(GrapeAPIClass cls).getSelf().track()
+private API::Node grapeApiInstance() {
+ result = any(GrapeApiClass cls).getSelf().track()
}
/**
* A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
*/
class GrapeEndpoint extends DataFlow::CallNode {
- private GrapeAPIClass apiClass;
+ private GrapeApiClass apiClass;
GrapeEndpoint() {
this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
@@ -96,7 +96,7 @@ class GrapeEndpoint extends DataFlow::CallNode {
/**
* Gets the API class containing this endpoint.
*/
- GrapeAPIClass getAPIClass() { result = apiClass }
+ GrapeApiClass getApiClass() { result = apiClass }
/**
* Gets the block containing the endpoint logic.
@@ -131,7 +131,7 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
private class GrapeParamsCall extends ParamsCallImpl {
GrapeParamsCall() {
// Simplified approach: find params calls that are descendants of Grape API class methods
- exists(GrapeAPIClass api |
+ exists(GrapeApiClass api |
this.getMethodName() = "params" and
this.getParent+() = api.getADeclaration()
)
@@ -163,7 +163,7 @@ private class GrapeHeadersCall extends MethodCall {
)
or
// Also handle cases where headers is called on an instance of a Grape API class
- this = grapeAPIInstance().getAMethodCall("headers").asExpr().getExpr()
+ this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr()
}
}
@@ -206,7 +206,7 @@ private class GrapeRequestCall extends MethodCall {
)
or
// Also handle cases where request is called on an instance of a Grape API class
- this = grapeAPIInstance().getAMethodCall("request").asExpr().getExpr()
+ this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr()
}
}
@@ -221,7 +221,7 @@ private class GrapeRouteParamCall extends MethodCall {
)
or
// Also handle cases where route_param is called on an instance of a Grape API class
- this = grapeAPIInstance().getAMethodCall("route_param").asExpr().getExpr()
+ this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr()
}
}
@@ -231,7 +231,7 @@ private class GrapeRouteParamCall extends MethodCall {
*/
private class GrapeHeadersBlockCall extends MethodCall {
GrapeHeadersBlockCall() {
- exists(GrapeAPIClass api |
+ exists(GrapeApiClass api |
this.getParent+() = api.getADeclaration() and
this.getMethodName() = "headers" and
exists(this.getBlock())
@@ -245,7 +245,7 @@ private class GrapeHeadersBlockCall extends MethodCall {
*/
private class GrapeCookiesBlockCall extends MethodCall {
GrapeCookiesBlockCall() {
- exists(GrapeAPIClass api |
+ exists(GrapeApiClass api |
this.getParent+() = api.getADeclaration() and
this.getMethodName() = "cookies" and
exists(this.getBlock())
@@ -280,7 +280,7 @@ private class GrapeCookiesCall extends MethodCall {
)
or
// Also handle cases where cookies is called on an instance of a Grape API class
- this = grapeAPIInstance().getAMethodCall("cookies").asExpr().getExpr()
+ this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr()
}
}
@@ -289,7 +289,7 @@ private class GrapeCookiesCall extends MethodCall {
* These methods become available in endpoint contexts through Grape's DSL.
*/
private class GrapeHelperMethod extends Method {
- private GrapeAPIClass apiClass;
+ private GrapeApiClass apiClass;
GrapeHelperMethod() {
exists(DataFlow::CallNode helpersCall |
@@ -301,7 +301,7 @@ private class GrapeHelperMethod extends Method {
/**
* Gets the API class that contains this helper method.
*/
- GrapeAPIClass getAPIClass() { result = apiClass }
+ GrapeApiClass getAPIClass() { result = apiClass }
}
/**
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
index c0bee75371c..af4d936e88d 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
@@ -1,4 +1,4 @@
-grapeAPIClasses
+grapeApiClasses
| app.rb:1:1:90:3 | MyAPI |
| app.rb:92:1:96:3 | AdminAPI |
grapeEndpoints
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
index 63d59d0bdd7..ebfb304dbe7 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -3,9 +3,9 @@ import codeql.ruby.frameworks.Grape
import codeql.ruby.Concepts
import codeql.ruby.AST
-query predicate grapeAPIClasses(GrapeAPIClass api) { any() }
+query predicate grapeApiClasses(GrapeApiClass api) { any() }
-query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) {
+query predicate grapeEndpoints(GrapeApiClass api, GrapeEndpoint endpoint, string method, string path) {
endpoint = api.getAnEndpoint() and
method = endpoint.getHttpMethod() and
path = endpoint.getPath()
From 19cb1874368723b7441b01bb07d6aaf16f8c009d Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Mon, 15 Sep 2025 22:03:27 -0400
Subject: [PATCH 10/90] Update ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 72dd1e13b9b..417d4ee4da4 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -136,7 +136,9 @@ private class GrapeParamsCall extends ParamsCallImpl {
this.getParent+() = api.getADeclaration()
)
}
-}/**
+}
+
+/**
* A call to `headers` from within a Grape API endpoint or headers block.
* Headers can also be a source of user input.
*/
From fc98cd8d08e9f1d258611757094f73b00095963e Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Mon, 15 Sep 2025 22:11:33 -0400
Subject: [PATCH 11/90] Fix naming standards
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 72dd1e13b9b..7b963c92ee1 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -301,7 +301,7 @@ private class GrapeHelperMethod extends Method {
/**
* Gets the API class that contains this helper method.
*/
- GrapeApiClass getAPIClass() { result = apiClass }
+ GrapeApiClass getApiClass() { result = apiClass }
}
/**
From 7b04cf1a73ddb561738e31ba5b31348ced20c626 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 12:15:44 +0100
Subject: [PATCH 12/90] Rust: Fix up the test annotations.
---
.../security/CWE-319/UseOfHttp.expected | 10 -------
.../test/query-tests/security/CWE-319/main.rs | 30 +++++++++----------
2 files changed, 15 insertions(+), 25 deletions(-)
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
index 53cc8606cc8..f2a2e7e05f4 100644
--- a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -66,13 +66,3 @@ nodes
| main.rs:60:21:60:42 | ...::get | semmle.label | ...::get |
| main.rs:60:44:60:46 | url | semmle.label | url |
subpaths
-testFailures
-| main.rs:22:20:22:39 | "http://example.com" | Unexpected result: Source |
-| main.rs:22:42:22:71 | //... | Missing result: Alert[rust/non-https-url] |
-| main.rs:25:21:25:42 | ...::get | Unexpected result: Alert |
-| main.rs:33:20:33:28 | "http://" | Unexpected result: Source |
-| main.rs:33:31:33:60 | //... | Missing result: Alert[rust/non-https-url] |
-| main.rs:36:30:36:51 | ...::get | Unexpected result: Alert |
-| main.rs:59:15:59:49 | "http://example.com/sensitive-... | Unexpected result: Source |
-| main.rs:59:52:59:81 | //... | Missing result: Alert[rust/non-https-url] |
-| main.rs:60:21:60:42 | ...::get | Unexpected result: Alert |
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
index ae58967a49b..52f744e39a1 100644
--- a/rust/ql/test/query-tests/security/CWE-319/main.rs
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -11,30 +11,30 @@ fn test_direct_literals() {
// BAD: Direct HTTP URLs that should be flagged
let _response1 = reqwest::blocking::get("http://example.com/api").unwrap(); // $ Alert[rust/non-https-url]
let _response2 = reqwest::blocking::get("http://api.example.com/data").unwrap(); // $ Alert[rust/non-https-url]
-
- // GOOD: HTTPS URLs that should not be flagged
+
+ // GOOD: HTTPS URLs that should not be flagged
let _response3 = reqwest::blocking::get("https://example.com/api").unwrap();
let _response4 = reqwest::blocking::get("https://api.example.com/data").unwrap();
}
fn test_dynamic_urls() {
// BAD: HTTP URLs constructed dynamically
- let base_url = "http://example.com"; // $ Alert[rust/non-https-url]
+ let base_url = "http://example.com"; // $ Source
let endpoint = "/api/users";
let full_url = format!("{}{}", base_url, endpoint);
- let _response = reqwest::blocking::get(&full_url).unwrap();
-
+ let _response = reqwest::blocking::get(&full_url).unwrap(); // $ Alert[rust/non-https-url]
+
// GOOD: HTTPS URLs constructed dynamically
let secure_base = "https://example.com";
let secure_full = format!("{}{}", secure_base, endpoint);
let _secure_response = reqwest::blocking::get(&secure_full).unwrap();
-
+
// BAD: HTTP protocol string
- let protocol = "http://"; // $ Alert[rust/non-https-url]
+ let protocol = "http://"; // $ Source
let host = "api.example.com";
let insecure_url = format!("{}{}", protocol, host);
- let _insecure_response = reqwest::blocking::get(&insecure_url).unwrap();
-
+ let _insecure_response = reqwest::blocking::get(&insecure_url).unwrap(); // $ Alert[rust/non-https-url]
+
// GOOD: HTTPS protocol string
let secure_protocol = "https://";
let secure_url = format!("{}{}", secure_protocol, host);
@@ -47,7 +47,7 @@ fn test_localhost_exemptions() {
let _local2 = reqwest::blocking::get("http://127.0.0.1:3000/test").unwrap();
let _local3 = reqwest::blocking::get("http://192.168.1.100/internal").unwrap();
let _local4 = reqwest::blocking::get("http://10.0.0.1/admin").unwrap();
-
+
// Test IPv6 localhost variants
let _local5 = reqwest::blocking::get("http://[::1]:8080/api").unwrap();
let _local6 = reqwest::blocking::get("http://[0:0:0:0:0:0:0:1]/test").unwrap();
@@ -56,10 +56,10 @@ fn test_localhost_exemptions() {
// Additional test cases that mirror the Bad/Good examples
fn test_examples() {
// From UseOfHttpBad.rs - BAD case
- let url = "http://example.com/sensitive-data"; // $ Alert[rust/non-https-url]
- let _response = reqwest::blocking::get(url).unwrap();
-
- // From UseOfHttpGood.rs - GOOD case
+ let url = "http://example.com/sensitive-data"; // $ Source
+ let _response = reqwest::blocking::get(url).unwrap(); // $ Alert[rust/non-https-url]
+
+ // From UseOfHttpGood.rs - GOOD case
let secure_url = "https://example.com/sensitive-data";
let _secure_response = reqwest::blocking::get(secure_url).unwrap();
-}
\ No newline at end of file
+}
From 0924dec545a0eff7113574d629f4c5f2b99007be Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:21:57 +0100
Subject: [PATCH 13/90] Rust: Make the tests of the example code closer to the
actual example code.
---
.../security/CWE-319/UseOfHttp.expected | 16 ++++++++--------
.../test/query-tests/security/CWE-319/main.rs | 18 ++++++++++++++----
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
index f2a2e7e05f4..e8b7d301335 100644
--- a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -3,7 +3,7 @@
| main.rs:13:22:13:43 | ...::get | main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:13:45:13:73 | "http://api.example.com/data" | this HTTP URL |
| main.rs:25:21:25:42 | ...::get | main.rs:22:20:22:39 | "http://example.com" | main.rs:25:21:25:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:22:20:22:39 | "http://example.com" | this HTTP URL |
| main.rs:36:30:36:51 | ...::get | main.rs:33:20:33:28 | "http://" | main.rs:36:30:36:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:33:20:33:28 | "http://" | this HTTP URL |
-| main.rs:60:21:60:42 | ...::get | main.rs:59:15:59:49 | "http://example.com/sensitive-... | main.rs:60:21:60:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:59:15:59:49 | "http://example.com/sensitive-... | this HTTP URL |
+| main.rs:63:24:63:45 | ...::get | main.rs:60:19:60:53 | "http://example.com/sensitive-... | main.rs:63:24:63:45 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:60:19:60:53 | "http://example.com/sensitive-... | this HTTP URL |
edges
| main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
@@ -27,9 +27,9 @@ edges
| main.rs:35:32:35:53 | { ... } | main.rs:35:32:35:53 | ...::must_use(...) | provenance | MaD:3 |
| main.rs:36:53:36:65 | &insecure_url [&ref] | main.rs:36:30:36:51 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:36:54:36:65 | insecure_url | main.rs:36:53:36:65 | &insecure_url [&ref] | provenance | |
-| main.rs:59:9:59:11 | url | main.rs:60:44:60:46 | url | provenance | |
-| main.rs:59:15:59:49 | "http://example.com/sensitive-... | main.rs:59:9:59:11 | url | provenance | |
-| main.rs:60:44:60:46 | url | main.rs:60:21:60:42 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:60:13:60:15 | url | main.rs:63:47:63:49 | url | provenance | |
+| main.rs:60:19:60:53 | "http://example.com/sensitive-... | main.rs:60:13:60:15 | url | provenance | |
+| main.rs:63:47:63:49 | url | main.rs:63:24:63:45 | ...::get | provenance | MaD:1 Sink:MaD:1 |
models
| 1 | Sink: reqwest::blocking::get; Argument[0]; request-url |
| 2 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
@@ -61,8 +61,8 @@ nodes
| main.rs:36:30:36:51 | ...::get | semmle.label | ...::get |
| main.rs:36:53:36:65 | &insecure_url [&ref] | semmle.label | &insecure_url [&ref] |
| main.rs:36:54:36:65 | insecure_url | semmle.label | insecure_url |
-| main.rs:59:9:59:11 | url | semmle.label | url |
-| main.rs:59:15:59:49 | "http://example.com/sensitive-... | semmle.label | "http://example.com/sensitive-... |
-| main.rs:60:21:60:42 | ...::get | semmle.label | ...::get |
-| main.rs:60:44:60:46 | url | semmle.label | url |
+| main.rs:60:13:60:15 | url | semmle.label | url |
+| main.rs:60:19:60:53 | "http://example.com/sensitive-... | semmle.label | "http://example.com/sensitive-... |
+| main.rs:63:24:63:45 | ...::get | semmle.label | ...::get |
+| main.rs:63:47:63:49 | url | semmle.label | url |
subpaths
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
index 52f744e39a1..cec94840f29 100644
--- a/rust/ql/test/query-tests/security/CWE-319/main.rs
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -56,10 +56,20 @@ fn test_localhost_exemptions() {
// Additional test cases that mirror the Bad/Good examples
fn test_examples() {
// From UseOfHttpBad.rs - BAD case
- let url = "http://example.com/sensitive-data"; // $ Source
- let _response = reqwest::blocking::get(url).unwrap(); // $ Alert[rust/non-https-url]
+ {
+ let url = "http://example.com/sensitive-data"; // $ Source
+
+ // This makes an insecure HTTP request that can be intercepted
+ let response = reqwest::blocking::get(url).unwrap(); // $ Alert[rust/non-https-url]
+ println!("Response: {}", response.text().unwrap());
+ }
// From UseOfHttpGood.rs - GOOD case
- let secure_url = "https://example.com/sensitive-data";
- let _secure_response = reqwest::blocking::get(secure_url).unwrap();
+ {
+ let url = "https://example.com/sensitive-data";
+
+ // This makes a secure HTTPS request that is encrypted
+ let response = reqwest::blocking::get(url).unwrap();
+ println!("Response: {}", response.text().unwrap());
+ }
}
From 9c7fc583373c353f5bf6cf35d055af3b6f28ad0e Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 12:06:05 +0100
Subject: [PATCH 14/90] Rust: Add tests for a few more edge cases.
---
.../security/CWE-319/UseOfHttp.expected | 120 ++++++++++--------
.../test/query-tests/security/CWE-319/main.rs | 22 +++-
2 files changed, 79 insertions(+), 63 deletions(-)
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
index e8b7d301335..216d11b3606 100644
--- a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -1,35 +1,39 @@
#select
| main.rs:12:22:12:43 | ...::get | main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:12:45:12:68 | "http://example.com/api" | this HTTP URL |
-| main.rs:13:22:13:43 | ...::get | main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:13:45:13:73 | "http://api.example.com/data" | this HTTP URL |
-| main.rs:25:21:25:42 | ...::get | main.rs:22:20:22:39 | "http://example.com" | main.rs:25:21:25:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:22:20:22:39 | "http://example.com" | this HTTP URL |
-| main.rs:36:30:36:51 | ...::get | main.rs:33:20:33:28 | "http://" | main.rs:36:30:36:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:33:20:33:28 | "http://" | this HTTP URL |
-| main.rs:63:24:63:45 | ...::get | main.rs:60:19:60:53 | "http://example.com/sensitive-... | main.rs:63:24:63:45 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:60:19:60:53 | "http://example.com/sensitive-... | this HTTP URL |
+| main.rs:14:22:14:43 | ...::get | main.rs:14:45:14:73 | "http://api.example.com/data" | main.rs:14:22:14:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:14:45:14:73 | "http://api.example.com/data" | this HTTP URL |
+| main.rs:26:21:26:42 | ...::get | main.rs:23:20:23:39 | "http://example.com" | main.rs:26:21:26:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:23:20:23:39 | "http://example.com" | this HTTP URL |
+| main.rs:37:30:37:51 | ...::get | main.rs:34:20:34:28 | "http://" | main.rs:37:30:37:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:34:20:34:28 | "http://" | this HTTP URL |
+| main.rs:53:19:53:40 | ...::get | main.rs:53:42:53:68 | "http://172.31.255.255/bar" | main.rs:53:19:53:40 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:53:42:53:68 | "http://172.31.255.255/bar" | this HTTP URL |
+| main.rs:60:20:60:41 | ...::get | main.rs:60:43:60:65 | "http://172.32.0.0/baz" | main.rs:60:20:60:41 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:60:43:60:65 | "http://172.32.0.0/baz" | this HTTP URL |
+| main.rs:71:24:71:45 | ...::get | main.rs:68:19:68:53 | "http://example.com/sensitive-... | main.rs:71:24:71:45 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:68:19:68:53 | "http://example.com/sensitive-... | this HTTP URL |
edges
| main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
-| main.rs:13:45:13:73 | "http://api.example.com/data" | main.rs:13:22:13:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
-| main.rs:22:9:22:16 | base_url | main.rs:24:28:24:53 | MacroExpr | provenance | |
-| main.rs:22:20:22:39 | "http://example.com" | main.rs:22:9:22:16 | base_url | provenance | |
-| main.rs:24:9:24:16 | full_url | main.rs:25:45:25:52 | full_url | provenance | |
-| main.rs:24:20:24:26 | res | main.rs:24:28:24:53 | { ... } | provenance | |
-| main.rs:24:28:24:53 | ...::format(...) | main.rs:24:20:24:26 | res | provenance | |
-| main.rs:24:28:24:53 | ...::must_use(...) | main.rs:24:9:24:16 | full_url | provenance | |
-| main.rs:24:28:24:53 | MacroExpr | main.rs:24:28:24:53 | ...::format(...) | provenance | MaD:2 |
-| main.rs:24:28:24:53 | { ... } | main.rs:24:28:24:53 | ...::must_use(...) | provenance | MaD:3 |
-| main.rs:25:44:25:52 | &full_url [&ref] | main.rs:25:21:25:42 | ...::get | provenance | MaD:1 Sink:MaD:1 |
-| main.rs:25:45:25:52 | full_url | main.rs:25:44:25:52 | &full_url [&ref] | provenance | |
-| main.rs:33:9:33:16 | protocol | main.rs:35:32:35:53 | MacroExpr | provenance | |
-| main.rs:33:20:33:28 | "http://" | main.rs:33:9:33:16 | protocol | provenance | |
-| main.rs:35:9:35:20 | insecure_url | main.rs:36:54:36:65 | insecure_url | provenance | |
-| main.rs:35:24:35:30 | res | main.rs:35:32:35:53 | { ... } | provenance | |
-| main.rs:35:32:35:53 | ...::format(...) | main.rs:35:24:35:30 | res | provenance | |
-| main.rs:35:32:35:53 | ...::must_use(...) | main.rs:35:9:35:20 | insecure_url | provenance | |
-| main.rs:35:32:35:53 | MacroExpr | main.rs:35:32:35:53 | ...::format(...) | provenance | MaD:2 |
-| main.rs:35:32:35:53 | { ... } | main.rs:35:32:35:53 | ...::must_use(...) | provenance | MaD:3 |
-| main.rs:36:53:36:65 | &insecure_url [&ref] | main.rs:36:30:36:51 | ...::get | provenance | MaD:1 Sink:MaD:1 |
-| main.rs:36:54:36:65 | insecure_url | main.rs:36:53:36:65 | &insecure_url [&ref] | provenance | |
-| main.rs:60:13:60:15 | url | main.rs:63:47:63:49 | url | provenance | |
-| main.rs:60:19:60:53 | "http://example.com/sensitive-... | main.rs:60:13:60:15 | url | provenance | |
-| main.rs:63:47:63:49 | url | main.rs:63:24:63:45 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:14:45:14:73 | "http://api.example.com/data" | main.rs:14:22:14:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:23:9:23:16 | base_url | main.rs:25:28:25:53 | MacroExpr | provenance | |
+| main.rs:23:20:23:39 | "http://example.com" | main.rs:23:9:23:16 | base_url | provenance | |
+| main.rs:25:9:25:16 | full_url | main.rs:26:45:26:52 | full_url | provenance | |
+| main.rs:25:20:25:26 | res | main.rs:25:28:25:53 | { ... } | provenance | |
+| main.rs:25:28:25:53 | ...::format(...) | main.rs:25:20:25:26 | res | provenance | |
+| main.rs:25:28:25:53 | ...::must_use(...) | main.rs:25:9:25:16 | full_url | provenance | |
+| main.rs:25:28:25:53 | MacroExpr | main.rs:25:28:25:53 | ...::format(...) | provenance | MaD:2 |
+| main.rs:25:28:25:53 | { ... } | main.rs:25:28:25:53 | ...::must_use(...) | provenance | MaD:3 |
+| main.rs:26:44:26:52 | &full_url [&ref] | main.rs:26:21:26:42 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:26:45:26:52 | full_url | main.rs:26:44:26:52 | &full_url [&ref] | provenance | |
+| main.rs:34:9:34:16 | protocol | main.rs:36:32:36:53 | MacroExpr | provenance | |
+| main.rs:34:20:34:28 | "http://" | main.rs:34:9:34:16 | protocol | provenance | |
+| main.rs:36:9:36:20 | insecure_url | main.rs:37:54:37:65 | insecure_url | provenance | |
+| main.rs:36:24:36:30 | res | main.rs:36:32:36:53 | { ... } | provenance | |
+| main.rs:36:32:36:53 | ...::format(...) | main.rs:36:24:36:30 | res | provenance | |
+| main.rs:36:32:36:53 | ...::must_use(...) | main.rs:36:9:36:20 | insecure_url | provenance | |
+| main.rs:36:32:36:53 | MacroExpr | main.rs:36:32:36:53 | ...::format(...) | provenance | MaD:2 |
+| main.rs:36:32:36:53 | { ... } | main.rs:36:32:36:53 | ...::must_use(...) | provenance | MaD:3 |
+| main.rs:37:53:37:65 | &insecure_url [&ref] | main.rs:37:30:37:51 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:37:54:37:65 | insecure_url | main.rs:37:53:37:65 | &insecure_url [&ref] | provenance | |
+| main.rs:53:42:53:68 | "http://172.31.255.255/bar" | main.rs:53:19:53:40 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:60:43:60:65 | "http://172.32.0.0/baz" | main.rs:60:20:60:41 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:68:13:68:15 | url | main.rs:71:47:71:49 | url | provenance | |
+| main.rs:68:19:68:53 | "http://example.com/sensitive-... | main.rs:68:13:68:15 | url | provenance | |
+| main.rs:71:47:71:49 | url | main.rs:71:24:71:45 | ...::get | provenance | MaD:1 Sink:MaD:1 |
models
| 1 | Sink: reqwest::blocking::get; Argument[0]; request-url |
| 2 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
@@ -37,32 +41,36 @@ models
nodes
| main.rs:12:22:12:43 | ...::get | semmle.label | ...::get |
| main.rs:12:45:12:68 | "http://example.com/api" | semmle.label | "http://example.com/api" |
-| main.rs:13:22:13:43 | ...::get | semmle.label | ...::get |
-| main.rs:13:45:13:73 | "http://api.example.com/data" | semmle.label | "http://api.example.com/data" |
-| main.rs:22:9:22:16 | base_url | semmle.label | base_url |
-| main.rs:22:20:22:39 | "http://example.com" | semmle.label | "http://example.com" |
-| main.rs:24:9:24:16 | full_url | semmle.label | full_url |
-| main.rs:24:20:24:26 | res | semmle.label | res |
-| main.rs:24:28:24:53 | ...::format(...) | semmle.label | ...::format(...) |
-| main.rs:24:28:24:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
-| main.rs:24:28:24:53 | MacroExpr | semmle.label | MacroExpr |
-| main.rs:24:28:24:53 | { ... } | semmle.label | { ... } |
-| main.rs:25:21:25:42 | ...::get | semmle.label | ...::get |
-| main.rs:25:44:25:52 | &full_url [&ref] | semmle.label | &full_url [&ref] |
-| main.rs:25:45:25:52 | full_url | semmle.label | full_url |
-| main.rs:33:9:33:16 | protocol | semmle.label | protocol |
-| main.rs:33:20:33:28 | "http://" | semmle.label | "http://" |
-| main.rs:35:9:35:20 | insecure_url | semmle.label | insecure_url |
-| main.rs:35:24:35:30 | res | semmle.label | res |
-| main.rs:35:32:35:53 | ...::format(...) | semmle.label | ...::format(...) |
-| main.rs:35:32:35:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
-| main.rs:35:32:35:53 | MacroExpr | semmle.label | MacroExpr |
-| main.rs:35:32:35:53 | { ... } | semmle.label | { ... } |
-| main.rs:36:30:36:51 | ...::get | semmle.label | ...::get |
-| main.rs:36:53:36:65 | &insecure_url [&ref] | semmle.label | &insecure_url [&ref] |
-| main.rs:36:54:36:65 | insecure_url | semmle.label | insecure_url |
-| main.rs:60:13:60:15 | url | semmle.label | url |
-| main.rs:60:19:60:53 | "http://example.com/sensitive-... | semmle.label | "http://example.com/sensitive-... |
-| main.rs:63:24:63:45 | ...::get | semmle.label | ...::get |
-| main.rs:63:47:63:49 | url | semmle.label | url |
+| main.rs:14:22:14:43 | ...::get | semmle.label | ...::get |
+| main.rs:14:45:14:73 | "http://api.example.com/data" | semmle.label | "http://api.example.com/data" |
+| main.rs:23:9:23:16 | base_url | semmle.label | base_url |
+| main.rs:23:20:23:39 | "http://example.com" | semmle.label | "http://example.com" |
+| main.rs:25:9:25:16 | full_url | semmle.label | full_url |
+| main.rs:25:20:25:26 | res | semmle.label | res |
+| main.rs:25:28:25:53 | ...::format(...) | semmle.label | ...::format(...) |
+| main.rs:25:28:25:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
+| main.rs:25:28:25:53 | MacroExpr | semmle.label | MacroExpr |
+| main.rs:25:28:25:53 | { ... } | semmle.label | { ... } |
+| main.rs:26:21:26:42 | ...::get | semmle.label | ...::get |
+| main.rs:26:44:26:52 | &full_url [&ref] | semmle.label | &full_url [&ref] |
+| main.rs:26:45:26:52 | full_url | semmle.label | full_url |
+| main.rs:34:9:34:16 | protocol | semmle.label | protocol |
+| main.rs:34:20:34:28 | "http://" | semmle.label | "http://" |
+| main.rs:36:9:36:20 | insecure_url | semmle.label | insecure_url |
+| main.rs:36:24:36:30 | res | semmle.label | res |
+| main.rs:36:32:36:53 | ...::format(...) | semmle.label | ...::format(...) |
+| main.rs:36:32:36:53 | ...::must_use(...) | semmle.label | ...::must_use(...) |
+| main.rs:36:32:36:53 | MacroExpr | semmle.label | MacroExpr |
+| main.rs:36:32:36:53 | { ... } | semmle.label | { ... } |
+| main.rs:37:30:37:51 | ...::get | semmle.label | ...::get |
+| main.rs:37:53:37:65 | &insecure_url [&ref] | semmle.label | &insecure_url [&ref] |
+| main.rs:37:54:37:65 | insecure_url | semmle.label | insecure_url |
+| main.rs:53:19:53:40 | ...::get | semmle.label | ...::get |
+| main.rs:53:42:53:68 | "http://172.31.255.255/bar" | semmle.label | "http://172.31.255.255/bar" |
+| main.rs:60:20:60:41 | ...::get | semmle.label | ...::get |
+| main.rs:60:43:60:65 | "http://172.32.0.0/baz" | semmle.label | "http://172.32.0.0/baz" |
+| main.rs:68:13:68:15 | url | semmle.label | url |
+| main.rs:68:19:68:53 | "http://example.com/sensitive-... | semmle.label | "http://example.com/sensitive-... |
+| main.rs:71:24:71:45 | ...::get | semmle.label | ...::get |
+| main.rs:71:47:71:49 | url | semmle.label | url |
subpaths
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
index cec94840f29..0dd59ce0880 100644
--- a/rust/ql/test/query-tests/security/CWE-319/main.rs
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -10,7 +10,8 @@ fn main() {
fn test_direct_literals() {
// BAD: Direct HTTP URLs that should be flagged
let _response1 = reqwest::blocking::get("http://example.com/api").unwrap(); // $ Alert[rust/non-https-url]
- let _response2 = reqwest::blocking::get("http://api.example.com/data").unwrap(); // $ Alert[rust/non-https-url]
+ let _response2 = reqwest::blocking::get("HTTP://EXAMPLE.COM/API").unwrap(); // $ MISSING: Alert[rust/non-https-url]
+ let _response3 = reqwest::blocking::get("http://api.example.com/data").unwrap(); // $ Alert[rust/non-https-url]
// GOOD: HTTPS URLs that should not be flagged
let _response3 = reqwest::blocking::get("https://example.com/api").unwrap();
@@ -44,13 +45,20 @@ fn test_dynamic_urls() {
fn test_localhost_exemptions() {
// GOOD: localhost URLs should not be flagged (local development)
let _local1 = reqwest::blocking::get("http://localhost:8080/api").unwrap();
- let _local2 = reqwest::blocking::get("http://127.0.0.1:3000/test").unwrap();
- let _local3 = reqwest::blocking::get("http://192.168.1.100/internal").unwrap();
- let _local4 = reqwest::blocking::get("http://10.0.0.1/admin").unwrap();
+ let _local2 = reqwest::blocking::get("HTTP://LOCALHOST:8080/api").unwrap();
+ let _local3 = reqwest::blocking::get("http://127.0.0.1:3000/test").unwrap();
+ let _local4 = reqwest::blocking::get("http://192.168.1.100/internal").unwrap();
+ let _local5 = reqwest::blocking::get("http://10.0.0.1/admin").unwrap();
+ let _local6 = reqwest::blocking::get("http://172.16.0.0/foo").unwrap();
+ let _local7 = reqwest::blocking::get("http://172.31.255.255/bar").unwrap(); // $ SPURIOUS: Alert[rust/non-https-url]
+
+ // GOOD: test IPv6 localhost variants
+ let _local8 = reqwest::blocking::get("http://[::1]:8080/api").unwrap();
+ let _local9 = reqwest::blocking::get("http://[0:0:0:0:0:0:0:1]/test").unwrap();
+
+ // BAD: non-private IP address
+ let _local10 = reqwest::blocking::get("http://172.32.0.0/baz").unwrap(); // $ Alert[rust/non-https-url]
- // Test IPv6 localhost variants
- let _local5 = reqwest::blocking::get("http://[::1]:8080/api").unwrap();
- let _local6 = reqwest::blocking::get("http://[0:0:0:0:0:0:0:1]/test").unwrap();
}
// Additional test cases that mirror the Bad/Good examples
From 0f5aa857b874b22bfb53b67b08be2336e14af351 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:32:52 +0100
Subject: [PATCH 15/90] Rust: Remove unnecessary import.
---
rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll | 1 -
1 file changed, 1 deletion(-)
diff --git a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
index 026880785b6..8001e6270dd 100644
--- a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
+++ b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
@@ -6,7 +6,6 @@
import rust
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.dataflow.FlowSink
-private import codeql.rust.elements.LiteralExprExt
private import codeql.rust.Concepts
/**
From 0b900711bf1d3df6d5d16d7cd7688495b2ca60dd Mon Sep 17 00:00:00 2001
From: Asger F
Date: Tue, 16 Sep 2025 13:48:26 +0200
Subject: [PATCH 16/90] Update
javascript/ql/lib/semmle/javascript/frameworks/Express.qll
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
javascript/ql/lib/semmle/javascript/frameworks/Express.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Express.qll b/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
index 41e4d1c860c..be3cb7b1ccb 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/Express.qll
@@ -803,7 +803,7 @@ module Express {
}
/**
- * An argument passed to the `json` or `json` method of an HTTP response object.
+ * An argument passed to the `json` or `jsonp` method of an HTTP response object.
*/
private class ResponseJsonCallArgument extends Http::ResponseSendArgument {
ResponseJsonCall call;
From 80ce55ab1072fb534776fc7b02a93ca6a2b18ce2 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:58:23 +0100
Subject: [PATCH 17/90] Rust: Make the private address spaces URL more
accurate.
---
rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll | 2 +-
rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected | 4 ----
rust/ql/test/query-tests/security/CWE-319/main.rs | 2 +-
3 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
index 8001e6270dd..58466dd0a4f 100644
--- a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
+++ b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
@@ -38,7 +38,7 @@ module UseOfHttp {
exists(string s | this.getTextValue() = s |
// Match HTTP URLs that are not private/local
s.regexpMatch("\"http://.*\"") and
- not s.regexpMatch("\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.16\\.[0-9]+\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
+ not s.regexpMatch("\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
)
}
}
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
index 216d11b3606..ef99b001fcf 100644
--- a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -3,7 +3,6 @@
| main.rs:14:22:14:43 | ...::get | main.rs:14:45:14:73 | "http://api.example.com/data" | main.rs:14:22:14:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:14:45:14:73 | "http://api.example.com/data" | this HTTP URL |
| main.rs:26:21:26:42 | ...::get | main.rs:23:20:23:39 | "http://example.com" | main.rs:26:21:26:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:23:20:23:39 | "http://example.com" | this HTTP URL |
| main.rs:37:30:37:51 | ...::get | main.rs:34:20:34:28 | "http://" | main.rs:37:30:37:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:34:20:34:28 | "http://" | this HTTP URL |
-| main.rs:53:19:53:40 | ...::get | main.rs:53:42:53:68 | "http://172.31.255.255/bar" | main.rs:53:19:53:40 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:53:42:53:68 | "http://172.31.255.255/bar" | this HTTP URL |
| main.rs:60:20:60:41 | ...::get | main.rs:60:43:60:65 | "http://172.32.0.0/baz" | main.rs:60:20:60:41 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:60:43:60:65 | "http://172.32.0.0/baz" | this HTTP URL |
| main.rs:71:24:71:45 | ...::get | main.rs:68:19:68:53 | "http://example.com/sensitive-... | main.rs:71:24:71:45 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:68:19:68:53 | "http://example.com/sensitive-... | this HTTP URL |
edges
@@ -29,7 +28,6 @@ edges
| main.rs:36:32:36:53 | { ... } | main.rs:36:32:36:53 | ...::must_use(...) | provenance | MaD:3 |
| main.rs:37:53:37:65 | &insecure_url [&ref] | main.rs:37:30:37:51 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:37:54:37:65 | insecure_url | main.rs:37:53:37:65 | &insecure_url [&ref] | provenance | |
-| main.rs:53:42:53:68 | "http://172.31.255.255/bar" | main.rs:53:19:53:40 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:60:43:60:65 | "http://172.32.0.0/baz" | main.rs:60:20:60:41 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:68:13:68:15 | url | main.rs:71:47:71:49 | url | provenance | |
| main.rs:68:19:68:53 | "http://example.com/sensitive-... | main.rs:68:13:68:15 | url | provenance | |
@@ -65,8 +63,6 @@ nodes
| main.rs:37:30:37:51 | ...::get | semmle.label | ...::get |
| main.rs:37:53:37:65 | &insecure_url [&ref] | semmle.label | &insecure_url [&ref] |
| main.rs:37:54:37:65 | insecure_url | semmle.label | insecure_url |
-| main.rs:53:19:53:40 | ...::get | semmle.label | ...::get |
-| main.rs:53:42:53:68 | "http://172.31.255.255/bar" | semmle.label | "http://172.31.255.255/bar" |
| main.rs:60:20:60:41 | ...::get | semmle.label | ...::get |
| main.rs:60:43:60:65 | "http://172.32.0.0/baz" | semmle.label | "http://172.32.0.0/baz" |
| main.rs:68:13:68:15 | url | semmle.label | url |
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
index 0dd59ce0880..908e6c61c2c 100644
--- a/rust/ql/test/query-tests/security/CWE-319/main.rs
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -50,7 +50,7 @@ fn test_localhost_exemptions() {
let _local4 = reqwest::blocking::get("http://192.168.1.100/internal").unwrap();
let _local5 = reqwest::blocking::get("http://10.0.0.1/admin").unwrap();
let _local6 = reqwest::blocking::get("http://172.16.0.0/foo").unwrap();
- let _local7 = reqwest::blocking::get("http://172.31.255.255/bar").unwrap(); // $ SPURIOUS: Alert[rust/non-https-url]
+ let _local7 = reqwest::blocking::get("http://172.31.255.255/bar").unwrap();
// GOOD: test IPv6 localhost variants
let _local8 = reqwest::blocking::get("http://[::1]:8080/api").unwrap();
From 4b281fdf12fb8a64cd194aee324ded70855b4695 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 13:02:54 +0100
Subject: [PATCH 18/90] Rust: Use case insensitive regexps.
---
rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll | 4 ++--
rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected | 4 ++++
rust/ql/test/query-tests/security/CWE-319/main.rs | 2 +-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
index 58466dd0a4f..5e0d534fb7d 100644
--- a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
+++ b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
@@ -37,8 +37,8 @@ module UseOfHttp {
HttpStringLiteral() {
exists(string s | this.getTextValue() = s |
// Match HTTP URLs that are not private/local
- s.regexpMatch("\"http://.*\"") and
- not s.regexpMatch("\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
+ s.regexpMatch("(?i)\"http://.*\"") and
+ not s.regexpMatch("(?i)\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
)
}
}
diff --git a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
index ef99b001fcf..952bd741d1c 100644
--- a/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
+++ b/rust/ql/test/query-tests/security/CWE-319/UseOfHttp.expected
@@ -1,5 +1,6 @@
#select
| main.rs:12:22:12:43 | ...::get | main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:12:45:12:68 | "http://example.com/api" | this HTTP URL |
+| main.rs:13:22:13:43 | ...::get | main.rs:13:45:13:68 | "HTTP://EXAMPLE.COM/API" | main.rs:13:22:13:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:13:45:13:68 | "HTTP://EXAMPLE.COM/API" | this HTTP URL |
| main.rs:14:22:14:43 | ...::get | main.rs:14:45:14:73 | "http://api.example.com/data" | main.rs:14:22:14:43 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:14:45:14:73 | "http://api.example.com/data" | this HTTP URL |
| main.rs:26:21:26:42 | ...::get | main.rs:23:20:23:39 | "http://example.com" | main.rs:26:21:26:42 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:23:20:23:39 | "http://example.com" | this HTTP URL |
| main.rs:37:30:37:51 | ...::get | main.rs:34:20:34:28 | "http://" | main.rs:37:30:37:51 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:34:20:34:28 | "http://" | this HTTP URL |
@@ -7,6 +8,7 @@
| main.rs:71:24:71:45 | ...::get | main.rs:68:19:68:53 | "http://example.com/sensitive-... | main.rs:71:24:71:45 | ...::get | This URL may be constructed with the HTTP protocol, from $@. | main.rs:68:19:68:53 | "http://example.com/sensitive-... | this HTTP URL |
edges
| main.rs:12:45:12:68 | "http://example.com/api" | main.rs:12:22:12:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
+| main.rs:13:45:13:68 | "HTTP://EXAMPLE.COM/API" | main.rs:13:22:13:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:14:45:14:73 | "http://api.example.com/data" | main.rs:14:22:14:43 | ...::get | provenance | MaD:1 Sink:MaD:1 |
| main.rs:23:9:23:16 | base_url | main.rs:25:28:25:53 | MacroExpr | provenance | |
| main.rs:23:20:23:39 | "http://example.com" | main.rs:23:9:23:16 | base_url | provenance | |
@@ -39,6 +41,8 @@ models
nodes
| main.rs:12:22:12:43 | ...::get | semmle.label | ...::get |
| main.rs:12:45:12:68 | "http://example.com/api" | semmle.label | "http://example.com/api" |
+| main.rs:13:22:13:43 | ...::get | semmle.label | ...::get |
+| main.rs:13:45:13:68 | "HTTP://EXAMPLE.COM/API" | semmle.label | "HTTP://EXAMPLE.COM/API" |
| main.rs:14:22:14:43 | ...::get | semmle.label | ...::get |
| main.rs:14:45:14:73 | "http://api.example.com/data" | semmle.label | "http://api.example.com/data" |
| main.rs:23:9:23:16 | base_url | semmle.label | base_url |
diff --git a/rust/ql/test/query-tests/security/CWE-319/main.rs b/rust/ql/test/query-tests/security/CWE-319/main.rs
index 908e6c61c2c..0a3539923da 100644
--- a/rust/ql/test/query-tests/security/CWE-319/main.rs
+++ b/rust/ql/test/query-tests/security/CWE-319/main.rs
@@ -10,7 +10,7 @@ fn main() {
fn test_direct_literals() {
// BAD: Direct HTTP URLs that should be flagged
let _response1 = reqwest::blocking::get("http://example.com/api").unwrap(); // $ Alert[rust/non-https-url]
- let _response2 = reqwest::blocking::get("HTTP://EXAMPLE.COM/API").unwrap(); // $ MISSING: Alert[rust/non-https-url]
+ let _response2 = reqwest::blocking::get("HTTP://EXAMPLE.COM/API").unwrap(); // $ Alert[rust/non-https-url]
let _response3 = reqwest::blocking::get("http://api.example.com/data").unwrap(); // $ Alert[rust/non-https-url]
// GOOD: HTTPS URLs that should not be flagged
From 0eb602aad2991216268e79bbe5db05472f2156c0 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 14:00:43 +0100
Subject: [PATCH 19/90] Rust: Update a redirected URL.
---
.../src/queries/security/CWE-319/UseOfHttp.qhelp | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
index a8ca1d9c7c7..a1345b189bb 100644
--- a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
@@ -6,21 +6,21 @@
Constructing URLs with the HTTP protocol can lead to unsecured connections.
-Furthermore, constructing URLs with the HTTP protocol can create problems if other parts of the
-code expect HTTPS URLs. A typical pattern is to use libraries that expect secure connections,
+
Furthermore, constructing URLs with the HTTP protocol can create problems if other parts of the
+code expect HTTPS URLs. A typical pattern is to use libraries that expect secure connections,
which may fail or fall back to insecure behavior when provided with HTTP URLs instead of HTTPS URLs.
-When you construct a URL for network requests, ensure that you use an HTTPS URL rather than an HTTP URL.
+
When you construct a URL for network requests, ensure that you use an HTTPS URL rather than an HTTP URL.
Then, any connections that are made using that URL are secure SSL/TLS connections.
-The following example shows two ways of making a network request using a URL. When the request is
-made using an HTTP URL rather than an HTTPS URL, the connection is unsecured and can be intercepted
+
The following example shows two ways of making a network request using a URL. When the request is
+made using an HTTP URL rather than an HTTPS URL, the connection is unsecured and can be intercepted
by attackers. When the request is made using an HTTPS URL, the connection is a secure SSL/TLS connection.
@@ -34,15 +34,15 @@ by attackers. When the request is made using an HTTPS URL, the connection is a s
OWASP:
-Transport Layer Protection Cheat Sheet.
+Transport Layer Security Cheat Sheet.
OWASP Top 10:
A08:2021 - Software and Data Integrity Failures.
-Rust reqwest documentation:
+Rust reqwest documentation:
reqwest crate.
-
\ No newline at end of file
+
From 31bf86fd1bcb60946c038d1329f132647bc60288 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 14:04:47 +0100
Subject: [PATCH 20/90] Rust: Improve the flow around the qhelp example.
---
rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
index a1345b189bb..e4e0fc5eaa9 100644
--- a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
@@ -19,13 +19,14 @@ Then, any connections that are made using that URL are secure SSL/TLS connection
-The following example shows two ways of making a network request using a URL. When the request is
+
The following examples show two ways of making a network request using a URL. When the request is
made using an HTTP URL rather than an HTTPS URL, the connection is unsecured and can be intercepted
-by attackers. When the request is made using an HTTPS URL, the connection is a secure SSL/TLS connection.
+by attackers:
-A better approach is to use HTTPS:
+A better approach is to use HTTPS. When the request is made using an HTTPS URL, the connection
+is a secure SSL/TLS connection:
From ffd32efba274f0b1400d592562f24cae4a286ddf Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Tue, 16 Sep 2025 09:08:07 -0400
Subject: [PATCH 21/90] codeql query format
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 37 ++++++-------------
.../library-tests/frameworks/grape/Grape.ql | 2 +-
2 files changed, 12 insertions(+), 27 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index faf762f53a0..ea7bc8c576c 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -52,9 +52,7 @@ class GrapeApiClass extends DataFlow::ClassNode {
/**
* Gets a `GrapeEndpoint` defined in this class.
*/
- GrapeEndpoint getAnEndpoint() {
- result.getApiClass() = this
- }
+ GrapeEndpoint getAnEndpoint() { result.getApiClass() = this }
/**
* Gets a `self` that possibly refers to an instance of this class.
@@ -72,9 +70,7 @@ private DataFlow::ConstRef grapeApiBaseClass() {
result = DataFlow::getConstant("Grape").getConstant("API")
}
-private API::Node grapeApiInstance() {
- result = any(GrapeApiClass cls).getSelf().track()
-}
+private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() }
/**
* A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
@@ -83,15 +79,14 @@ class GrapeEndpoint extends DataFlow::CallNode {
private GrapeApiClass apiClass;
GrapeEndpoint() {
- this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
+ this =
+ apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
}
/**
* Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
*/
- string getHttpMethod() {
- result = this.getMethodName().toUpperCase()
- }
+ string getHttpMethod() { result = this.getMethodName().toUpperCase() }
/**
* Gets the API class containing this endpoint.
@@ -106,9 +101,7 @@ class GrapeEndpoint extends DataFlow::CallNode {
/**
* Gets the path pattern for this endpoint, if specified.
*/
- string getPath() {
- result = this.getArgument(0).getConstantValue().getString()
- }
+ string getPath() { result = this.getArgument(0).getConstantValue().getString() }
}
/**
@@ -116,9 +109,7 @@ class GrapeEndpoint extends DataFlow::CallNode {
* Grape parameters available via the `params` method within an endpoint.
*/
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
- GrapeParamsSource() {
- this.asExpr().getExpr() instanceof GrapeParamsCall
- }
+ GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall }
override string getSourceType() { result = "Grape::API#params" }
@@ -174,9 +165,7 @@ private class GrapeHeadersCall extends MethodCall {
* The request object can contain user input.
*/
class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
- GrapeRequestSource() {
- this.asExpr().getExpr() instanceof GrapeRequestCall
- }
+ GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall }
override string getSourceType() { result = "Grape::API#request" }
@@ -188,9 +177,7 @@ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
* Route parameters are extracted from the URL path and can be a source of user input.
*/
class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
- GrapeRouteParamSource() {
- this.asExpr().getExpr() instanceof GrapeRouteParamCall
- }
+ GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall }
override string getSourceType() { result = "Grape::API#route_param" }
@@ -316,12 +303,10 @@ private class GrapeHelperMethodTaintStep extends AdditionalTaintStep {
exists(GrapeHelperMethod helperMethod, MethodCall call, int i |
// Find calls to helper methods from within Grape endpoints
call.getMethodName() = helperMethod.getName() and
- exists(GrapeEndpoint endpoint |
- call.getParent+() = endpoint.getBody().asExpr().getExpr()
- ) and
+ exists(GrapeEndpoint endpoint | call.getParent+() = endpoint.getBody().asExpr().getExpr()) and
// Map argument to parameter
nodeFrom.asExpr().getExpr() = call.getArgument(i) and
nodeTo.asParameter() = helperMethod.getParameter(i)
)
}
-}
\ No newline at end of file
+}
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
index ebfb304dbe7..c9aa7c29082 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -19,4 +19,4 @@ query predicate grapeRequest(GrapeRequestSource request) { any() }
query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() }
-query predicate grapeCookies(GrapeCookiesSource cookies) { any() }
\ No newline at end of file
+query predicate grapeCookies(GrapeCookiesSource cookies) { any() }
From 6f1fcbf41bf7484025a208af19a19630dc3a4df9 Mon Sep 17 00:00:00 2001
From: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
Date: Tue, 16 Sep 2025 17:08:22 +0100
Subject: [PATCH 22/90] Rust: Add IPv6 private address range (and explanatory
comments).
---
rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
index 5e0d534fb7d..bd91cde238f 100644
--- a/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
+++ b/rust/ql/lib/codeql/rust/security/UseOfHttpExtensions.qll
@@ -36,9 +36,12 @@ module UseOfHttp {
class HttpStringLiteral extends StringLiteralExpr {
HttpStringLiteral() {
exists(string s | this.getTextValue() = s |
- // Match HTTP URLs that are not private/local
+ // match HTTP URLs
s.regexpMatch("(?i)\"http://.*\"") and
- not s.regexpMatch("(?i)\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]).*\"")
+ // exclude private/local addresses:
+ // - IPv4: localhost / 127.0.0.1, 192.168.x.x, 10.x.x.x, 172.16.x.x -> 172.31.x.x
+ // - IPv6 (address inside []): ::1 (or 0:0:0:0:0:0:0:1), fc00::/7 (i.e. anything beginning `fcxx:` or `fdxx:`)
+ not s.regexpMatch("(?i)\"http://(localhost|127\\.0\\.0\\.1|192\\.168\\.[0-9]+\\.[0-9]+|10\\.[0-9]+\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+|\\[::1\\]|\\[0:0:0:0:0:0:0:1\\]|\\[f[cd][0-9a-f]{2}:.*\\]).*\"")
)
}
}
From c5e3be2c4cc30b1d8b92dfde67097577d4867dc0 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Tue, 16 Sep 2025 17:09:18 -0400
Subject: [PATCH 23/90] Grape - detect params calls inside helper methods -
added unit tests for flow using inline format - removed grape from Arel tests
(temporary)
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 28 ++++++-
.../frameworks/grape/Flow.expected | 77 +++++++++++++++++++
.../library-tests/frameworks/grape/Flow.ql | 25 ++++++
.../frameworks/grape/Grape.expected | 16 ++++
.../library-tests/frameworks/grape/app.rb | 74 +++++++++++++++++-
.../security/cwe-089/ArelInjection.rb | 64 +--------------
.../security/cwe-089/SqlInjection.expected | 66 ----------------
7 files changed, 216 insertions(+), 134 deletions(-)
create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Flow.expected
create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Flow.ql
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index ea7bc8c576c..a1646b8654c 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -121,11 +121,18 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
*/
private class GrapeParamsCall extends ParamsCallImpl {
GrapeParamsCall() {
- // Simplified approach: find params calls that are descendants of Grape API class methods
+ // Params calls within endpoint blocks
exists(GrapeApiClass api |
this.getMethodName() = "params" and
this.getParent+() = api.getADeclaration()
)
+ or
+ // Params calls within helper methods (defined in helpers blocks)
+ exists(GrapeApiClass api, DataFlow::CallNode helpersCall |
+ helpersCall = api.getAModuleLevelCall("helpers") and
+ this.getMethodName() = "params" and
+ this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
+ )
}
}
@@ -295,18 +302,31 @@ private class GrapeHelperMethod extends Method {
/**
* Additional taint step to model dataflow from method arguments to parameters
- * for Grape helper methods defined in `helpers` blocks.
+ * and from return values back to call sites for Grape helper methods defined in `helpers` blocks.
* This bridges the gap where standard dataflow doesn't recognize the Grape DSL semantics.
*/
private class GrapeHelperMethodTaintStep extends AdditionalTaintStep {
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
+ // Map arguments to parameters for helper method calls
exists(GrapeHelperMethod helperMethod, MethodCall call, int i |
- // Find calls to helper methods from within Grape endpoints
+ // Find calls to helper methods from within Grape endpoints or other helper methods
call.getMethodName() = helperMethod.getName() and
- exists(GrapeEndpoint endpoint | call.getParent+() = endpoint.getBody().asExpr().getExpr()) and
+ exists(GrapeApiClass api | call.getParent+() = api.getADeclaration()) and
// Map argument to parameter
nodeFrom.asExpr().getExpr() = call.getArgument(i) and
nodeTo.asParameter() = helperMethod.getParameter(i)
)
+ or
+ // Model implicit return values: the last expression in a helper method flows to the call site
+ exists(GrapeHelperMethod helperMethod, MethodCall helperCall, Expr lastExpr |
+ // Find calls to helper methods from within Grape endpoints or other helper methods
+ helperCall.getMethodName() = helperMethod.getName() and
+ exists(GrapeApiClass api | helperCall.getParent+() = api.getADeclaration()) and
+ // Get the last expression in the helper method (Ruby's implicit return)
+ lastExpr = helperMethod.getLastStmt() and
+ // Flow from the last expression in the helper method to the call site
+ nodeFrom.asExpr().getExpr() = lastExpr and
+ nodeTo.asExpr().getExpr() = helperCall
+ )
}
}
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected
new file mode 100644
index 00000000000..0fd19d4eace
--- /dev/null
+++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected
@@ -0,0 +1,77 @@
+models
+edges
+| app.rb:103:13:103:18 | call to params | app.rb:103:13:103:70 | call to select | provenance | |
+| app.rb:103:13:103:70 | call to select | app.rb:149:21:149:31 | call to user_params | provenance | AdditionalTaintStep |
+| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | AdditionalTaintStep |
+| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | AdditionalTaintStep |
+| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | AdditionalTaintStep |
+| app.rb:126:9:126:15 | user_id | app.rb:133:14:133:20 | user_id | provenance | |
+| app.rb:126:19:126:24 | call to params | app.rb:126:19:126:34 | ...[...] | provenance | |
+| app.rb:126:19:126:34 | ...[...] | app.rb:126:9:126:15 | user_id | provenance | |
+| app.rb:127:9:127:16 | route_id | app.rb:134:14:134:21 | route_id | provenance | |
+| app.rb:127:20:127:40 | call to route_param | app.rb:127:9:127:16 | route_id | provenance | |
+| app.rb:128:9:128:12 | auth | app.rb:135:14:135:17 | auth | provenance | |
+| app.rb:128:16:128:22 | call to headers | app.rb:128:16:128:38 | ...[...] | provenance | |
+| app.rb:128:16:128:38 | ...[...] | app.rb:128:9:128:12 | auth | provenance | |
+| app.rb:129:9:129:15 | session | app.rb:136:14:136:20 | session | provenance | |
+| app.rb:129:19:129:25 | call to cookies | app.rb:129:19:129:38 | ...[...] | provenance | |
+| app.rb:129:19:129:38 | ...[...] | app.rb:129:9:129:15 | session | provenance | |
+| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | |
+| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | |
+| app.rb:149:9:149:17 | user_data | app.rb:151:14:151:22 | user_data | provenance | |
+| app.rb:149:21:149:31 | call to user_params | app.rb:149:9:149:17 | user_data | provenance | |
+| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | |
+| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | |
+| app.rb:159:13:159:19 | user_id | app.rb:160:18:160:24 | user_id | provenance | |
+| app.rb:159:23:159:28 | call to params | app.rb:159:23:159:33 | ...[...] | provenance | |
+| app.rb:159:23:159:33 | ...[...] | app.rb:159:13:159:19 | user_id | provenance | |
+| app.rb:165:9:165:17 | user_data | app.rb:166:14:166:22 | user_data | provenance | |
+| app.rb:165:21:165:31 | call to user_params | app.rb:165:9:165:17 | user_data | provenance | |
+nodes
+| app.rb:103:13:103:18 | call to params | semmle.label | call to params |
+| app.rb:103:13:103:70 | call to select | semmle.label | call to select |
+| app.rb:107:13:107:32 | call to source | semmle.label | call to source |
+| app.rb:111:13:111:33 | call to source | semmle.label | call to source |
+| app.rb:126:9:126:15 | user_id | semmle.label | user_id |
+| app.rb:126:19:126:24 | call to params | semmle.label | call to params |
+| app.rb:126:19:126:34 | ...[...] | semmle.label | ...[...] |
+| app.rb:127:9:127:16 | route_id | semmle.label | route_id |
+| app.rb:127:20:127:40 | call to route_param | semmle.label | call to route_param |
+| app.rb:128:9:128:12 | auth | semmle.label | auth |
+| app.rb:128:16:128:22 | call to headers | semmle.label | call to headers |
+| app.rb:128:16:128:38 | ...[...] | semmle.label | ...[...] |
+| app.rb:129:9:129:15 | session | semmle.label | session |
+| app.rb:129:19:129:25 | call to cookies | semmle.label | call to cookies |
+| app.rb:129:19:129:38 | ...[...] | semmle.label | ...[...] |
+| app.rb:133:14:133:20 | user_id | semmle.label | user_id |
+| app.rb:134:14:134:21 | route_id | semmle.label | route_id |
+| app.rb:135:14:135:17 | auth | semmle.label | auth |
+| app.rb:136:14:136:20 | session | semmle.label | session |
+| app.rb:143:9:143:14 | result | semmle.label | result |
+| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper |
+| app.rb:144:14:144:19 | result | semmle.label | result |
+| app.rb:149:9:149:17 | user_data | semmle.label | user_data |
+| app.rb:149:21:149:31 | call to user_params | semmle.label | call to user_params |
+| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result |
+| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper |
+| app.rb:151:14:151:22 | user_data | semmle.label | user_data |
+| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result |
+| app.rb:159:13:159:19 | user_id | semmle.label | user_id |
+| app.rb:159:23:159:28 | call to params | semmle.label | call to params |
+| app.rb:159:23:159:33 | ...[...] | semmle.label | ...[...] |
+| app.rb:160:18:160:24 | user_id | semmle.label | user_id |
+| app.rb:165:9:165:17 | user_data | semmle.label | user_data |
+| app.rb:165:21:165:31 | call to user_params | semmle.label | call to user_params |
+| app.rb:166:14:166:22 | user_data | semmle.label | user_data |
+subpaths
+testFailures
+#select
+| app.rb:133:14:133:20 | user_id | app.rb:126:19:126:24 | call to params | app.rb:133:14:133:20 | user_id | $@ | app.rb:126:19:126:24 | call to params | call to params |
+| app.rb:134:14:134:21 | route_id | app.rb:127:20:127:40 | call to route_param | app.rb:134:14:134:21 | route_id | $@ | app.rb:127:20:127:40 | call to route_param | call to route_param |
+| app.rb:135:14:135:17 | auth | app.rb:128:16:128:22 | call to headers | app.rb:135:14:135:17 | auth | $@ | app.rb:128:16:128:22 | call to headers | call to headers |
+| app.rb:136:14:136:20 | session | app.rb:129:19:129:25 | call to cookies | app.rb:136:14:136:20 | session | $@ | app.rb:129:19:129:25 | call to cookies | call to cookies |
+| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source |
+| app.rb:151:14:151:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:151:14:151:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
+| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source |
+| app.rb:160:18:160:24 | user_id | app.rb:159:23:159:28 | call to params | app.rb:160:18:160:24 | user_id | $@ | app.rb:159:23:159:28 | call to params | call to params |
+| app.rb:166:14:166:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:166:14:166:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.ql b/ruby/ql/test/library-tests/frameworks/grape/Flow.ql
new file mode 100644
index 00000000000..baa3fa4307f
--- /dev/null
+++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.ql
@@ -0,0 +1,25 @@
+/**
+ * @kind path-problem
+ */
+
+import ruby
+import utils.test.InlineFlowTest
+import PathGraph
+import codeql.ruby.frameworks.Grape
+import codeql.ruby.Concepts
+
+module GrapeConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node source) {
+ source instanceof Http::Server::RequestInputAccess::Range
+ or
+ DefaultFlowConfig::isSource(source)
+ }
+
+ predicate isSink(DataFlow::Node sink) { DefaultFlowConfig::isSink(sink) }
+}
+
+import FlowTest
+
+from PathNode source, PathNode sink
+where flowPath(source, sink)
+select sink, source, sink, "$@", source, source.toString()
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
index af4d936e88d..d39d9430f92 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected
@@ -1,6 +1,7 @@
grapeApiClasses
| app.rb:1:1:90:3 | MyAPI |
| app.rb:92:1:96:3 | AdminAPI |
+| app.rb:98:1:168:3 | UserAPI |
grapeEndpoints
| app.rb:1:1:90:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
| app.rb:1:1:90:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
@@ -13,6 +14,10 @@ grapeEndpoints
| app.rb:1:1:90:3 | MyAPI | app.rb:78:3:82:5 | call to get | GET | /cookie_test |
| app.rb:1:1:90:3 | MyAPI | app.rb:85:3:89:5 | call to get | GET | /header_test |
| app.rb:92:1:96:3 | AdminAPI | app.rb:93:3:95:5 | call to get | GET | /admin |
+| app.rb:98:1:168:3 | UserAPI | app.rb:124:5:138:7 | call to get | GET | /comprehensive_test/:user_id |
+| app.rb:98:1:168:3 | UserAPI | app.rb:140:5:145:7 | call to get | GET | /helper_test/:user_id |
+| app.rb:98:1:168:3 | UserAPI | app.rb:147:5:153:7 | call to post | POST | /users |
+| app.rb:98:1:168:3 | UserAPI | app.rb:164:5:167:7 | call to post | POST | /users |
grapeParams
| app.rb:8:12:8:17 | call to params |
| app.rb:14:3:16:5 | call to params |
@@ -22,19 +27,30 @@ grapeParams
| app.rb:36:5:36:10 | call to params |
| app.rb:60:12:60:17 | call to params |
| app.rb:94:5:94:10 | call to params |
+| app.rb:103:13:103:18 | call to params |
+| app.rb:126:19:126:24 | call to params |
+| app.rb:142:19:142:24 | call to params |
+| app.rb:159:23:159:28 | call to params |
grapeHeaders
| app.rb:9:18:9:24 | call to headers |
| app.rb:46:5:46:11 | call to headers |
| app.rb:66:3:69:5 | call to headers |
| app.rb:86:12:86:18 | call to headers |
| app.rb:87:14:87:20 | call to headers |
+| app.rb:116:5:118:7 | call to headers |
+| app.rb:128:16:128:22 | call to headers |
grapeRequest
| app.rb:25:12:25:18 | call to request |
+| app.rb:130:21:130:27 | call to request |
grapeRouteParam
| app.rb:51:15:51:35 | call to route_param |
| app.rb:52:15:52:36 | call to route_param |
| app.rb:57:3:63:5 | call to route_param |
+| app.rb:127:20:127:40 | call to route_param |
+| app.rb:156:5:162:7 | call to route_param |
grapeCookies
| app.rb:72:3:75:5 | call to cookies |
| app.rb:79:15:79:21 | call to cookies |
| app.rb:80:16:80:22 | call to cookies |
+| app.rb:120:5:122:7 | call to cookies |
+| app.rb:129:19:129:25 | call to cookies |
diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb
index a034f325f7b..6fbb184cab9 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/app.rb
+++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb
@@ -93,4 +93,76 @@ class AdminAPI < Grape::API
get '/admin' do
params[:token]
end
-end
\ No newline at end of file
+end
+
+class UserAPI < Grape::API
+ VALID_PARAMS = %w(name email password password_confirmation)
+
+ helpers do
+ def user_params
+ params.select{|key,value| VALID_PARAMS.include?(key.to_s)} # Real helper implementation
+ end
+
+ def vulnerable_helper(user_id)
+ source "paramHelper" # Test parameter passing to helper
+ end
+
+ def simple_helper
+ source "simpleHelper" # Test simple helper return
+ end
+ end
+
+ # Headers and cookies blocks for DSL testing
+ headers do
+ requires :Authorization, type: String
+ end
+
+ cookies do
+ requires :session_id, type: String
+ end
+
+ get '/comprehensive_test/:user_id' do
+ # Test all Grape input sources
+ user_id = params[:user_id] # params taint source
+ route_id = route_param(:user_id) # route_param taint source
+ auth = headers[:Authorization] # headers taint source
+ session = cookies[:session_id] # cookies taint source
+ body_data = request.body.read # request taint source
+
+ # Test sinks for all sources
+ sink user_id # $ hasTaintFlow
+ sink route_id # $ hasTaintFlow
+ sink auth # $ hasTaintFlow
+ sink session # $ hasTaintFlow
+ # Note: request.body.read may not be detected by this flow test config
+ end
+
+ get '/helper_test/:user_id' do
+ # Test helper method parameter passing dataflow
+ user_id = params[:user_id]
+ result = vulnerable_helper(user_id)
+ sink result # $ hasTaintFlow=paramHelper
+ end
+
+ post '/users' do
+ # Test helper method return dataflow
+ user_data = user_params
+ simple_result = simple_helper
+ sink user_data # $ hasTaintFlow
+ sink simple_result # $ hasTaintFlow=simpleHelper
+ end
+
+ # Test route_param block pattern
+ route_param :id do
+ get do
+ # params[:id] should be user input from the path
+ user_id = params[:id]
+ sink user_id # $ hasTaintFlow
+ end
+ end
+
+ post '/users' do
+ user_data = user_params
+ sink user_data # $ hasTaintFlow
+ end
+end
diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
index 8c9c3bff4fb..1cd6782b241 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
+++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb
@@ -6,66 +6,4 @@ class PotatoController < ActionController::Base
sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
end
-end
-
-class PotatoAPI < Grape::API
- get '/unsafe_endpoint' do
- name = params[:user_name]
- # BAD: SQL statement constructed from user input
- sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
- sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
- end
-end
-
-class SimpleAPI < Grape::API
- get '/test' do
- x = params[:name]
- Arel.sql("SELECT * FROM users WHERE name = #{x}")
- end
-end
-
- # Test helper method pattern in Grape helpers block
- class TestAPI < Grape::API
- helpers do
- def vulnerable_helper(user_id)
- # BAD: SQL statement constructed from user input passed as parameter
- Arel.sql("SELECT * FROM users WHERE id = #{user_id}")
- end
- end
-
- # Headers and cookies blocks for DSL testing
- headers do
- requires :Authorization, type: String
- end
-
- cookies do
- requires :session_id, type: String
- end
-
- get '/comprehensive_test/:user_id' do
- # BAD: Comprehensive test using all Grape input sources in one SQL query
- user_id = params[:user_id] # params taint source
- route_id = route_param(:user_id) # route_param taint source
- auth = headers[:Authorization] # headers taint source
- session = cookies[:session_id] # cookies taint source
- body_data = request.body.read # request taint source
-
- # All sources flow to SQL injection
- Arel.sql("SELECT * FROM users WHERE id = #{user_id} AND route_id = #{route_id} AND auth = #{auth} AND session = #{session} AND data = #{body_data}")
- end
-
- get '/helper_test' do
- # BAD: Test helper method dataflow
- user_id = params[:user_id]
- vulnerable_helper(user_id)
- end
-
- # Test route_param block pattern
- route_param :id do
- get do
- # BAD: params[:id] should be user input from the path
- user_id = params[:id]
- Arel.sql("SELECT * FROM users WHERE id = #{user_id}")
- end
- end
- end
\ No newline at end of file
+end
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
index 34128474cb9..069cb34810f 100644
--- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
+++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected
@@ -81,32 +81,6 @@ edges
| ArelInjection.rb:4:5:4:8 | name | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:4:12:4:29 | ...[...] | provenance | |
| ArelInjection.rb:4:12:4:29 | ...[...] | ArelInjection.rb:4:5:4:8 | name | provenance | |
-| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | |
-| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | |
-| ArelInjection.rb:22:5:22:5 | x | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | |
-| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | |
-| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:47:7:47:13 | user_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:47:17:47:32 | ...[...] | provenance | |
-| ArelInjection.rb:47:17:47:32 | ...[...] | ArelInjection.rb:47:7:47:13 | user_id | provenance | |
-| ArelInjection.rb:48:7:48:14 | route_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:48:7:48:14 | route_id | provenance | |
-| ArelInjection.rb:49:7:49:10 | auth | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:49:14:49:36 | ...[...] | provenance | |
-| ArelInjection.rb:49:14:49:36 | ...[...] | ArelInjection.rb:49:7:49:10 | auth | provenance | |
-| ArelInjection.rb:50:7:50:13 | session | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:50:17:50:36 | ...[...] | provenance | |
-| ArelInjection.rb:50:17:50:36 | ...[...] | ArelInjection.rb:50:7:50:13 | session | provenance | |
-| ArelInjection.rb:59:7:59:13 | user_id | ArelInjection.rb:60:25:60:31 | user_id | provenance | |
-| ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:59:17:59:32 | ...[...] | provenance | |
-| ArelInjection.rb:59:17:59:32 | ...[...] | ArelInjection.rb:59:7:59:13 | user_id | provenance | |
-| ArelInjection.rb:60:25:60:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
-| ArelInjection.rb:67:9:67:15 | user_id | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
-| ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:67:19:67:29 | ...[...] | provenance | |
-| ArelInjection.rb:67:19:67:29 | ...[...] | ArelInjection.rb:67:9:67:15 | user_id | provenance | |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -235,37 +209,6 @@ nodes
| ArelInjection.rb:4:12:4:29 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
-| ArelInjection.rb:13:5:13:8 | name | semmle.label | name |
-| ArelInjection.rb:13:12:13:17 | call to params | semmle.label | call to params |
-| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
-| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
-| ArelInjection.rb:22:5:22:5 | x | semmle.label | x |
-| ArelInjection.rb:22:9:22:14 | call to params | semmle.label | call to params |
-| ArelInjection.rb:22:9:22:21 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
-| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id |
-| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
-| ArelInjection.rb:47:7:47:13 | user_id | semmle.label | user_id |
-| ArelInjection.rb:47:17:47:22 | call to params | semmle.label | call to params |
-| ArelInjection.rb:47:17:47:32 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:48:7:48:14 | route_id | semmle.label | route_id |
-| ArelInjection.rb:48:18:48:38 | call to route_param | semmle.label | call to route_param |
-| ArelInjection.rb:49:7:49:10 | auth | semmle.label | auth |
-| ArelInjection.rb:49:14:49:20 | call to headers | semmle.label | call to headers |
-| ArelInjection.rb:49:14:49:36 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:50:7:50:13 | session | semmle.label | session |
-| ArelInjection.rb:50:17:50:23 | call to cookies | semmle.label | call to cookies |
-| ArelInjection.rb:50:17:50:36 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
-| ArelInjection.rb:59:7:59:13 | user_id | semmle.label | user_id |
-| ArelInjection.rb:59:17:59:22 | call to params | semmle.label | call to params |
-| ArelInjection.rb:59:17:59:32 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:60:25:60:31 | user_id | semmle.label | user_id |
-| ArelInjection.rb:67:9:67:15 | user_id | semmle.label | user_id |
-| ArelInjection.rb:67:19:67:24 | call to params | semmle.label | call to params |
-| ArelInjection.rb:67:19:67:29 | ...[...] | semmle.label | ...[...] |
-| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -323,15 +266,6 @@ subpaths
| ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | ActiveRecordInjection.rb:222:29:222:34 | call to params | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | This SQL query depends on a $@. | ActiveRecordInjection.rb:222:29:222:34 | call to params | user-provided value |
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
-| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
-| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
-| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value |
-| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:59:17:59:22 | call to params | user-provided value |
-| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:47:17:47:22 | call to params | user-provided value |
-| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:48:18:48:38 | call to route_param | user-provided value |
-| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:49:14:49:20 | call to headers | user-provided value |
-| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:50:17:50:23 | call to cookies | user-provided value |
-| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:67:19:67:24 | call to params | user-provided value |
| PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
From 95a84ad6559f4180039dac0dc07dcd1b4d7756ee Mon Sep 17 00:00:00 2001
From: Taus
Date: Fri, 19 Sep 2025 15:06:46 +0000
Subject: [PATCH 24/90] Python: Fix false positive for unmatchable dollar/caret
Our previous modelling did not account for the fact that a lookahead can
potentially extend all the way to the end of the input (and similarly,
that a lookbehind can extend all the way to the beginning).
To fix this, I extended `firstPart` and `lastPart` to handle lookbehinds
and lookaheads correctly, and added some test cases (all of which yield
no new results).
Fixes #20429.
---
.../semmle/python/regexp/RegexTreeView.qll | 8 +--
.../python/regexp/internal/ParseRegExp.qll | 70 ++++++++++++-------
...atchable-dollar-and-caret-in-assertions.md | 5 ++
.../query-tests/Expressions/Regex/test.py | 10 ++-
4 files changed, 64 insertions(+), 29 deletions(-)
create mode 100644 python/ql/src/change-notes/2025-09-19-fix-unmatchable-dollar-and-caret-in-assertions.md
diff --git a/python/ql/lib/semmle/python/regexp/RegexTreeView.qll b/python/ql/lib/semmle/python/regexp/RegexTreeView.qll
index a2952a5680b..897c97bb783 100644
--- a/python/ql/lib/semmle/python/regexp/RegexTreeView.qll
+++ b/python/ql/lib/semmle/python/regexp/RegexTreeView.qll
@@ -964,7 +964,7 @@ module Impl implements RegexTreeViewSig {
* ```
*/
class RegExpPositiveLookahead extends RegExpLookahead {
- RegExpPositiveLookahead() { re.positiveLookaheadAssertionGroup(start, end) }
+ RegExpPositiveLookahead() { re.positiveLookaheadAssertionGroup(start, end, _, _) }
override string getPrimaryQLClass() { result = "RegExpPositiveLookahead" }
}
@@ -979,7 +979,7 @@ module Impl implements RegexTreeViewSig {
* ```
*/
additional class RegExpNegativeLookahead extends RegExpLookahead {
- RegExpNegativeLookahead() { re.negativeLookaheadAssertionGroup(start, end) }
+ RegExpNegativeLookahead() { re.negativeLookaheadAssertionGroup(start, end, _, _) }
override string getPrimaryQLClass() { result = "RegExpNegativeLookahead" }
}
@@ -1006,7 +1006,7 @@ module Impl implements RegexTreeViewSig {
* ```
*/
class RegExpPositiveLookbehind extends RegExpLookbehind {
- RegExpPositiveLookbehind() { re.positiveLookbehindAssertionGroup(start, end) }
+ RegExpPositiveLookbehind() { re.positiveLookbehindAssertionGroup(start, end, _, _) }
override string getPrimaryQLClass() { result = "RegExpPositiveLookbehind" }
}
@@ -1021,7 +1021,7 @@ module Impl implements RegexTreeViewSig {
* ```
*/
additional class RegExpNegativeLookbehind extends RegExpLookbehind {
- RegExpNegativeLookbehind() { re.negativeLookbehindAssertionGroup(start, end) }
+ RegExpNegativeLookbehind() { re.negativeLookbehindAssertionGroup(start, end, _, _) }
override string getPrimaryQLClass() { result = "RegExpNegativeLookbehind" }
}
diff --git a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll
index 7e23554e058..d91c4bbd78c 100644
--- a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll
+++ b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll
@@ -554,9 +554,9 @@ class RegExp extends Expr instanceof StringLiteral {
or
this.negativeAssertionGroup(start, end)
or
- this.positiveLookaheadAssertionGroup(start, end)
+ this.positiveLookaheadAssertionGroup(start, end, _, _)
or
- this.positiveLookbehindAssertionGroup(start, end)
+ this.positiveLookbehindAssertionGroup(start, end, _, _)
}
/** Holds if an empty group is found between `start` and `end`. */
@@ -572,7 +572,7 @@ class RegExp extends Expr instanceof StringLiteral {
or
this.negativeAssertionGroup(start, end)
or
- this.positiveLookaheadAssertionGroup(start, end)
+ this.positiveLookaheadAssertionGroup(start, end, _, _)
}
private predicate emptyMatchAtEndGroup(int start, int end) {
@@ -580,7 +580,7 @@ class RegExp extends Expr instanceof StringLiteral {
or
this.negativeAssertionGroup(start, end)
or
- this.positiveLookbehindAssertionGroup(start, end)
+ this.positiveLookbehindAssertionGroup(start, end, _, _)
}
private predicate negativeAssertionGroup(int start, int end) {
@@ -593,32 +593,40 @@ class RegExp extends Expr instanceof StringLiteral {
)
}
- /** Holds if a negative lookahead is found between `start` and `end` */
- predicate negativeLookaheadAssertionGroup(int start, int end) {
- exists(int in_start | this.negative_lookahead_assertion_start(start, in_start) |
- this.groupContents(start, end, in_start, _)
- )
+ /**
+ * Holds if a negative lookahead is found between `start` and `end`, with contents
+ * between `in_start` and `in_end`.
+ */
+ predicate negativeLookaheadAssertionGroup(int start, int end, int in_start, int in_end) {
+ this.negative_lookahead_assertion_start(start, in_start) and
+ this.groupContents(start, end, in_start, in_end)
}
- /** Holds if a negative lookbehind is found between `start` and `end` */
- predicate negativeLookbehindAssertionGroup(int start, int end) {
- exists(int in_start | this.negative_lookbehind_assertion_start(start, in_start) |
- this.groupContents(start, end, in_start, _)
- )
+ /**
+ * Holds if a negative lookbehind is found between `start` and `end`, with contents
+ * between `in_start` and `in_end`.
+ */
+ predicate negativeLookbehindAssertionGroup(int start, int end, int in_start, int in_end) {
+ this.negative_lookbehind_assertion_start(start, in_start) and
+ this.groupContents(start, end, in_start, in_end)
}
- /** Holds if a positive lookahead is found between `start` and `end` */
- predicate positiveLookaheadAssertionGroup(int start, int end) {
- exists(int in_start | this.lookahead_assertion_start(start, in_start) |
- this.groupContents(start, end, in_start, _)
- )
+ /**
+ * Holds if a positive lookahead is found between `start` and `end`, with contents
+ * between `in_start` and `in_end`.
+ */
+ predicate positiveLookaheadAssertionGroup(int start, int end, int in_start, int in_end) {
+ this.lookahead_assertion_start(start, in_start) and
+ this.groupContents(start, end, in_start, in_end)
}
- /** Holds if a positive lookbehind is found between `start` and `end` */
- predicate positiveLookbehindAssertionGroup(int start, int end) {
- exists(int in_start | this.lookbehind_assertion_start(start, in_start) |
- this.groupContents(start, end, in_start, _)
- )
+ /**
+ * Holds if a positive lookbehind is found between `start` and `end`, with contents
+ * between `in_start` and `in_end`.
+ */
+ predicate positiveLookbehindAssertionGroup(int start, int end, int in_start, int in_end) {
+ this.lookbehind_assertion_start(start, in_start) and
+ this.groupContents(start, end, in_start, in_end)
}
private predicate group_start(int start, int end) {
@@ -1049,6 +1057,13 @@ class RegExp extends Expr instanceof StringLiteral {
or
this.alternationOption(x, y, start, end)
)
+ or
+ // Lookbehind assertions can potentially match the start of the string
+ (
+ this.positiveLookbehindAssertionGroup(_, _, start, _) or
+ this.negativeLookbehindAssertionGroup(_, _, start, _)
+ ) and
+ this.item(start, end)
}
/** A part of the regex that may match the end of the string. */
@@ -1074,6 +1089,13 @@ class RegExp extends Expr instanceof StringLiteral {
or
this.alternationOption(x, y, start, end)
)
+ or
+ // Lookahead assertions can potentially match the end of the string
+ (
+ this.positiveLookaheadAssertionGroup(_, _, _, end) or
+ this.negativeLookaheadAssertionGroup(_, _, _, end)
+ ) and
+ this.item(start, end)
}
/**
diff --git a/python/ql/src/change-notes/2025-09-19-fix-unmatchable-dollar-and-caret-in-assertions.md b/python/ql/src/change-notes/2025-09-19-fix-unmatchable-dollar-and-caret-in-assertions.md
new file mode 100644
index 00000000000..cf63dd9ed4d
--- /dev/null
+++ b/python/ql/src/change-notes/2025-09-19-fix-unmatchable-dollar-and-caret-in-assertions.md
@@ -0,0 +1,5 @@
+---
+category: minorAnalysis
+---
+
+- The queries that check for unmatchable `$` and `^` in regular expressions did not account correctly for occurrences inside lookahead and lookbehind assertions. These occurrences are now handled correctly, eliminating this source of false positives.
diff --git a/python/ql/test/query-tests/Expressions/Regex/test.py b/python/ql/test/query-tests/Expressions/Regex/test.py
index 6dadbccb4b6..717663e335c 100644
--- a/python/ql/test/query-tests/Expressions/Regex/test.py
+++ b/python/ql/test/query-tests/Expressions/Regex/test.py
@@ -150,4 +150,12 @@ re.compile(r"[\U00010000-\U0010FFFF]")
re.compile(r"[\u0000-\uFFFF]")
#Allow unicode names
-re.compile(r"[\N{degree sign}\N{EM DASH}]")
\ No newline at end of file
+re.compile(r"[\N{degree sign}\N{EM DASH}]")
+
+#Lookahead assertions. None of these are unmatchable dollars:
+re.compile(r"^(?=a$)[ab]")
+re.compile(r"^(?!a$)[ab]")
+
+#Lookbehind assertions. None of these are unmatchable carets:
+re.compile(r"(?<=^a)a")
+re.compile(r"(?
Date: Fri, 19 Sep 2025 15:39:12 +0000
Subject: [PATCH 25/90] Python: Update test output
---
python/ql/test/library-tests/regex/FirstLast.expected | 2 ++
1 file changed, 2 insertions(+)
diff --git a/python/ql/test/library-tests/regex/FirstLast.expected b/python/ql/test/library-tests/regex/FirstLast.expected
index b187033ee22..0abf9c790c2 100644
--- a/python/ql/test/library-tests/regex/FirstLast.expected
+++ b/python/ql/test/library-tests/regex/FirstLast.expected
@@ -4,6 +4,7 @@
| (?!not-this)^[A-Z_]+$ | first | 12 | 13 |
| (?!not-this)^[A-Z_]+$ | first | 13 | 19 |
| (?!not-this)^[A-Z_]+$ | first | 13 | 20 |
+| (?!not-this)^[A-Z_]+$ | last | 3 | 11 |
| (?!not-this)^[A-Z_]+$ | last | 13 | 19 |
| (?!not-this)^[A-Z_]+$ | last | 13 | 20 |
| (?!not-this)^[A-Z_]+$ | last | 20 | 21 |
@@ -101,6 +102,7 @@
| ^[A-Z_]+$(?
Date: Fri, 19 Sep 2025 16:49:54 +0100
Subject: [PATCH 26/90] Apply suggestions from code review
Co-authored-by: Simon Friis Vindum
---
rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
index e4e0fc5eaa9..088f202965a 100644
--- a/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
+++ b/rust/ql/src/queries/security/CWE-319/UseOfHttp.qhelp
@@ -4,7 +4,7 @@
-Constructing URLs with the HTTP protocol can lead to unsecured connections.
+Constructing URLs with the HTTP protocol can lead to insecure connections.
Furthermore, constructing URLs with the HTTP protocol can create problems if other parts of the
code expect HTTPS URLs. A typical pattern is to use libraries that expect secure connections,
@@ -14,7 +14,7 @@ which may fail or fall back to insecure behavior when provided with HTTP URLs in
When you construct a URL for network requests, ensure that you use an HTTPS URL rather than an HTTP URL.
-Then, any connections that are made using that URL are secure SSL/TLS connections.
+Then, any connections that are made using that URL are secure TLS connections.
@@ -26,7 +26,7 @@ by attackers:
A better approach is to use HTTPS. When the request is made using an HTTPS URL, the connection
-is a secure SSL/TLS connection:
+is a secure TLS connection:
From 89e9ee43c00159a561bbad75b43296acefd87ef7 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 19 Sep 2025 18:28:45 -0400
Subject: [PATCH 27/90] Convert from GrapeHelperMethodTaintStep extends
AdditionalTaintStep to a simplified GrapeHelperMethodTarget extends
AdditionalCallTarget
---
.../2025-09-15-grape-framework-support.md | 2 +-
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 39 +++++++------------
.../frameworks/grape/Flow.expected | 36 +++++++++++++++--
.../library-tests/frameworks/grape/app.rb | 4 +-
4 files changed, 49 insertions(+), 32 deletions(-)
diff --git a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
index 258da40d36c..08ceed887f2 100644
--- a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
+++ b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
@@ -1,4 +1,4 @@
---
category: feature
---
-* Initial modeling for the Ruby Grape framework in `Grape.qll` have been added to detect API endpoints, parameters, and headers within Grape API classes.
\ No newline at end of file
+* Initial modeling for the Ruby Grape framework in `Grape.qll` has been added to detect API endpoints, parameters, and headers within Grape API classes.
\ No newline at end of file
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index a1646b8654c..31632e01948 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -3,6 +3,7 @@
*/
private import codeql.ruby.AST
+private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.DataFlow
@@ -301,32 +302,20 @@ private class GrapeHelperMethod extends Method {
}
/**
- * Additional taint step to model dataflow from method arguments to parameters
- * and from return values back to call sites for Grape helper methods defined in `helpers` blocks.
- * This bridges the gap where standard dataflow doesn't recognize the Grape DSL semantics.
+ * Additional call-target to resolve helper method calls defined in `helpers` blocks.
+ *
+ * This class is responsible for resolving calls to helper methods defined in
+ * `helpers` blocks, allowing the dataflow framework to accurately track
+ * the flow of information between these methods and their call sites.
*/
-private class GrapeHelperMethodTaintStep extends AdditionalTaintStep {
- override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
- // Map arguments to parameters for helper method calls
- exists(GrapeHelperMethod helperMethod, MethodCall call, int i |
- // Find calls to helper methods from within Grape endpoints or other helper methods
- call.getMethodName() = helperMethod.getName() and
- exists(GrapeApiClass api | call.getParent+() = api.getADeclaration()) and
- // Map argument to parameter
- nodeFrom.asExpr().getExpr() = call.getArgument(i) and
- nodeTo.asParameter() = helperMethod.getParameter(i)
- )
- or
- // Model implicit return values: the last expression in a helper method flows to the call site
- exists(GrapeHelperMethod helperMethod, MethodCall helperCall, Expr lastExpr |
- // Find calls to helper methods from within Grape endpoints or other helper methods
- helperCall.getMethodName() = helperMethod.getName() and
- exists(GrapeApiClass api | helperCall.getParent+() = api.getADeclaration()) and
- // Get the last expression in the helper method (Ruby's implicit return)
- lastExpr = helperMethod.getLastStmt() and
- // Flow from the last expression in the helper method to the call site
- nodeFrom.asExpr().getExpr() = lastExpr and
- nodeTo.asExpr().getExpr() = helperCall
+private class GrapeHelperMethodTarget extends AdditionalCallTarget {
+ override DataFlowCallable viableTarget(CfgNodes::ExprNodes::CallCfgNode call) {
+ // Find calls to helper methods from within Grape endpoints or other helper methods
+ exists(GrapeHelperMethod helperMethod, MethodCall mc |
+ result.asCfgScope() = helperMethod and
+ mc = call.getAstNode() and
+ mc.getMethodName() = helperMethod.getName() and
+ mc.getParent+() = helperMethod.getApiClass().getADeclaration()
)
}
}
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected
index 0fd19d4eace..c104b36afb2 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected
+++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected
@@ -1,10 +1,15 @@
models
edges
| app.rb:103:13:103:18 | call to params | app.rb:103:13:103:70 | call to select | provenance | |
-| app.rb:103:13:103:70 | call to select | app.rb:149:21:149:31 | call to user_params | provenance | AdditionalTaintStep |
-| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | AdditionalTaintStep |
-| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | AdditionalTaintStep |
-| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | AdditionalTaintStep |
+| app.rb:103:13:103:18 | call to params | app.rb:103:13:103:70 | call to select : [collection] [element] | provenance | |
+| app.rb:103:13:103:70 | call to select | app.rb:149:21:149:31 | call to user_params | provenance | |
+| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | |
+| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:149:21:149:31 | call to user_params : [collection] [element] | provenance | |
+| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:165:21:165:31 | call to user_params : [collection] [element] | provenance | |
+| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | |
+| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | |
+| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | |
+| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | |
| app.rb:126:9:126:15 | user_id | app.rb:133:14:133:20 | user_id | provenance | |
| app.rb:126:19:126:24 | call to params | app.rb:126:19:126:34 | ...[...] | provenance | |
| app.rb:126:19:126:34 | ...[...] | app.rb:126:9:126:15 | user_id | provenance | |
@@ -17,20 +22,31 @@ edges
| app.rb:129:19:129:25 | call to cookies | app.rb:129:19:129:38 | ...[...] | provenance | |
| app.rb:129:19:129:38 | ...[...] | app.rb:129:9:129:15 | session | provenance | |
| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | |
+| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | |
+| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | |
| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | |
| app.rb:149:9:149:17 | user_data | app.rb:151:14:151:22 | user_data | provenance | |
+| app.rb:149:9:149:17 | user_data : [collection] [element] | app.rb:151:14:151:22 | user_data | provenance | |
| app.rb:149:21:149:31 | call to user_params | app.rb:149:9:149:17 | user_data | provenance | |
+| app.rb:149:21:149:31 | call to user_params : [collection] [element] | app.rb:149:9:149:17 | user_data : [collection] [element] | provenance | |
| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | |
+| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | |
+| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | |
| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | |
| app.rb:159:13:159:19 | user_id | app.rb:160:18:160:24 | user_id | provenance | |
| app.rb:159:23:159:28 | call to params | app.rb:159:23:159:33 | ...[...] | provenance | |
| app.rb:159:23:159:33 | ...[...] | app.rb:159:13:159:19 | user_id | provenance | |
| app.rb:165:9:165:17 | user_data | app.rb:166:14:166:22 | user_data | provenance | |
+| app.rb:165:9:165:17 | user_data : [collection] [element] | app.rb:166:14:166:22 | user_data | provenance | |
| app.rb:165:21:165:31 | call to user_params | app.rb:165:9:165:17 | user_data | provenance | |
+| app.rb:165:21:165:31 | call to user_params : [collection] [element] | app.rb:165:9:165:17 | user_data : [collection] [element] | provenance | |
nodes
| app.rb:103:13:103:18 | call to params | semmle.label | call to params |
| app.rb:103:13:103:70 | call to select | semmle.label | call to select |
+| app.rb:103:13:103:70 | call to select : [collection] [element] | semmle.label | call to select : [collection] [element] |
| app.rb:107:13:107:32 | call to source | semmle.label | call to source |
+| app.rb:107:13:107:32 | call to source | semmle.label | call to source |
+| app.rb:111:13:111:33 | call to source | semmle.label | call to source |
| app.rb:111:13:111:33 | call to source | semmle.label | call to source |
| app.rb:126:9:126:15 | user_id | semmle.label | user_id |
| app.rb:126:19:126:24 | call to params | semmle.label | call to params |
@@ -48,20 +64,30 @@ nodes
| app.rb:135:14:135:17 | auth | semmle.label | auth |
| app.rb:136:14:136:20 | session | semmle.label | session |
| app.rb:143:9:143:14 | result | semmle.label | result |
+| app.rb:143:9:143:14 | result | semmle.label | result |
+| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper |
| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper |
| app.rb:144:14:144:19 | result | semmle.label | result |
+| app.rb:144:14:144:19 | result | semmle.label | result |
| app.rb:149:9:149:17 | user_data | semmle.label | user_data |
+| app.rb:149:9:149:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] |
| app.rb:149:21:149:31 | call to user_params | semmle.label | call to user_params |
+| app.rb:149:21:149:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] |
+| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result |
| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result |
| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper |
+| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper |
| app.rb:151:14:151:22 | user_data | semmle.label | user_data |
| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result |
+| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result |
| app.rb:159:13:159:19 | user_id | semmle.label | user_id |
| app.rb:159:23:159:28 | call to params | semmle.label | call to params |
| app.rb:159:23:159:33 | ...[...] | semmle.label | ...[...] |
| app.rb:160:18:160:24 | user_id | semmle.label | user_id |
| app.rb:165:9:165:17 | user_data | semmle.label | user_data |
+| app.rb:165:9:165:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] |
| app.rb:165:21:165:31 | call to user_params | semmle.label | call to user_params |
+| app.rb:165:21:165:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] |
| app.rb:166:14:166:22 | user_data | semmle.label | user_data |
subpaths
testFailures
@@ -71,7 +97,9 @@ testFailures
| app.rb:135:14:135:17 | auth | app.rb:128:16:128:22 | call to headers | app.rb:135:14:135:17 | auth | $@ | app.rb:128:16:128:22 | call to headers | call to headers |
| app.rb:136:14:136:20 | session | app.rb:129:19:129:25 | call to cookies | app.rb:136:14:136:20 | session | $@ | app.rb:129:19:129:25 | call to cookies | call to cookies |
| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source |
+| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source |
| app.rb:151:14:151:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:151:14:151:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source |
+| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source |
| app.rb:160:18:160:24 | user_id | app.rb:159:23:159:28 | call to params | app.rb:160:18:160:24 | user_id | $@ | app.rb:159:23:159:28 | call to params | call to params |
| app.rb:166:14:166:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:166:14:166:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb
index 6fbb184cab9..81f46482687 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/app.rb
+++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb
@@ -141,7 +141,7 @@ class UserAPI < Grape::API
# Test helper method parameter passing dataflow
user_id = params[:user_id]
result = vulnerable_helper(user_id)
- sink result # $ hasTaintFlow=paramHelper
+ sink result # $ hasValueFlow=paramHelper
end
post '/users' do
@@ -149,7 +149,7 @@ class UserAPI < Grape::API
user_data = user_params
simple_result = simple_helper
sink user_data # $ hasTaintFlow
- sink simple_result # $ hasTaintFlow=simpleHelper
+ sink simple_result # $ hasValueFlow=simpleHelper
end
# Test route_param block pattern
From f4bbbc346fe5b270f6fbfc3ed351fdfdaee3fa46 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Fri, 19 Sep 2025 19:06:50 -0400
Subject: [PATCH 28/90] Refactor Grape framework to be encapsulated properly in
Module
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 550 +++++++++---------
.../library-tests/frameworks/grape/Grape.ql | 14 +-
2 files changed, 285 insertions(+), 279 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 31632e01948..0999be94505 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -29,293 +29,299 @@ module Grape {
not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent())
}
}
-}
-/**
- * A class that extends `Grape::API`.
- * For example,
- *
- * ```rb
- * class FooAPI < Grape::API
- * get '/users' do
- * name = params[:name]
- * User.where("name = #{name}")
- * end
- * end
- * ```
- */
-class GrapeApiClass extends DataFlow::ClassNode {
- GrapeApiClass() {
- this = grapeApiBaseClass().getADescendentModule() and
- not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m)
+ /**
+ * A class that extends `Grape::API`.
+ * For example,
+ *
+ * ```rb
+ * class FooAPI < Grape::API
+ * get '/users' do
+ * name = params[:name]
+ * User.where("name = #{name}")
+ * end
+ * end
+ * ```
+ */
+ class GrapeApiClass extends DataFlow::ClassNode {
+ GrapeApiClass() {
+ this = grapeApiBaseClass().getADescendentModule() and
+ not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m)
+ }
+
+ /**
+ * Gets a `GrapeEndpoint` defined in this class.
+ */
+ GrapeEndpoint getAnEndpoint() { result.getApiClass() = this }
+
+ /**
+ * Gets a `self` that possibly refers to an instance of this class.
+ */
+ DataFlow::LocalSourceNode getSelf() {
+ result = this.getAnInstanceSelf()
+ or
+ // Include the module-level `self` to recover some cases where a block at the module level
+ // is invoked with an instance as the `self`.
+ result = this.getModuleLevelSelf()
+ }
+ }
+
+ private DataFlow::ConstRef grapeApiBaseClass() {
+ result = DataFlow::getConstant("Grape").getConstant("API")
+ }
+
+ private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() }
+
+ /**
+ * A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
+ */
+ class GrapeEndpoint extends DataFlow::CallNode {
+ private GrapeApiClass apiClass;
+
+ GrapeEndpoint() {
+ this =
+ apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
+ }
+
+ /**
+ * Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
+ */
+ string getHttpMethod() { result = this.getMethodName().toUpperCase() }
+
+ /**
+ * Gets the API class containing this endpoint.
+ */
+ GrapeApiClass getApiClass() { result = apiClass }
+
+ /**
+ * Gets the block containing the endpoint logic.
+ */
+ DataFlow::BlockNode getBody() { result = this.getBlock() }
+
+ /**
+ * Gets the path pattern for this endpoint, if specified.
+ */
+ string getPath() { result = this.getArgument(0).getConstantValue().getString() }
}
/**
- * Gets a `GrapeEndpoint` defined in this class.
+ * A `RemoteFlowSource::Range` to represent accessing the
+ * Grape parameters available via the `params` method within an endpoint.
*/
- GrapeEndpoint getAnEndpoint() { result.getApiClass() = this }
+ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
+ GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall }
- /**
- * Gets a `self` that possibly refers to an instance of this class.
- */
- DataFlow::LocalSourceNode getSelf() {
- result = this.getAnInstanceSelf()
- or
- // Include the module-level `self` to recover some cases where a block at the module level
- // is invoked with an instance as the `self`.
- result = this.getModuleLevelSelf()
- }
-}
+ override string getSourceType() { result = "Grape::API#params" }
-private DataFlow::ConstRef grapeApiBaseClass() {
- result = DataFlow::getConstant("Grape").getConstant("API")
-}
-
-private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() }
-
-/**
- * A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
- */
-class GrapeEndpoint extends DataFlow::CallNode {
- private GrapeApiClass apiClass;
-
- GrapeEndpoint() {
- this =
- apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
+ override Http::Server::RequestInputKind getKind() {
+ result = Http::Server::parameterInputKind()
+ }
}
/**
- * Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
+ * A call to `params` from within a Grape API endpoint or helper method.
*/
- string getHttpMethod() { result = this.getMethodName().toUpperCase() }
-
- /**
- * Gets the API class containing this endpoint.
- */
- GrapeApiClass getApiClass() { result = apiClass }
-
- /**
- * Gets the block containing the endpoint logic.
- */
- DataFlow::BlockNode getBody() { result = this.getBlock() }
-
- /**
- * Gets the path pattern for this endpoint, if specified.
- */
- string getPath() { result = this.getArgument(0).getConstantValue().getString() }
-}
-
-/**
- * A `RemoteFlowSource::Range` to represent accessing the
- * Grape parameters available via the `params` method within an endpoint.
- */
-class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
- GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall }
-
- override string getSourceType() { result = "Grape::API#params" }
-
- override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
-}
-
-/**
- * A call to `params` from within a Grape API endpoint or helper method.
- */
-private class GrapeParamsCall extends ParamsCallImpl {
- GrapeParamsCall() {
- // Params calls within endpoint blocks
- exists(GrapeApiClass api |
- this.getMethodName() = "params" and
- this.getParent+() = api.getADeclaration()
- )
- or
- // Params calls within helper methods (defined in helpers blocks)
- exists(GrapeApiClass api, DataFlow::CallNode helpersCall |
- helpersCall = api.getAModuleLevelCall("helpers") and
- this.getMethodName() = "params" and
- this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
- )
- }
-}
-
-/**
- * A call to `headers` from within a Grape API endpoint or headers block.
- * Headers can also be a source of user input.
- */
-class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
- GrapeHeadersSource() {
- this.asExpr().getExpr() instanceof GrapeHeadersCall
- or
- this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
- }
-
- override string getSourceType() { result = "Grape::API#headers" }
-
- override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
-}
-
-/**
- * A call to `headers` from within a Grape API endpoint.
- */
-private class GrapeHeadersCall extends MethodCall {
- GrapeHeadersCall() {
- exists(GrapeEndpoint endpoint |
- this.getParent+() = endpoint.getBody().asCallableAstNode() and
- this.getMethodName() = "headers"
- )
- or
- // Also handle cases where headers is called on an instance of a Grape API class
- this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr()
- }
-}
-
-/**
- * A call to `request` from within a Grape API endpoint.
- * The request object can contain user input.
- */
-class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
- GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall }
-
- override string getSourceType() { result = "Grape::API#request" }
-
- override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
-}
-
-/**
- * A call to `route_param` from within a Grape API endpoint.
- * Route parameters are extracted from the URL path and can be a source of user input.
- */
-class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
- GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall }
-
- override string getSourceType() { result = "Grape::API#route_param" }
-
- override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
-}
-
-/**
- * A call to `request` from within a Grape API endpoint.
- */
-private class GrapeRequestCall extends MethodCall {
- GrapeRequestCall() {
- exists(GrapeEndpoint endpoint |
- this.getParent+() = endpoint.getBody().asCallableAstNode() and
- this.getMethodName() = "request"
- )
- or
- // Also handle cases where request is called on an instance of a Grape API class
- this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr()
- }
-}
-
-/**
- * A call to `route_param` from within a Grape API endpoint.
- */
-private class GrapeRouteParamCall extends MethodCall {
- GrapeRouteParamCall() {
- exists(GrapeEndpoint endpoint |
- this.getParent+() = endpoint.getBody().asExpr().getExpr() and
- this.getMethodName() = "route_param"
- )
- or
- // Also handle cases where route_param is called on an instance of a Grape API class
- this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr()
- }
-}
-
-/**
- * A call to `headers` block within a Grape API class.
- * This is different from the headers() method call - this is the DSL block for defining header requirements.
- */
-private class GrapeHeadersBlockCall extends MethodCall {
- GrapeHeadersBlockCall() {
- exists(GrapeApiClass api |
- this.getParent+() = api.getADeclaration() and
- this.getMethodName() = "headers" and
- exists(this.getBlock())
- )
- }
-}
-
-/**
- * A call to `cookies` block within a Grape API class.
- * This DSL block defines cookie requirements and those cookies are user-controlled.
- */
-private class GrapeCookiesBlockCall extends MethodCall {
- GrapeCookiesBlockCall() {
- exists(GrapeApiClass api |
- this.getParent+() = api.getADeclaration() and
- this.getMethodName() = "cookies" and
- exists(this.getBlock())
- )
- }
-}
-
-/**
- * A call to `cookies` method from within a Grape API endpoint or cookies block.
- * Similar to headers, cookies can be accessed as a method and are user-controlled input.
- */
-class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
- GrapeCookiesSource() {
- this.asExpr().getExpr() instanceof GrapeCookiesCall
- or
- this.asExpr().getExpr() instanceof GrapeCookiesBlockCall
- }
-
- override string getSourceType() { result = "Grape::API#cookies" }
-
- override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
-}
-
-/**
- * A call to `cookies` method from within a Grape API endpoint.
- */
-private class GrapeCookiesCall extends MethodCall {
- GrapeCookiesCall() {
- exists(GrapeEndpoint endpoint |
- this.getParent+() = endpoint.getBody().asCallableAstNode() and
- this.getMethodName() = "cookies"
- )
- or
- // Also handle cases where cookies is called on an instance of a Grape API class
- this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr()
- }
-}
-
-/**
- * A method defined within a `helpers` block in a Grape API class.
- * These methods become available in endpoint contexts through Grape's DSL.
- */
-private class GrapeHelperMethod extends Method {
- private GrapeApiClass apiClass;
-
- GrapeHelperMethod() {
- exists(DataFlow::CallNode helpersCall |
- helpersCall = apiClass.getAModuleLevelCall("helpers") and
- this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
- )
+ private class GrapeParamsCall extends ParamsCallImpl {
+ GrapeParamsCall() {
+ // Params calls within endpoint blocks
+ exists(GrapeApiClass api |
+ this.getMethodName() = "params" and
+ this.getParent+() = api.getADeclaration()
+ )
+ or
+ // Params calls within helper methods (defined in helpers blocks)
+ exists(GrapeApiClass api, DataFlow::CallNode helpersCall |
+ helpersCall = api.getAModuleLevelCall("helpers") and
+ this.getMethodName() = "params" and
+ this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
+ )
+ }
}
/**
- * Gets the API class that contains this helper method.
+ * A call to `headers` from within a Grape API endpoint or headers block.
+ * Headers can also be a source of user input.
*/
- GrapeApiClass getApiClass() { result = apiClass }
-}
+ class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
+ GrapeHeadersSource() {
+ this.asExpr().getExpr() instanceof GrapeHeadersCall
+ or
+ this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
+ }
-/**
- * Additional call-target to resolve helper method calls defined in `helpers` blocks.
- *
- * This class is responsible for resolving calls to helper methods defined in
- * `helpers` blocks, allowing the dataflow framework to accurately track
- * the flow of information between these methods and their call sites.
- */
-private class GrapeHelperMethodTarget extends AdditionalCallTarget {
- override DataFlowCallable viableTarget(CfgNodes::ExprNodes::CallCfgNode call) {
- // Find calls to helper methods from within Grape endpoints or other helper methods
- exists(GrapeHelperMethod helperMethod, MethodCall mc |
- result.asCfgScope() = helperMethod and
- mc = call.getAstNode() and
- mc.getMethodName() = helperMethod.getName() and
- mc.getParent+() = helperMethod.getApiClass().getADeclaration()
- )
+ override string getSourceType() { result = "Grape::API#headers" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
+ }
+
+ /**
+ * A call to `headers` from within a Grape API endpoint.
+ */
+ private class GrapeHeadersCall extends MethodCall {
+ GrapeHeadersCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "headers"
+ )
+ or
+ // Also handle cases where headers is called on an instance of a Grape API class
+ this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr()
+ }
+ }
+
+ /**
+ * A call to `request` from within a Grape API endpoint.
+ * The request object can contain user input.
+ */
+ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
+ GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall }
+
+ override string getSourceType() { result = "Grape::API#request" }
+
+ override Http::Server::RequestInputKind getKind() {
+ result = Http::Server::parameterInputKind()
+ }
+ }
+
+ /**
+ * A call to `route_param` from within a Grape API endpoint.
+ * Route parameters are extracted from the URL path and can be a source of user input.
+ */
+ class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
+ GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall }
+
+ override string getSourceType() { result = "Grape::API#route_param" }
+
+ override Http::Server::RequestInputKind getKind() {
+ result = Http::Server::parameterInputKind()
+ }
+ }
+
+ /**
+ * A call to `request` from within a Grape API endpoint.
+ */
+ private class GrapeRequestCall extends MethodCall {
+ GrapeRequestCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "request"
+ )
+ or
+ // Also handle cases where request is called on an instance of a Grape API class
+ this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr()
+ }
+ }
+
+ /**
+ * A call to `route_param` from within a Grape API endpoint.
+ */
+ private class GrapeRouteParamCall extends MethodCall {
+ GrapeRouteParamCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asExpr().getExpr() and
+ this.getMethodName() = "route_param"
+ )
+ or
+ // Also handle cases where route_param is called on an instance of a Grape API class
+ this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr()
+ }
+ }
+
+ /**
+ * A call to `headers` block within a Grape API class.
+ * This is different from the headers() method call - this is the DSL block for defining header requirements.
+ */
+ private class GrapeHeadersBlockCall extends MethodCall {
+ GrapeHeadersBlockCall() {
+ exists(GrapeApiClass api |
+ this.getParent+() = api.getADeclaration() and
+ this.getMethodName() = "headers" and
+ exists(this.getBlock())
+ )
+ }
+ }
+
+ /**
+ * A call to `cookies` block within a Grape API class.
+ * This DSL block defines cookie requirements and those cookies are user-controlled.
+ */
+ private class GrapeCookiesBlockCall extends MethodCall {
+ GrapeCookiesBlockCall() {
+ exists(GrapeApiClass api |
+ this.getParent+() = api.getADeclaration() and
+ this.getMethodName() = "cookies" and
+ exists(this.getBlock())
+ )
+ }
+ }
+
+ /**
+ * A call to `cookies` method from within a Grape API endpoint or cookies block.
+ * Similar to headers, cookies can be accessed as a method and are user-controlled input.
+ */
+ class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
+ GrapeCookiesSource() {
+ this.asExpr().getExpr() instanceof GrapeCookiesCall
+ or
+ this.asExpr().getExpr() instanceof GrapeCookiesBlockCall
+ }
+
+ override string getSourceType() { result = "Grape::API#cookies" }
+
+ override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
+ }
+
+ /**
+ * A call to `cookies` method from within a Grape API endpoint.
+ */
+ private class GrapeCookiesCall extends MethodCall {
+ GrapeCookiesCall() {
+ exists(GrapeEndpoint endpoint |
+ this.getParent+() = endpoint.getBody().asCallableAstNode() and
+ this.getMethodName() = "cookies"
+ )
+ or
+ // Also handle cases where cookies is called on an instance of a Grape API class
+ this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr()
+ }
+ }
+
+ /**
+ * A method defined within a `helpers` block in a Grape API class.
+ * These methods become available in endpoint contexts through Grape's DSL.
+ */
+ private class GrapeHelperMethod extends Method {
+ private GrapeApiClass apiClass;
+
+ GrapeHelperMethod() {
+ exists(DataFlow::CallNode helpersCall |
+ helpersCall = apiClass.getAModuleLevelCall("helpers") and
+ this.getParent+() = helpersCall.getBlock().asExpr().getExpr()
+ )
+ }
+
+ /**
+ * Gets the API class that contains this helper method.
+ */
+ GrapeApiClass getApiClass() { result = apiClass }
+ }
+
+ /**
+ * Additional call-target to resolve helper method calls defined in `helpers` blocks.
+ *
+ * This class is responsible for resolving calls to helper methods defined in
+ * `helpers` blocks, allowing the dataflow framework to accurately track
+ * the flow of information between these methods and their call sites.
+ */
+ private class GrapeHelperMethodTarget extends AdditionalCallTarget {
+ override DataFlowCallable viableTarget(CfgNodes::ExprNodes::CallCfgNode call) {
+ // Find calls to helper methods from within Grape endpoints or other helper methods
+ exists(GrapeHelperMethod helperMethod, MethodCall mc |
+ result.asCfgScope() = helperMethod and
+ mc = call.getAstNode() and
+ mc.getMethodName() = helperMethod.getName() and
+ mc.getParent+() = helperMethod.getApiClass().getADeclaration()
+ )
+ }
}
}
diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
index c9aa7c29082..c5f0798f7a6 100644
--- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
+++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql
@@ -3,20 +3,20 @@ import codeql.ruby.frameworks.Grape
import codeql.ruby.Concepts
import codeql.ruby.AST
-query predicate grapeApiClasses(GrapeApiClass api) { any() }
+query predicate grapeApiClasses(Grape::GrapeApiClass api) { any() }
-query predicate grapeEndpoints(GrapeApiClass api, GrapeEndpoint endpoint, string method, string path) {
+query predicate grapeEndpoints(Grape::GrapeApiClass api, Grape::GrapeEndpoint endpoint, string method, string path) {
endpoint = api.getAnEndpoint() and
method = endpoint.getHttpMethod() and
path = endpoint.getPath()
}
-query predicate grapeParams(GrapeParamsSource params) { any() }
+query predicate grapeParams(Grape::GrapeParamsSource params) { any() }
-query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
+query predicate grapeHeaders(Grape::GrapeHeadersSource headers) { any() }
-query predicate grapeRequest(GrapeRequestSource request) { any() }
+query predicate grapeRequest(Grape::GrapeRequestSource request) { any() }
-query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() }
+query predicate grapeRouteParam(Grape::GrapeRouteParamSource routeParam) { any() }
-query predicate grapeCookies(GrapeCookiesSource cookies) { any() }
+query predicate grapeCookies(Grape::GrapeCookiesSource cookies) { any() }
From bdeeb3217ec087d58345cdfcf813d3a56a5050ad Mon Sep 17 00:00:00 2001
From: Tom Hvitved
Date: Fri, 19 Sep 2025 15:06:47 +0200
Subject: [PATCH 29/90] Rust: Add path resolution tests
---
.../PathResolutionInlineExpectationsTest.qll | 12 +++++++----
.../library-tests/path-resolution/my2/mod.rs | 2 ++
.../path-resolution/my2/my3/mod.rs | 4 ++++
.../path-resolution/path-resolution.expected | 20 +++++++++++--------
4 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/rust/ql/lib/utils/test/PathResolutionInlineExpectationsTest.qll b/rust/ql/lib/utils/test/PathResolutionInlineExpectationsTest.qll
index 8d2fdb2d2eb..df668194c07 100644
--- a/rust/ql/lib/utils/test/PathResolutionInlineExpectationsTest.qll
+++ b/rust/ql/lib/utils/test/PathResolutionInlineExpectationsTest.qll
@@ -25,10 +25,14 @@ private module ResolveTest implements TestSig {
private predicate item(ItemNode i, string value) {
exists(string filepath, int line, boolean inMacro | itemAt(i, filepath, line, inMacro) |
- commmentAt(value, filepath, line)
- or
- not commmentAt(_, filepath, line) and
- value = i.getName()
+ if i instanceof SourceFile
+ then value = i.getFile().getBaseName()
+ else (
+ commmentAt(value, filepath, line)
+ or
+ not commmentAt(_, filepath, line) and
+ value = i.getName()
+ )
)
}
diff --git a/rust/ql/test/library-tests/path-resolution/my2/mod.rs b/rust/ql/test/library-tests/path-resolution/my2/mod.rs
index 85edb683202..6b86c78237c 100644
--- a/rust/ql/test/library-tests/path-resolution/my2/mod.rs
+++ b/rust/ql/test/library-tests/path-resolution/my2/mod.rs
@@ -15,6 +15,8 @@ pub use nested2::nested7::nested8::{ // $ item=I118
use nested2::nested5::nested6::f as nested6_f; // $ item=I116
+use std::ops::Deref; // $ item=Deref
+
pub mod my3;
#[path = "renamed.rs"]
diff --git a/rust/ql/test/library-tests/path-resolution/my2/my3/mod.rs b/rust/ql/test/library-tests/path-resolution/my2/my3/mod.rs
index b459ca05aa6..1a98df1b560 100644
--- a/rust/ql/test/library-tests/path-resolution/my2/my3/mod.rs
+++ b/rust/ql/test/library-tests/path-resolution/my2/my3/mod.rs
@@ -8,3 +8,7 @@ use super::super::h; // $ item=I25
use super::g; // $ item=I9
use super::nested6_f; // $ item=I116
+
+use super::*; // $ item=mod.rs
+
+trait MyTrait: Deref {} // $ MISSING: item=Deref
diff --git a/rust/ql/test/library-tests/path-resolution/path-resolution.expected b/rust/ql/test/library-tests/path-resolution/path-resolution.expected
index a908ec1e5c1..1a925a31cce 100644
--- a/rust/ql/test/library-tests/path-resolution/path-resolution.expected
+++ b/rust/ql/test/library-tests/path-resolution/path-resolution.expected
@@ -33,8 +33,8 @@ mod
| main.rs:712:1:764:1 | mod associated_types |
| main.rs:770:1:789:1 | mod impl_with_attribute_macro |
| my2/mod.rs:1:1:1:16 | mod nested2 |
-| my2/mod.rs:18:1:18:12 | mod my3 |
-| my2/mod.rs:20:1:21:10 | mod mymod |
+| my2/mod.rs:20:1:20:12 | mod my3 |
+| my2/mod.rs:22:1:23:10 | mod mymod |
| my2/nested2.rs:1:1:11:1 | mod nested3 |
| my2/nested2.rs:2:5:10:5 | mod nested4 |
| my2/nested2.rs:13:1:19:1 | mod nested5 |
@@ -406,7 +406,7 @@ resolvePath
| main.rs:814:5:814:14 | ...::f | my2/nested2.rs:15:9:17:9 | fn f |
| main.rs:815:5:815:11 | nested8 | my2/nested2.rs:22:5:26:5 | mod nested8 |
| main.rs:815:5:815:14 | ...::f | my2/nested2.rs:23:9:25:9 | fn f |
-| main.rs:816:5:816:7 | my3 | my2/mod.rs:18:1:18:12 | mod my3 |
+| main.rs:816:5:816:7 | my3 | my2/mod.rs:20:1:20:12 | mod my3 |
| main.rs:816:5:816:10 | ...::f | my2/my3/mod.rs:1:1:5:1 | fn f |
| main.rs:817:5:817:12 | nested_f | my/my4/my5/mod.rs:1:1:3:1 | fn f |
| main.rs:818:5:818:7 | m18 | main.rs:553:1:571:1 | mod m18 |
@@ -440,17 +440,21 @@ resolvePath
| my2/mod.rs:16:5:16:20 | ...::nested5 | my2/nested2.rs:13:1:19:1 | mod nested5 |
| my2/mod.rs:16:5:16:29 | ...::nested6 | my2/nested2.rs:14:5:18:5 | mod nested6 |
| my2/mod.rs:16:5:16:32 | ...::f | my2/nested2.rs:15:9:17:9 | fn f |
-| my2/mod.rs:23:9:23:13 | mymod | my2/mod.rs:20:1:21:10 | mod mymod |
-| my2/mod.rs:23:9:23:16 | ...::f | my2/renamed.rs:1:1:1:13 | fn f |
+| my2/mod.rs:18:5:18:7 | std | {EXTERNAL LOCATION} | Crate(std@0.0.0) |
+| my2/mod.rs:18:5:18:12 | ...::ops | {EXTERNAL LOCATION} | mod ops |
+| my2/mod.rs:18:5:18:19 | ...::Deref | {EXTERNAL LOCATION} | trait Deref |
+| my2/mod.rs:25:9:25:13 | mymod | my2/mod.rs:22:1:23:10 | mod mymod |
+| my2/mod.rs:25:9:25:16 | ...::f | my2/renamed.rs:1:1:1:13 | fn f |
| my2/my3/mod.rs:3:5:3:5 | g | my2/mod.rs:3:1:6:1 | fn g |
| my2/my3/mod.rs:4:5:4:5 | h | main.rs:56:1:75:1 | fn h |
-| my2/my3/mod.rs:7:5:7:9 | super | my2/mod.rs:1:1:23:34 | SourceFile |
+| my2/my3/mod.rs:7:5:7:9 | super | my2/mod.rs:1:1:25:34 | SourceFile |
| my2/my3/mod.rs:7:5:7:16 | ...::super | main.rs:1:1:826:2 | SourceFile |
| my2/my3/mod.rs:7:5:7:19 | ...::h | main.rs:56:1:75:1 | fn h |
-| my2/my3/mod.rs:8:5:8:9 | super | my2/mod.rs:1:1:23:34 | SourceFile |
+| my2/my3/mod.rs:8:5:8:9 | super | my2/mod.rs:1:1:25:34 | SourceFile |
| my2/my3/mod.rs:8:5:8:12 | ...::g | my2/mod.rs:3:1:6:1 | fn g |
-| my2/my3/mod.rs:10:5:10:9 | super | my2/mod.rs:1:1:23:34 | SourceFile |
+| my2/my3/mod.rs:10:5:10:9 | super | my2/mod.rs:1:1:25:34 | SourceFile |
| my2/my3/mod.rs:10:5:10:20 | ...::nested6_f | my2/nested2.rs:15:9:17:9 | fn f |
+| my2/my3/mod.rs:12:5:12:9 | super | my2/mod.rs:1:1:25:34 | SourceFile |
| my.rs:3:5:3:10 | nested | my.rs:1:1:1:15 | mod nested |
| my.rs:3:5:3:13 | ...::g | my/nested.rs:19:1:22:1 | fn g |
| my.rs:11:5:11:5 | g | my/nested.rs:19:1:22:1 | fn g |
From 50bf9ae7563e671814820819269200980cb5a5b3 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Sun, 21 Sep 2025 20:44:46 -0400
Subject: [PATCH 30/90] Refactor RootApi class to use getAnImmediateDescendent
for clarity
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index 0999be94505..ce0b47502f9 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -26,7 +26,7 @@ module Grape {
*/
class RootApi extends GrapeApiClass {
RootApi() {
- not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent())
+ not this = any(GrapeApiClass parent).getAnImmediateDescendent()
}
}
From 1bf6101967e860043695e2d93e10c34373ea8b39 Mon Sep 17 00:00:00 2001
From: Chad Bentz <1760475+felickz@users.noreply.github.com>
Date: Sun, 21 Sep 2025 20:52:28 -0400
Subject: [PATCH 31/90] Remove redundant exclusion of base Grape::API module
from GrapeApiClass - should not impact extracted application code
---
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
index ce0b47502f9..4e178792572 100644
--- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
+++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
@@ -45,8 +45,7 @@ module Grape {
*/
class GrapeApiClass extends DataFlow::ClassNode {
GrapeApiClass() {
- this = grapeApiBaseClass().getADescendentModule() and
- not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m)
+ this = grapeApiBaseClass().getADescendentModule()
}
/**
From b2cc01c490115f1c1078318e45d328a1ddbedc7c Mon Sep 17 00:00:00 2001
From: Tom Hvitved
Date: Mon, 22 Sep 2025 09:38:30 +0200
Subject: [PATCH 32/90] Rust: Visibility check for qualified path resolution
---
.../lib/codeql/rust/internal/CachedStages.qll | 2 +-
.../codeql/rust/internal/PathResolution.qll | 209 +++++++++++-------
.../path-resolution/my2/my3/mod.rs | 2 +-
.../path-resolution/path-resolution.expected | 1 +
4 files changed, 136 insertions(+), 78 deletions(-)
diff --git a/rust/ql/lib/codeql/rust/internal/CachedStages.qll b/rust/ql/lib/codeql/rust/internal/CachedStages.qll
index cfd3d690522..132b9ec8f7e 100644
--- a/rust/ql/lib/codeql/rust/internal/CachedStages.qll
+++ b/rust/ql/lib/codeql/rust/internal/CachedStages.qll
@@ -117,7 +117,7 @@ module Stages {
or
exists(resolvePath(_))
or
- exists(any(ItemNode i).getASuccessor(_, _))
+ exists(any(ItemNode i).getASuccessor(_, _, _))
or
exists(any(ImplOrTraitItemNode i).getASelfPath())
or
diff --git a/rust/ql/lib/codeql/rust/internal/PathResolution.qll b/rust/ql/lib/codeql/rust/internal/PathResolution.qll
index 44e8b452255..785dc2e4319 100644
--- a/rust/ql/lib/codeql/rust/internal/PathResolution.qll
+++ b/rust/ql/lib/codeql/rust/internal/PathResolution.qll
@@ -6,6 +6,7 @@ private import rust
private import codeql.rust.elements.internal.generated.ParentChild
private import codeql.rust.internal.CachedStages
private import codeql.rust.frameworks.stdlib.Builtins as Builtins
+private import codeql.util.Option
private newtype TNamespace =
TTypeNamespace() or
@@ -78,6 +79,10 @@ private ItemNode getAChildSuccessor(ItemNode item, string name, SuccessorKind ki
)
}
+private module UseOption = Option