fix(client): harden CLI args, log escaping, and local artifact opens
- parse_ull_arg() rejects a leading '-'/'+' (strtoull would silently wrap -1 to ULLONG_MAX) and --chunk-size/--delta-max enforce their upper bounds. - Escape local untrusted paths before logging (client_send, scanner, --filter rule, pattern-file reads) with output_escape(..., 8-bit mode). - Read --exclude-from/--include-from through the bounded line reader. - Open --log-file with O_NOFOLLOW|O_CLOEXEC, mode 0600, via open+fdopen; create --write-batch with O_NOFOLLOW|O_CLOEXEC, mode 0600. - Reject --dry-run together with --write-batch (dry-run must not write the batch file), alongside the existing --read-batch/--only-write-batch rules. Tests: signed/oversized numeric rejection, over-long pattern file, dry-run + write-batch unit and integration coverage.
This commit is contained in:
@@ -117,4 +117,16 @@ def test_batch_modes_conflict():
|
||||
cmd = _run(["--source-dir", SOURCE_DIR, "--dest-dir", DEST1] + flags)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||
assert result.returncode != 0, \
|
||||
f"expected conflict failure for {flags}: {result.stderr}"
|
||||
f"expected conflict failure for {flags}: {result.stderr}"
|
||||
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_dry_run_rejects_write_batch():
|
||||
"""--dry-run must not emit a batch file (it must not mutate anything)."""
|
||||
if os.path.exists(BATCH_FILE):
|
||||
os.unlink(BATCH_FILE)
|
||||
cmd = _run(["--source-dir", SOURCE_DIR, "--dest-dir", DEST1,
|
||||
"--dry-run", "--write-batch", BATCH_FILE])
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||
assert result.returncode != 0, result.stderr
|
||||
assert not os.path.exists(BATCH_FILE), "dry-run must not create a batch file"
|
||||
@@ -3275,6 +3275,58 @@ static void test_parse_args_block_size() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* An over-long --exclude-from/--include-from line is rejected at parse time
|
||||
* rather than being read without a bound. */
|
||||
static void test_parse_args_pattern_file_oversized_rejected() {
|
||||
const char* list_path = "cli_pattern_oversized.txt";
|
||||
size_t len = UTILS_MAX_LINE_LEN + 4096;
|
||||
char* big = malloc(len);
|
||||
EXPECT_NOT_NULL(big);
|
||||
memset(big, 'a', len);
|
||||
write_file_bytes(list_path, big, len);
|
||||
free(big);
|
||||
|
||||
Config* cfg = config_create();
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
char* argv[] = {"fastsync", "--exclude-from", (char*)list_path, "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
remove(list_path);
|
||||
}
|
||||
|
||||
/* A leading '-'/'+' must be rejected for every unsigned numeric option so
|
||||
* strtoull can never silently wrap (e.g. -1 -> ULLONG_MAX). */
|
||||
static void test_parse_args_unsigned_options_reject_sign() {
|
||||
static const char* const opts[] = {"--chunk-size", "--bwlimit", "--delta-max"};
|
||||
for (size_t i = 0; i < sizeof(opts) / sizeof(opts[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
char* argv[] = {"fastsync", (char*)opts[i], "-1", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* An over-cap --chunk-size is rejected at parse time (max 64 MiB). */
|
||||
Config* cfg = config_create();
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
char* big_argv[] = {"fastsync", "--chunk-size", "67108865", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, big_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --dry-run must not emit a batch file, so it is rejected alongside
|
||||
* --read-batch/--only-write-batch. */
|
||||
static void test_validate_config_dry_run_rejects_write_batch() {
|
||||
Config* cfg = valid_client_config();
|
||||
cfg->dry_run = true;
|
||||
cfg->write_batch = str_dup("batch.dat");
|
||||
EXPECT_FALSE(validate_config(cfg));
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
void test_client_cli() {
|
||||
test_validate_config_required_paths();
|
||||
test_parse_args_numeric_ids();
|
||||
@@ -3432,4 +3484,7 @@ void test_client_cli() {
|
||||
test_parse_args_remote_option_short_M();
|
||||
test_parse_args_no_motd();
|
||||
test_parse_args_password_file();
|
||||
test_parse_args_pattern_file_oversized_rejected();
|
||||
test_parse_args_unsigned_options_reject_sign();
|
||||
test_validate_config_dry_run_rejects_write_batch();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user