From c86f1ffafdaf0880f46c53860de89529f663df29 Mon Sep 17 00:00:00 2001 From: Yyunozor Date: Fri, 31 Jul 2026 18:03:34 +0200 Subject: [PATCH] fix(pipeline): reject non-http slash arguments Signed-off-by: Yyunozor --- internal/cbm/service_patterns.c | 6 ++- internal/cbm/service_patterns.h | 4 ++ src/pipeline/pass_parallel.c | 10 +++++ src/pipeline/pass_route_nodes.c | 3 +- tests/test_infrascan.c | 1 + tests/test_pipeline.c | 65 +++++++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 5ff5abeb5..e3740f5aa 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -678,7 +678,11 @@ static bool callee_is_delimiter_or_filesystem_builder(const char *callee_name) { method = last_colon + 2; } if (strcmp(method, "split") == 0 || strcmp(method, "rsplit") == 0 || - strcmp(method, "partition") == 0 || strcmp(method, "join") == 0) { + strcmp(method, "partition") == 0 || strcmp(method, "join") == 0 || + strcmp(method, "replace") == 0 || strcmp(method, "replaceAll") == 0 || + strcmp(method, "match") == 0 || strcmp(method, "matchAll") == 0 || + strcmp(method, "search") == 0 || strcmp(method, "test") == 0 || + strcmp(method, "exec") == 0) { return true; } return strstr(callee_name, "os.path.join") != NULL || strstr(callee_name, "path.join") != NULL; diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index 28cbfe1b9..4642a08de 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -44,6 +44,10 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn); * classified via its real resolved QN instead and never reaches this check. */ bool cbm_service_pattern_is_global_fetch(const char *callee_name); +/* True when a string literal is a plausible HTTP route for the given callee. + * Rejects filesystem paths and non-HTTP string consumers. */ +bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name); + /* Per-worker TLS cache for cbm_service_pattern_match results. The * pattern matcher runs once per resolved CALL edge in emit_service_ * edge — that's 6 pattern lists × ~30 patterns × strstr per call ≈ diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 13e395a04..1ce766927 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1792,6 +1792,13 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call) { for (int ai = 0; ai < call->arg_count; ai++) { const CBMCallArg *ca = &call->args[ai]; + /* A slash-prefixed raw expression is not a URL string. In JS/TS this + * is notably a regex literal (`/value && ca->expr && ca->expr[0] == '/') { + continue; + } const char *url = ca->value ? ca->value : ca->expr; if (!url || (url[0] != '/' && url[0] != '`')) { continue; @@ -1800,6 +1807,9 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, if (!normalize_url_arg(url, norm, (int)sizeof(norm))) { continue; } + if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) { + continue; + } char route_qn[CBM_ROUTE_QN_SIZE]; char cpath[CBM_SZ_256]; snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s", diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 664c8252c..163e5a1fc 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -33,12 +33,11 @@ enum { #include #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" +#include "service_patterns.h" /* cbm_service_pattern_is_http_route_literal */ #include #include -bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name); - /* True for characters that may appear in a ":name" route parameter. */ static inline bool is_route_ident_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; diff --git a/tests/test_infrascan.c b/tests/test_infrascan.c index a0e45d156..f6da94955 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -26,6 +26,7 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) { ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/var/run/app.json", "requests.get")); ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/locations/", "str.split")); ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/api", "os.path.join")); + ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/html/g", "template.replace")); ASSERT_FALSE(cbm_service_pattern_is_http_route_literal(NULL, "requests.get")); ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("", "requests.get")); ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/orders", "requests.get")); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 8e27a5d5d..d62a5572f 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -5352,6 +5352,70 @@ TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier) { PASS(); } +/* Slash-prefixed call arguments are not necessarily HTTP routes. Keep the + * parallel arg-url heuristic from minting Route nodes for filesystem paths or + * regex-replacement operands, while preserving a genuine API path. */ +TEST(pipeline_arg_url_rejects_non_http_slash_arguments) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_guard_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "src/args.py", + "import requests\n" + "TMP = '/tmp/pgv_fuzz.bin'\n" + "def run_copy(path):\n" + " return path\n" + "def write_fixture():\n" + " return run_copy(TMP)\n" + "def load_api():\n" + " return requests.get('/api/data')\n"); + write_temp_file(tmp, "src/regex.js", + "function sink(value) { return value; }\n" + "export function sanitize(template) {\n" + " sink(/