|
11 | 11 | #include "node_profiling.h" |
12 | 12 | #include "node_snapshot_builder.h" |
13 | 13 | #include "permission/permission.h" |
| 14 | +#include "path.h" |
14 | 15 | #include "util-inl.h" |
15 | 16 | #include "v8-cppgc.h" |
16 | 17 | #include "v8-profiler.h" |
@@ -504,6 +505,264 @@ Worker::~Worker() { |
504 | 505 | Debug(this, "Worker %llu destroyed", thread_id_.id); |
505 | 506 | } |
506 | 507 |
|
| 508 | + |
| 509 | +// SEMVER-MAJOR: Permission ceiling for Worker explicit execArgv. |
| 510 | +static bool WorkerConfiguredPermission(const EnvironmentOptions* w) { |
| 511 | + if (w->permission || w->permission_audit) { |
| 512 | + return true; |
| 513 | + } |
| 514 | + if (!w->allow_fs_read.empty() || !w->allow_fs_write.empty()) { |
| 515 | + return true; |
| 516 | + } |
| 517 | + return w->allow_addons || w->allow_inspector || w->allow_child_process || |
| 518 | + w->allow_net || w->allow_wasi || w->allow_ffi || |
| 519 | + w->allow_openssl_store || w->allow_worker_threads; |
| 520 | +} |
| 521 | + |
| 522 | +static void ApplyParentPermissionCeiling(EnvironmentOptions* w, |
| 523 | + const EnvironmentOptions* parent) { |
| 524 | + w->permission = true; |
| 525 | + w->permission_audit = parent->permission_audit; |
| 526 | + w->allow_addons = parent->allow_addons; |
| 527 | + w->allow_inspector = parent->allow_inspector; |
| 528 | + w->allow_child_process = parent->allow_child_process; |
| 529 | + w->allow_net = parent->allow_net; |
| 530 | + w->allow_wasi = parent->allow_wasi; |
| 531 | + w->allow_ffi = parent->allow_ffi; |
| 532 | + w->allow_openssl_store = parent->allow_openssl_store; |
| 533 | + w->allow_worker_threads = parent->allow_worker_threads; |
| 534 | + w->allow_fs_read = parent->allow_fs_read; |
| 535 | + w->allow_fs_write = parent->allow_fs_write; |
| 536 | +} |
| 537 | + |
| 538 | +static void NormalizePathForCompare(std::string* s) { |
| 539 | + while (s->size() > 1 && (s->back() == '/' || s->back() == '\\')) { |
| 540 | + s->pop_back(); |
| 541 | + } |
| 542 | +#ifdef _WIN32 |
| 543 | + for (char& c : *s) { |
| 544 | + if (c >= 'A' && c <= 'Z') { |
| 545 | + c = static_cast<char>(c - 'A' + 'a'); |
| 546 | + } |
| 547 | + if (c == '/') { |
| 548 | + c = '\\'; |
| 549 | + } |
| 550 | + } |
| 551 | +#endif |
| 552 | +} |
| 553 | + |
| 554 | +static std::string ResolveForCompare(Environment* env, const std::string& in) { |
| 555 | + if (in.empty() || in == "*") { |
| 556 | + return in; |
| 557 | + } |
| 558 | + std::string resolved = |
| 559 | + PathResolve(env, std::vector<std::string_view>{std::string_view(in)}); |
| 560 | + if (resolved.empty()) { |
| 561 | + resolved = in; |
| 562 | + } |
| 563 | + NormalizePathForCompare(&resolved); |
| 564 | + return resolved; |
| 565 | +} |
| 566 | + |
| 567 | +// Create-time prefix filter; runtime FSPermission is authoritative. |
| 568 | +static bool PathCoveredByParentEntry(Environment* env, |
| 569 | + const std::string& parent_raw, |
| 570 | + const std::string& requested_raw) { |
| 571 | + if (parent_raw == "*") { |
| 572 | + return true; |
| 573 | + } |
| 574 | + const std::string parent = ResolveForCompare(env, parent_raw); |
| 575 | + const std::string requested = ResolveForCompare(env, requested_raw); |
| 576 | + if (parent.empty()) { |
| 577 | + return false; |
| 578 | + } |
| 579 | + if (requested == parent) { |
| 580 | + return true; |
| 581 | + } |
| 582 | + if (requested.size() <= parent.size()) { |
| 583 | + return false; |
| 584 | + } |
| 585 | + if (requested.compare(0, parent.size(), parent) != 0) { |
| 586 | + return false; |
| 587 | + } |
| 588 | + const char next = requested[parent.size()]; |
| 589 | + return next == '/' || next == '\\'; |
| 590 | +} |
| 591 | + |
| 592 | +static bool ParentListHasWildcard(const std::vector<std::string>& parent) { |
| 593 | + for (const std::string& entry : parent) { |
| 594 | + if (entry == "*") { |
| 595 | + return true; |
| 596 | + } |
| 597 | + } |
| 598 | + return false; |
| 599 | +} |
| 600 | + |
| 601 | +static void FilterPathListToParentSubset( |
| 602 | + Environment* env, |
| 603 | + EnvironmentOptions* w, |
| 604 | + std::vector<std::string>* worker, |
| 605 | + const std::vector<std::string>& parent) { |
| 606 | + if (worker == nullptr) { |
| 607 | + return; |
| 608 | + } |
| 609 | + if (worker->empty()) { |
| 610 | + if (w->permission || w->permission_audit) { |
| 611 | + return; |
| 612 | + } |
| 613 | + *worker = parent; |
| 614 | + return; |
| 615 | + } |
| 616 | + if (ParentListHasWildcard(parent)) { |
| 617 | + return; |
| 618 | + } |
| 619 | + std::vector<std::string> out; |
| 620 | + out.reserve(worker->size()); |
| 621 | + bool saw_star = false; |
| 622 | + for (const std::string& wpath : *worker) { |
| 623 | + if (wpath == "*") { |
| 624 | + saw_star = true; |
| 625 | + continue; |
| 626 | + } |
| 627 | + for (const std::string& entry : parent) { |
| 628 | + if (PathCoveredByParentEntry(env, entry, wpath)) { |
| 629 | + out.push_back(wpath); |
| 630 | + break; |
| 631 | + } |
| 632 | + } |
| 633 | + } |
| 634 | + if (saw_star && out.empty()) { |
| 635 | + *worker = parent; |
| 636 | + return; |
| 637 | + } |
| 638 | + *worker = std::move(out); |
| 639 | +} |
| 640 | + |
| 641 | +static void IntersectPermissionGrants(Environment* env, |
| 642 | + EnvironmentOptions* w, |
| 643 | + const EnvironmentOptions* parent) { |
| 644 | + w->permission = true; |
| 645 | + w->permission_audit = w->permission_audit || parent->permission_audit; |
| 646 | + w->allow_addons = w->allow_addons && parent->allow_addons; |
| 647 | + w->allow_inspector = w->allow_inspector && parent->allow_inspector; |
| 648 | + w->allow_child_process = |
| 649 | + w->allow_child_process && parent->allow_child_process; |
| 650 | + w->allow_net = w->allow_net && parent->allow_net; |
| 651 | + w->allow_wasi = w->allow_wasi && parent->allow_wasi; |
| 652 | + w->allow_ffi = w->allow_ffi && parent->allow_ffi; |
| 653 | + w->allow_openssl_store = |
| 654 | + w->allow_openssl_store && parent->allow_openssl_store; |
| 655 | + w->allow_worker_threads = |
| 656 | + w->allow_worker_threads && parent->allow_worker_threads; |
| 657 | + FilterPathListToParentSubset(env, w, &w->allow_fs_read, parent->allow_fs_read); |
| 658 | + FilterPathListToParentSubset( |
| 659 | + env, w, &w->allow_fs_write, parent->allow_fs_write); |
| 660 | +} |
| 661 | + |
| 662 | +static void ClampWorkerPermissionToParent(Environment* env, |
| 663 | + PerIsolateOptions* worker_opts) { |
| 664 | + if (worker_opts == nullptr || !env->permission()->enabled()) { |
| 665 | + return; |
| 666 | + } |
| 667 | + EnvironmentOptions* parent = env->isolate_data()->options()->get_per_env_options(); |
| 668 | + EnvironmentOptions* w = worker_opts->get_per_env_options(); |
| 669 | + if (parent == nullptr || w == nullptr) { |
| 670 | + return; |
| 671 | + } |
| 672 | + if (!WorkerConfiguredPermission(w)) { |
| 673 | + ApplyParentPermissionCeiling(w, parent); |
| 674 | + } else { |
| 675 | + IntersectPermissionGrants(env, w, parent); |
| 676 | + } |
| 677 | +} |
| 678 | + |
| 679 | +static bool IsPermissionCliToken(const std::string& a) { |
| 680 | + if (a == "--permission" || a == "--permission-audit") { |
| 681 | + return true; |
| 682 | + } |
| 683 | + static const char* kFlags[] = { |
| 684 | + "--allow-fs-read", |
| 685 | + "--allow-fs-write", |
| 686 | + "--allow-addons", |
| 687 | + "--allow-inspector", |
| 688 | + "--allow-child-process", |
| 689 | + "--allow-net", |
| 690 | + "--allow-wasi", |
| 691 | + "--allow-ffi", |
| 692 | + "--allow-openssl-store", |
| 693 | + "--allow-worker", |
| 694 | + }; |
| 695 | + for (const char* flag : kFlags) { |
| 696 | + const size_t n = std::char_traits<char>::length(flag); |
| 697 | + if (a == flag) { |
| 698 | + return true; |
| 699 | + } |
| 700 | + if (a.size() > n && a.compare(0, n, flag) == 0 && a[n] == '=') { |
| 701 | + return true; |
| 702 | + } |
| 703 | + } |
| 704 | + return false; |
| 705 | +} |
| 706 | + |
| 707 | +// Rebuild argv from clamped options. Do not assume argv[0] layout from Parse. |
| 708 | +static void RebuildExecArgvOutFromPermissionOptions( |
| 709 | + PerIsolateOptions* worker_opts, std::vector<std::string>* exec_argv_out) { |
| 710 | + if (worker_opts == nullptr || exec_argv_out == nullptr) { |
| 711 | + return; |
| 712 | + } |
| 713 | + EnvironmentOptions* w = worker_opts->get_per_env_options(); |
| 714 | + if (w == nullptr || !w->permission) { |
| 715 | + return; |
| 716 | + } |
| 717 | + |
| 718 | + std::vector<std::string> out; |
| 719 | + out.emplace_back(""); // program-name placeholder for parsers that expect it |
| 720 | + for (const std::string& tok : *exec_argv_out) { |
| 721 | + if (tok.empty()) { |
| 722 | + continue; |
| 723 | + } |
| 724 | + if (!IsPermissionCliToken(tok)) { |
| 725 | + out.push_back(tok); |
| 726 | + } |
| 727 | + } |
| 728 | + out.push_back("--permission"); |
| 729 | + if (w->permission_audit) { |
| 730 | + out.push_back("--permission-audit"); |
| 731 | + } |
| 732 | + if (w->allow_addons) { |
| 733 | + out.push_back("--allow-addons"); |
| 734 | + } |
| 735 | + if (w->allow_inspector) { |
| 736 | + out.push_back("--allow-inspector"); |
| 737 | + } |
| 738 | + if (w->allow_child_process) { |
| 739 | + out.push_back("--allow-child-process"); |
| 740 | + } |
| 741 | + if (w->allow_net) { |
| 742 | + out.push_back("--allow-net"); |
| 743 | + } |
| 744 | + if (w->allow_wasi) { |
| 745 | + out.push_back("--allow-wasi"); |
| 746 | + } |
| 747 | + if (w->allow_ffi) { |
| 748 | + out.push_back("--allow-ffi"); |
| 749 | + } |
| 750 | + if (w->allow_openssl_store) { |
| 751 | + out.push_back("--allow-openssl-store"); |
| 752 | + } |
| 753 | + if (w->allow_worker_threads) { |
| 754 | + out.push_back("--allow-worker"); |
| 755 | + } |
| 756 | + for (const std::string& path : w->allow_fs_read) { |
| 757 | + out.push_back("--allow-fs-read=" + path); |
| 758 | + } |
| 759 | + for (const std::string& path : w->allow_fs_write) { |
| 760 | + out.push_back("--allow-fs-write=" + path); |
| 761 | + } |
| 762 | + *exec_argv_out = std::move(out); |
| 763 | +} |
| 764 | + |
| 765 | + |
507 | 766 | void Worker::New(const FunctionCallbackInfo<Value>& args) { |
508 | 767 | Environment* env = Environment::GetCurrent(args); |
509 | 768 | THROW_IF_INSUFFICIENT_PERMISSIONS( |
@@ -683,6 +942,14 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) { |
683 | 942 | per_isolate_opts = env->isolate_data()->options()->Clone(); |
684 | 943 | } |
685 | 944 |
|
| 945 | + // Only explicit execArgv (including []): clamp options + rebuild argv. |
| 946 | + // Default Worker path (clone parent) is left unchanged. |
| 947 | + if (env->permission()->enabled() && per_isolate_opts && args[2]->IsArray()) { |
| 948 | + ClampWorkerPermissionToParent(env, per_isolate_opts.get()); |
| 949 | + RebuildExecArgvOutFromPermissionOptions(per_isolate_opts.get(), |
| 950 | + &exec_argv_out); |
| 951 | + } |
| 952 | + |
686 | 953 | // Internal workers should not wait for inspector frontend to connect or |
687 | 954 | // break on the first line of internal scripts. Module loader threads are |
688 | 955 | // essential to load user codes and must not be blocked by the inspector |
|
0 commit comments