diff --git a/src/cli/cli.c b/src/cli/cli.c index 5930d317d..a3492d065 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -80,6 +80,7 @@ enum { #endif #include // EEXIST #include // open, O_WRONLY, O_CREAT, O_TRUNC +#include // UINT_MAX #include // uintptr_t #include #include @@ -2485,12 +2486,22 @@ enum { ZIP_STORED = 0, ZIP_DEFLATE = 8 }; -static const uint32_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U; +static const size_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U; + +static uint16_t zip_read_u16le(const unsigned char *p) { + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << BYTE_SHIFT)); +} + +static uint32_t zip_read_u32le(const unsigned char *p) { + return ((uint32_t)p[0]) | ((uint32_t)p[1] << BYTE_SHIFT) | + ((uint32_t)p[2] << (BYTE_SHIFT * CLI_PAIR_LEN)) | + ((uint32_t)p[3] << (BYTE_SHIFT * CLI_JSON_INDENT)); +} /* Decompress a single zip entry (stored or deflated). Returns malloc'd buffer * or NULL on failure. *out_len receives the decompressed size. */ static unsigned char *zip_extract_entry(const unsigned char *file_data, uint16_t method, - uint32_t comp_size, uint32_t uncomp_size, int *out_len) { + size_t comp_size, size_t uncomp_size, int *out_len) { if (method == ZIP_STORED) { if (comp_size > ZIP_MAX_UNCOMP) { return NULL; @@ -2507,15 +2518,18 @@ static unsigned char *zip_extract_entry(const unsigned char *file_data, uint16_t if (uncomp_size > ZIP_MAX_UNCOMP) { return NULL; } + if (comp_size > UINT_MAX || uncomp_size > UINT_MAX) { + return NULL; + } unsigned char *out = malloc(uncomp_size); if (!out) { return NULL; } z_stream strm = {0}; strm.next_in = (unsigned char *)file_data; - strm.avail_in = comp_size; + strm.avail_in = (uInt)comp_size; strm.next_out = out; - strm.avail_out = uncomp_size; + strm.avail_out = (uInt)uncomp_size; if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) { free(out); return NULL; @@ -2545,28 +2559,14 @@ unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_l break; } - uint16_t method = (uint16_t)(data[pos + ZIP_OFF_METHOD] | - (data[pos + ZIP_OFF_METHOD + CLI_SKIP_ONE] << BYTE_SHIFT)); - uint32_t comp_size = - (uint32_t)(data[pos + ZIP_OFF_COMP] | - (data[pos + ZIP_OFF_COMP + CLI_SKIP_ONE] << BYTE_SHIFT) | - (data[pos + ZIP_OFF_COMP + CLI_PAIR_LEN] << (BYTE_SHIFT * CLI_PAIR_LEN)) | - (data[pos + ZIP_OFF_COMP + CLI_JSON_INDENT] - << (BYTE_SHIFT * CLI_JSON_INDENT))); - uint32_t uncomp_size = - (uint32_t)(data[pos + ZIP_OFF_UNCOMP] | - (data[pos + ZIP_OFF_UNCOMP + CLI_SKIP_ONE] << BYTE_SHIFT) | - (data[pos + ZIP_OFF_UNCOMP + CLI_PAIR_LEN] << (BYTE_SHIFT * CLI_PAIR_LEN)) | - (data[pos + ZIP_OFF_UNCOMP + CLI_JSON_INDENT] - << (BYTE_SHIFT * CLI_JSON_INDENT))); - uint16_t name_len = (uint16_t)(data[pos + ZIP_OFF_NAMELEN] | - (data[pos + ZIP_OFF_NAMELEN + CLI_SKIP_ONE] << BYTE_SHIFT)); - uint16_t extra_len = - (uint16_t)(data[pos + ZIP_OFF_EXTRALEN] | - (data[pos + ZIP_OFF_EXTRALEN + CLI_SKIP_ONE] << BYTE_SHIFT)); + uint16_t method = zip_read_u16le(data + pos + ZIP_OFF_METHOD); + uint32_t comp_size = zip_read_u32le(data + pos + ZIP_OFF_COMP); + uint32_t uncomp_size = zip_read_u32le(data + pos + ZIP_OFF_UNCOMP); + uint16_t name_len = zip_read_u16le(data + pos + ZIP_OFF_NAMELEN); + uint16_t extra_len = zip_read_u16le(data + pos + ZIP_OFF_EXTRALEN); int header_end = pos + ZIP_HDR_SZ + name_len + extra_len; - if (header_end + (int)comp_size > data_len) { + if (header_end > data_len || comp_size > (uint32_t)(data_len - header_end)) { break; } diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 77bc7105a..b211ae815 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -2441,11 +2441,11 @@ static bool eval_condition(const cbm_condition_t *c, binding_t *b) { /* IS NULL / IS NOT NULL */ if (strcmp(c->op, "IS NULL") == 0) { - result = (!actual || actual[0] == '\0'); + result = (actual[0] == '\0'); return c->negated ? !result : result; } if (strcmp(c->op, "IS NOT NULL") == 0) { - result = (actual && actual[0] != '\0'); + result = (actual[0] != '\0'); return c->negated ? !result : result; } diff --git a/tests/test_cli.c b/tests/test_cli.c index af300ab51..d8a71e342 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -14,6 +14,7 @@ #include "test_helpers.h" #include #include +#include #include #include #include @@ -1361,6 +1362,34 @@ TEST(cli_extract_binary_from_zip_invalid) { PASS(); } +TEST(cli_extract_binary_from_zip_truncated_large_entry) { + unsigned char zip[30 + 19] = {0}; + const char *filename = "codebase-memory-mcp"; + uint32_t declared_size = 65536U; + zip[0] = 0x50; + zip[1] = 0x4B; + zip[2] = 0x03; + zip[3] = 0x04; + zip[8] = 0; + zip[9] = 0; + zip[18] = (unsigned char)(declared_size & 0xFF); + zip[19] = (unsigned char)((declared_size >> 8) & 0xFF); + zip[20] = (unsigned char)((declared_size >> 16) & 0xFF); + zip[21] = (unsigned char)((declared_size >> 24) & 0xFF); + zip[22] = zip[18]; + zip[23] = zip[19]; + zip[24] = zip[20]; + zip[25] = zip[21]; + zip[26] = 19; + zip[27] = 0; + memcpy(zip + 30, filename, strlen(filename)); + + int out_len = 0; + unsigned char *extracted = cbm_extract_binary_from_zip(zip, sizeof(zip), &out_len); + ASSERT_NULL(extracted); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Skill dry-run tests * ═══════════════════════════════════════════════════════════════════ */ @@ -2981,6 +3010,7 @@ SUITE(cli) { RUN_TEST(cli_extract_binary_from_zip_not_found); RUN_TEST(cli_extract_binary_from_zip_path_traversal); RUN_TEST(cli_extract_binary_from_zip_invalid); + RUN_TEST(cli_extract_binary_from_zip_truncated_large_entry); /* Dry-run lifecycle (2 tests) */ RUN_TEST(cli_install_dry_run);