327int main(
int argc,
char** argv) {
329 std::cerr <<
"usage: archive_worker <archive-path>\n";
333 const fs::path path(argv[1]);
336 bool stored_size_mismatch =
false;
337 bool header_mismatch =
false;
338 bool invalid_offset =
false;
339 bool overlapping_entries =
false;
340 bool nested_archive =
false;
343 std::string risk =
"CLEAN";
344 std::string reason_code;
347 std::ifstream f(path, std::ios::binary);
349 std::cerr <<
"cannot open file\n";
353 f.seekg(0, std::ios::end);
354 const std::uint64_t file_size =
static_cast<std::uint64_t
>(f.tellg());
371 f.seekg(0, std::ios::beg);
391 std::uint64_t eocd_offset = 0;
392 if (!
find_eocd(f, file_size, eocd_offset)) {
395 reason_code =
"ZIP_EOCD_NOT_FOUND";
396 detail =
"end of central directory not found";
398 f.seekg(
static_cast<std::streamoff
>(eocd_offset), std::ios::beg);
403 reason_code =
"ZIP_EOCD_INVALID";
404 detail =
"invalid end of central directory signature";
414 if (entries_this_disk != total_entries) {
415 header_mismatch =
true;
419 if (
static_cast<std::uint64_t
>(cd_offset) + cd_size > file_size) {
420 invalid_offset =
true;
422 reason_code =
"ZIP_INVALID_OFFSET";
423 detail =
"central directory outside file";
425 std::vector<ZipEntryInfo> entries;
426 entries.reserve(total_entries);
428 std::uint64_t pos = cd_offset;
429 for (uint16_t i = 0; i < total_entries; ++i) {
430 if (pos + 46 > file_size) {
431 invalid_offset =
true;
433 if (reason_code.empty()) {
434 reason_code =
"ZIP_INVALID_OFFSET";
435 detail =
"central directory header outside file";
440 f.seekg(
static_cast<std::streamoff
>(pos), std::ios::beg);
443 header_mismatch =
true;
445 if (reason_code.empty()) {
446 reason_code =
"ZIP_HEADER_MISMATCH";
447 detail =
"central directory signature mismatch";
467 const uint32_t local_header_offset_32 =
read_u32_le(f);
470 if (cd_name.size() != name_len) {
471 invalid_offset =
true;
473 if (reason_code.empty()) {
474 reason_code =
"ZIP_INVALID_OFFSET";
475 detail =
"cannot read central directory filename";
481 std::vector<unsigned char> cd_extra(extra_len);
483 f.read(
reinterpret_cast<char*
>(cd_extra.data()), extra_len);
484 if (
static_cast<std::size_t
>(f.gcount()) != extra_len) {
485 invalid_offset =
true;
487 if (reason_code.empty()) {
488 reason_code =
"ZIP_INVALID_OFFSET";
489 detail =
"cannot read central directory extra field";
497 const bool cd_comp_is_sentinel = (cd_comp_size == 0xFFFFFFFFu);
498 const bool cd_uncomp_is_sentinel = (cd_uncomp_size == 0xFFFFFFFFu);
499 const bool cd_off_is_sentinel = (local_header_offset_32 == 0xFFFFFFFFu);
501 std::uint64_t cd_comp_size_64 = cd_comp_size;
502 std::uint64_t cd_uncomp_size_64 = cd_uncomp_size;
503 std::uint64_t local_header_offset = local_header_offset_32;
504 bool cd_has_zip64_extra =
false;
505 if (cd_comp_is_sentinel || cd_uncomp_is_sentinel || cd_off_is_sentinel) {
508 cd_uncomp_is_sentinel,
511 cd_uncomp_is_sentinel ? &cd_uncomp_size_64 :
nullptr,
512 cd_comp_is_sentinel ? &cd_comp_size_64 :
nullptr,
513 cd_off_is_sentinel ? &local_header_offset :
nullptr);
517 if (cd_off_is_sentinel && !cd_has_zip64_extra) {
518 invalid_offset =
true;
520 if (reason_code.empty()) {
521 reason_code =
"ZIP_INVALID_OFFSET";
522 detail =
"missing ZIP64 extra field for local header offset";
530 header_mismatch =
true;
532 if (reason_code.empty()) {
533 reason_code =
"ZIP_PATH_TRAVERSAL_NAME";
534 detail =
"suspicious path inside archive";
539 nested_archive =
true;
542 if (local_header_offset >= file_size) {
543 invalid_offset =
true;
545 if (reason_code.empty()) {
546 reason_code =
"ZIP_INVALID_OFFSET";
547 detail =
"local header offset outside file";
550 uint16_t local_method = 0;
551 uint32_t local_comp_size = 0;
552 uint32_t local_uncomp_size = 0;
553 uint16_t local_gp_flags = 0;
554 std::string local_name;
555 std::uint64_t data_offset = 0;
556 std::uint64_t data_end = 0;
557 std::uint64_t local_zip64_comp = 0;
558 std::uint64_t local_zip64_uncomp = 0;
559 bool local_has_zip64_extra =
false;
574 local_has_zip64_extra))
576 invalid_offset =
true;
578 if (reason_code.empty()) {
579 reason_code =
"ZIP_INVALID_OFFSET";
580 detail =
"invalid local header";
586 const bool local_has_dd = (local_gp_flags & 0x08) != 0;
587 const bool cd_has_dd = (cd_gp_flags & 0x08) != 0;
590 std::uint64_t eff_local_comp = local_comp_size;
591 std::uint64_t eff_local_uncomp = local_uncomp_size;
592 if (local_comp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
593 eff_local_comp = local_zip64_comp;
595 if (local_uncomp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
596 eff_local_uncomp = local_zip64_uncomp;
601 const bool skip_size_check = local_has_dd &&
602 local_comp_size == 0 &&
603 local_uncomp_size == 0;
605 bool name_mismatch = (local_name != cd_name);
606 bool method_mismatch = (local_method != cd_method);
607 bool size_mismatch = !skip_size_check &&
608 (eff_local_comp != cd_comp_size_64 ||
609 eff_local_uncomp != cd_uncomp_size_64);
612 bool dd_flag_mismatch = (local_has_dd != cd_has_dd);
614 if (name_mismatch || method_mismatch || size_mismatch) {
615 header_mismatch =
true;
617 if (reason_code.empty()) {
618 reason_code =
"ZIP_HEADER_MISMATCH";
619 detail =
"local header and central directory differ";
621 }
else if (dd_flag_mismatch) {
628 if (cd_method == 0 &&
629 !cd_comp_is_sentinel &&
630 !cd_uncomp_is_sentinel &&
632 cd_comp_size != cd_uncomp_size)
634 stored_size_mismatch =
true;
636 reason_code =
"ZIP_STORED_SIZE_MISMATCH";
637 detail =
"STORED entry but compressed and uncompressed sizes differ";
641 entry.name = cd_name;
642 entry.method = cd_method;
643 entry.compressed_size = cd_comp_size;
644 entry.uncompressed_size = cd_uncomp_size;
645 entry.local_header_offset =
646 static_cast<uint32_t
>(local_header_offset & 0xFFFFFFFFu);
647 entry.data_offset = data_offset;
651 entry.data_end = data_offset + cd_comp_size_64;
652 entries.push_back(std::move(entry));
656 pos += 46ULL + name_len + extra_len + comment_len;
657 if (pos > file_size) {
658 invalid_offset =
true;
660 if (reason_code.empty()) {
661 reason_code =
"ZIP_INVALID_OFFSET";
662 detail =
"central directory entry overflow";
668 if (!entries.empty()) {
669 std::sort(entries.begin(), entries.end(),
670 [](
const ZipEntryInfo& a,
const ZipEntryInfo& b) {
671 return a.data_offset < b.data_offset;
674 for (std::size_t i = 1; i < entries.size(); ++i) {
675 if (entries[i].data_offset < entries[i - 1].data_end) {
676 overlapping_entries =
true;
678 if (reason_code.empty()) {
679 reason_code =
"ZIP_OVERLAPPING_ENTRIES";
680 detail =
"overlapping archive entries detected";
687 if (nested_archive) {
692 if (reason_code.empty()) {
693 reason_code =
"ZIP_NESTED_ARCHIVE";
694 detail =
"nested archive detected";
706 if (risk ==
"MALFORMED") {
708 }
else if (risk_score >= 60) {
710 }
else if (risk_score >= 30) {
bool read_local_entry(std::ifstream &f, std::uint64_t file_size, std::uint64_t local_offset, uint16_t &method, uint32_t &compressed_size, uint32_t &uncompressed_size, uint16_t &gp_flags_out, std::string &filename, std::uint64_t &data_offset, std::uint64_t &data_end, std::uint64_t &zip64_comp_size, std::uint64_t &zip64_uncomp_size, bool &has_zip64_extra)