From d2fedd533df37991d7149bb29e6e106094ed912f Mon Sep 17 00:00:00 2001 From: "timmy.yuan" Date: Fri, 9 Oct 2026 12:40:17 +0800 Subject: [PATCH] fix(cpworker): reject content after the top-level value in config JSON cJSON was called with require_null_terminated=false, so a config file with trailing content (a stray comma, a second object, a truncated or concatenated write) was read up to its first complete value and accepted. Require only whitespace after the value. The error names the position of the trailing content. Duplicate keys and invalid \u escapes are left as they are: the config is generated by cpdaemon, which does not produce them. Fixes #238 --- cpworker/src/cjson_utils.c | 4 ++- cpworker/tests/unit/config_file.c | 46 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cpworker/src/cjson_utils.c b/cpworker/src/cjson_utils.c index c5c32382..a7a2cd15 100644 --- a/cpworker/src/cjson_utils.c +++ b/cpworker/src/cjson_utils.c @@ -12,7 +12,9 @@ cJSON *cjson_parse(const char *json, cJSONParseError *err) { const char *end = NULL; - cJSON *item = cJSON_ParseWithOpts(json, &end, false); + // Anything but whitespace after the top-level value is an error, so a truncated or concatenated file is not + // silently read up to its first complete value. + cJSON *item = cJSON_ParseWithOpts(json, &end, true); if (item) return item; diff --git a/cpworker/tests/unit/config_file.c b/cpworker/tests/unit/config_file.c index 2ab5420d..c4d107af 100644 --- a/cpworker/tests/unit/config_file.c +++ b/cpworker/tests/unit/config_file.c @@ -109,6 +109,47 @@ void test_syntax_error_in_config_file_names_the_position(void) unlink(path); } +// Content after the top-level value + +#define VALID_CONFIG \ + "{\"tasks\": [{\"capturer\": {\"type\": \"libpcap\", \"libpcap\": {\"interface\": \"eth0\"}}, \"outputs\": " \ + "[{\"type\": \"null\"}]}]}" + +void test_trailing_garbage_is_rejected(void) +{ + cJSONParseError err = {0}; + TEST_ASSERT_NULL(parse_config_data("{\"tasks\": []},", &err)); + TEST_ASSERT_EQUAL_STRING("JSON parse error at line 1, column 14 near ','", err.message); +} + +void test_second_top_level_value_is_rejected(void) +{ + cJSONParseError err = {0}; + TEST_ASSERT_NULL(parse_config_data(VALID_CONFIG "\n" VALID_CONFIG "\n", &err)); + assert_message_contains(&err, "JSON parse error at line 2, column 1 near '{\"tasks\""); +} + +void test_trailing_garbage_in_config_file_is_rejected(void) +{ + const char *path = write_tmp_file(VALID_CONFIG "\n}\n"); + cJSONParseError err = {0}; + TEST_ASSERT_NULL(parse_config_file(path, &err)); + TEST_ASSERT_EQUAL_STRING("JSON parse error at line 2, column 1 near '}'", err.message); + + err = (cJSONParseError){0}; + TEST_ASSERT_NULL(parse_tasks_file(path, &err)); + TEST_ASSERT_EQUAL_STRING("JSON parse error at line 2, column 1 near '}'", err.message); + unlink(path); +} + +void test_trailing_whitespace_is_accepted(void) +{ + cJSONParseError err = {0}; + Config *config = parse_config_data(VALID_CONFIG " \t\r\n\r\n", &err); + TEST_ASSERT_NOT_NULL_MESSAGE(config, err.message); + free_config(config); +} + // The control thread parses handshakes and commands while the reload thread parses the config. cJSON records the // last error position in a process-wide variable; the config error must not depend on it. static bool stop_parsing; @@ -162,5 +203,10 @@ int main(void) RUN_TEST(test_syntax_error_in_config_file_names_the_position); RUN_TEST(test_syntax_error_is_not_affected_by_concurrent_parsing); + RUN_TEST(test_trailing_garbage_is_rejected); + RUN_TEST(test_second_top_level_value_is_rejected); + RUN_TEST(test_trailing_garbage_in_config_file_is_rejected); + RUN_TEST(test_trailing_whitespace_is_accepted); + return UNITY_END(); }