Skip to content

Commit 6b06518

Browse files
committed
fix(pipeline): reject non-http slash arguments
Signed-off-by: Yyunozor <yyunozor@icloud.com>
1 parent 2789301 commit 6b06518

5 files changed

Lines changed: 85 additions & 1 deletion

File tree

‎internal/cbm/service_patterns.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,11 @@ static bool callee_is_delimiter_or_filesystem_builder(const char *callee_name) {
678678
method = last_colon + 2;
679679
}
680680
if (strcmp(method, "split") == 0 || strcmp(method, "rsplit") == 0 ||
681-
strcmp(method, "partition") == 0 || strcmp(method, "join") == 0) {
681+
strcmp(method, "partition") == 0 || strcmp(method, "join") == 0 ||
682+
strcmp(method, "replace") == 0 || strcmp(method, "replaceAll") == 0 ||
683+
strcmp(method, "match") == 0 || strcmp(method, "matchAll") == 0 ||
684+
strcmp(method, "search") == 0 || strcmp(method, "test") == 0 ||
685+
strcmp(method, "exec") == 0) {
682686
return true;
683687
}
684688
return strstr(callee_name, "os.path.join") != NULL || strstr(callee_name, "path.join") != NULL;

‎internal/cbm/service_patterns.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn);
4444
* classified via its real resolved QN instead and never reaches this check. */
4545
bool cbm_service_pattern_is_global_fetch(const char *callee_name);
4646

47+
/* True when a string literal is a plausible HTTP route for the given callee.
48+
* Rejects filesystem paths and non-HTTP string consumers. */
49+
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
50+
4751
/* Per-worker TLS cache for cbm_service_pattern_match results. The
4852
* pattern matcher runs once per resolved CALL edge in emit_service_
4953
* edge — that's 6 pattern lists × ~30 patterns × strstr per call ≈

‎src/pipeline/pass_parallel.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,13 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
18361836
const CBMCall *call) {
18371837
for (int ai = 0; ai < call->arg_count; ai++) {
18381838
const CBMCallArg *ca = &call->args[ai];
1839+
/* A slash-prefixed raw expression is not a URL string. In JS/TS this
1840+
* is notably a regex literal (`/<table/i`); genuine string literals
1841+
* and propagated constants are carried in `value`, while template
1842+
* literals keep their leading backtick in `expr`. */
1843+
if (!ca->value && ca->expr && ca->expr[0] == '/') {
1844+
continue;
1845+
}
18391846
const char *url = ca->value ? ca->value : ca->expr;
18401847
if (!url || (url[0] != '/' && url[0] != '`')) {
18411848
continue;
@@ -1844,6 +1851,9 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
18441851
if (!normalize_url_arg(url, norm, (int)sizeof(norm))) {
18451852
continue;
18461853
}
1854+
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
1855+
continue;
1856+
}
18471857
char route_qn[CBM_ROUTE_QN_SIZE];
18481858
char cpath[CBM_SZ_256];
18491859
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",

‎tests/test_infrascan.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) {
2626
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/var/run/app.json", "requests.get"));
2727
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/locations/", "str.split"));
2828
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/api", "os.path.join"));
29+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/html/g", "template.replace"));
2930
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal(NULL, "requests.get"));
3031
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("", "requests.get"));
3132
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/orders", "requests.get"));

‎tests/test_pipeline.c‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,70 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
11841184
PASS();
11851185
}
11861186

1187+
/* Slash-prefixed call arguments are not necessarily HTTP routes. Keep the
1188+
* parallel arg-url heuristic from minting Route nodes for filesystem paths or
1189+
* regex-replacement operands, while preserving a genuine API path. */
1190+
TEST(pipeline_arg_url_rejects_non_http_slash_arguments) {
1191+
char tmp[256];
1192+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_guard_XXXXXX");
1193+
if (!cbm_mkdtemp(tmp)) {
1194+
FAIL("tmpdir");
1195+
}
1196+
1197+
write_temp_file(tmp, "src/args.py",
1198+
"import requests\n"
1199+
"TMP = '/tmp/pgv_fuzz.bin'\n"
1200+
"def run_copy(path):\n"
1201+
" return path\n"
1202+
"def write_fixture():\n"
1203+
" return run_copy(TMP)\n"
1204+
"def load_api():\n"
1205+
" return requests.get('/api/data')\n");
1206+
write_temp_file(tmp, "src/regex.js",
1207+
"function sink(value) { return value; }\n"
1208+
"export function sanitize(template) {\n"
1209+
" sink(/<table/i);\n"
1210+
" return template.replace('/html/g', '');\n"
1211+
"}\n");
1212+
for (int i = 0; i < 52; i++) {
1213+
char name[64];
1214+
char body[128];
1215+
snprintf(name, sizeof(name), "src/filler%d.ts", i);
1216+
snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i);
1217+
write_temp_file(tmp, name, body);
1218+
}
1219+
1220+
char *old_workers = getenv("CBM_WORKERS");
1221+
char *saved = old_workers ? strdup(old_workers) : NULL;
1222+
cbm_setenv("CBM_WORKERS", "4", 1);
1223+
1224+
char db_path[512];
1225+
snprintf(db_path, sizeof(db_path), "%s/arg_url_guard.db", tmp);
1226+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
1227+
ASSERT_NOT_NULL(p);
1228+
ASSERT_EQ(cbm_pipeline_run(p), 0);
1229+
const char *project = cbm_pipeline_project_name(p);
1230+
1231+
cbm_store_t *s = cbm_store_open_path(db_path);
1232+
ASSERT_NOT_NULL(s);
1233+
1234+
ASSERT_EQ(count_nodes_named(s, project, "/html/g"), 0);
1235+
ASSERT_EQ(count_nodes_named(s, project, "/<table/i"), 0);
1236+
ASSERT_EQ(count_nodes_named(s, project, "/tmp/pgv_fuzz.bin"), 0);
1237+
ASSERT_GTE(count_nodes_named(s, project, "/api/data"), 1);
1238+
1239+
cbm_store_close(s);
1240+
cbm_pipeline_free(p);
1241+
if (saved) {
1242+
cbm_setenv("CBM_WORKERS", saved, 1);
1243+
free(saved);
1244+
} else {
1245+
cbm_unsetenv("CBM_WORKERS");
1246+
}
1247+
th_rmtree(tmp);
1248+
PASS();
1249+
}
1250+
11871251
/* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare
11881252
* unqualified call to the global fetch API has no import and no local
11891253
* definition anywhere in this project, so registry resolution comes back
@@ -7790,6 +7854,7 @@ SUITE(pipeline) {
77907854
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
77917855
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
77927856
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
7857+
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
77937858
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
77947859
RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls);
77957860
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);

0 commit comments

Comments
 (0)