@@ -507,20 +507,29 @@ Worker::~Worker() {
507507
508508
509509// 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- }
510+ // SEMVER-MAJOR: Permission ceiling for Worker when execArgv is explicit
511+ // (including []). Default Worker (no execArgv) is unchanged.
512+ //
513+ // After options parse, NODE_OPTIONS and repeated --allow-* are already in
514+ // EnvironmentOptions. Runtime FSPermission remains authoritative for FS checks;
515+ // path filtering here is create-time only (prefix / exact / *).
516+ //
517+ // Parent allow-list containing "*" already means unrestricted FS for that
518+ // dimension; FilterPathList early-return keeps worker paths (cannot exceed *).
519+
520+ namespace {
521+
522+ bool WorkerConfiguredPermission (const EnvironmentOptions* w) {
523+ if (w == nullptr ) return false ;
524+ if (w->permission || w->permission_audit ) return true ;
525+ if (!w->allow_fs_read .empty () || !w->allow_fs_write .empty ()) return true ;
517526 return w->allow_addons || w->allow_inspector || w->allow_child_process ||
518527 w->allow_net || w->allow_wasi || w->allow_ffi ||
519528 w->allow_openssl_store || w->allow_worker_threads ;
520529}
521530
522- static void ApplyParentPermissionCeiling (EnvironmentOptions* w,
523- const EnvironmentOptions* parent) {
531+ void ApplyParentPermissionCeiling (EnvironmentOptions* w,
532+ const EnvironmentOptions* parent) {
524533 w->permission = true ;
525534 w->permission_audit = parent->permission_audit ;
526535 w->allow_addons = parent->allow_addons ;
@@ -535,87 +544,67 @@ static void ApplyParentPermissionCeiling(EnvironmentOptions* w,
535544 w->allow_fs_write = parent->allow_fs_write ;
536545}
537546
538- static void NormalizePathForCompare (std::string* s) {
539- while (s->size () > 1 && (s->back () == ' /' || s->back () == ' \\ ' )) {
547+ void NormalizePathForCompare (std::string* s) {
548+ while (s->size () > 1 && (s->back () == ' /' || s->back () == ' \' )) {
540549 s->pop_back();
541550 }
542551#ifdef _WIN32
543552 for (char& c : *s) {
544553 if (c >= ' A' && c <= ' Z' ) {
545554 c = static_cast<char>(c - ' A' + ' a' );
546555 }
547- if (c == ' /' ) {
548- c = ' \\ ' ;
549- }
556+ if (c == ' /' ) c = ' \' ;
550557 }
551558#endif
552559}
553560
554- static std::string ResolveForCompare (Environment* env, const std::string& in) {
555- if (in.empty () || in == " *" ) {
556- return in;
557- }
561+ std::string ResolveForCompare(Environment* env, const std::string& in) {
562+ if (in.empty() || in == "*") return in;
558563 std::string resolved =
559564 PathResolve(env, std::vector<std::string_view>{std::string_view(in)});
560- if (resolved.empty ()) {
561- resolved = in;
562- }
565+ if (resolved.empty()) resolved = in;
563566 NormalizePathForCompare(&resolved);
564567 return resolved;
565568}
566569
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- }
570+ // Create-time filter only.
571+ bool PathCoveredByParentEntry(Environment* env,
572+ const std::string& parent_raw,
573+ const std::string& requested_raw) {
574+ if (parent_raw == "*") return true;
574575 const std::string parent = ResolveForCompare(env, parent_raw);
575576 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- }
577+ if (parent.empty()) return false;
578+ if (requested == parent) return true;
579+ if (requested.size() <= parent.size()) return false;
580+ if (requested.compare(0, parent.size(), parent) != 0) return false;
588581 const char next = requested[parent.size()];
589- return next == ' /' || next == ' \\ ' ;
582+ return next == ' /' || next == ' \' ;
590583}
591584
592- static bool ParentListHasWildcard (const std::vector<std::string>& parent) {
585+ bool ParentListHasWildcard(const std::vector<std::string>& parent) {
593586 for (const std::string& entry : parent) {
594- if (entry == " *" ) {
595- return true ;
596- }
587+ if (entry == "*") return true;
597588 }
598589 return false;
599590}
600591
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- }
592+ void FilterPathListToParentSubset(Environment* env,
593+ EnvironmentOptions* w,
594+ std::vector<std::string>* worker,
595+ const std::vector<std::string>& parent) {
596+ if (worker == nullptr) return;
597+
598+ // Worker enabled permission but listed no paths → keep empty (restrict).
609599 if (worker->empty()) {
610- if (w->permission || w->permission_audit ) {
611- return ;
612- }
600+ if (w->permission || w->permission_audit) return;
613601 *worker = parent;
614602 return;
615603 }
616- if (ParentListHasWildcard (parent)) {
617- return ;
618- }
604+
605+ // Parent "*" → FS already unrestricted; worker paths cannot exceed parent.
606+ if (ParentListHasWildcard(parent)) return;
607+
619608 std::vector<std::string> out;
620609 out.reserve(worker->size());
621610 bool saw_star = false;
@@ -638,11 +627,12 @@ static void FilterPathListToParentSubset(
638627 *worker = std::move(out);
639628}
640629
641- static void IntersectPermissionGrants (Environment* env,
642- EnvironmentOptions* w,
643- const EnvironmentOptions* parent) {
630+ void IntersectPermissionGrants(Environment* env,
631+ EnvironmentOptions* w,
632+ const EnvironmentOptions* parent) {
644633 w->permission = true;
645634 w->permission_audit = w->permission_audit || parent->permission_audit;
635+
646636 w->allow_addons = w->allow_addons && parent->allow_addons;
647637 w->allow_inspector = w->allow_inspector && parent->allow_inspector;
648638 w->allow_child_process =
@@ -654,32 +644,33 @@ static void IntersectPermissionGrants(Environment* env,
654644 w->allow_openssl_store && parent->allow_openssl_store;
655645 w->allow_worker_threads =
656646 w->allow_worker_threads && parent->allow_worker_threads;
647+
657648 FilterPathListToParentSubset(env, w, &w->allow_fs_read, parent->allow_fs_read);
658649 FilterPathListToParentSubset(
659650 env, w, &w->allow_fs_write, parent->allow_fs_write);
660651}
661652
662- static void ClampWorkerPermissionToParent (Environment* env,
663- PerIsolateOptions* worker_opts) {
664- if (worker_opts == nullptr || !env->permission ()->enabled ()) {
653+ void ClampWorkerPermissionToParent(Environment* env,
654+ PerIsolateOptions* worker_opts) {
655+ if (worker_opts == nullptr || env == nullptr ||
656+ !env->permission()->enabled()) {
665657 return;
666658 }
667- EnvironmentOptions* parent = env->isolate_data ()->options ()->get_per_env_options ();
659+ EnvironmentOptions* parent =
660+ env->isolate_data()->options()->get_per_env_options();
668661 EnvironmentOptions* w = worker_opts->get_per_env_options();
669- if (parent == nullptr || w == nullptr ) {
670- return ;
671- }
662+ if (parent == nullptr || w == nullptr) return;
663+
672664 if (!WorkerConfiguredPermission(w)) {
673665 ApplyParentPermissionCeiling(w, parent);
674666 } else {
675667 IntersectPermissionGrants(env, w, parent);
676668 }
677669}
678670
679- static bool IsPermissionCliToken (const std::string& a) {
680- if (a == " --permission" || a == " --permission-audit" ) {
681- return true ;
682- }
671+ // Exact flag name or flag=value (not a longer unrelated prefix).
672+ bool IsPermissionCliToken(const std::string& a) {
673+ if (a == "--permission" || a == "--permission-audit") return true;
683674 static const char* kFlags[] = {
684675 "--allow-fs-read",
685676 "--allow-fs-write",
@@ -694,74 +685,75 @@ static bool IsPermissionCliToken(const std::string& a) {
694685 };
695686 for (const char* flag : kFlags) {
696687 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- }
688+ if (a == flag) return true;
689+ if (a.size() > n && a.compare(0, n, flag) == 0 && a[n] == ' =' ) return true;
703690 }
704691 return false;
705692}
706693
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 ;
694+ bool PermissionFlagTakesNextArg(const std::string& a) {
695+ return a == "--allow-fs-read" || a == "--allow-fs-write";
696+ }
697+
698+ bool PathSafeForAllowFlag(const std::string& path) {
699+ if (path.empty()) return false;
700+ for (char c : path) {
701+ if (c == '
702+ ' || c == ' ' ) return false ;
716703 }
704+ return true;
705+ }
717706
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 ()) {
707+ void RebuildExecArgvOutFromPermissionOptions(
708+ PerIsolateOptions* worker_opts, std::vector<std::string>* exec_argv_out) {
709+ if (worker_opts == nullptr || exec_argv_out == nullptr) return;
710+ EnvironmentOptions* w = worker_opts->get_per_env_options();
711+ if (w == nullptr || !w->permission) return;
712+
713+ std::vector<std::string> kept;
714+ kept.reserve(exec_argv_out->size());
715+ for (size_t i = 0; i < exec_argv_out->size(); ++i) {
716+ const std::string& tok = (*exec_argv_out)[i];
717+ if (tok.empty()) continue;
718+ if (IsPermissionCliToken(tok)) {
719+ if (PermissionFlagTakesNextArg(tok) && i + 1 < exec_argv_out->size()) {
720+ const std::string& next = (*exec_argv_out)[i + 1];
721+ if (!next.empty() && next[0] != ' -' ) ++i;
722+ }
722723 continue;
723724 }
724- if (!IsPermissionCliToken (tok)) {
725- out.push_back (tok);
726- }
725+ kept.push_back(tok);
727726 }
727+
728+ std::vector<std::string> out;
729+ out.reserve(kept.size() + 16 + w->allow_fs_read.size() +
730+ w->allow_fs_write.size());
731+ out.emplace_back("");
732+ for (const std::string& tok : kept) out.push_back(tok);
733+
728734 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);
735+ if (w->permission_audit) out.push_back("--permission-audit");
736+ if (w->allow_addons) out.push_back("--allow-addons");
737+ if (w->allow_inspector) out.push_back("--allow-inspector");
738+ if (w->allow_child_process) out.push_back("--allow-child-process");
739+ if (w->allow_net) out.push_back("--allow-net");
740+ if (w->allow_wasi) out.push_back("--allow-wasi");
741+ if (w->allow_ffi) out.push_back("--allow-ffi");
742+ if (w->allow_openssl_store) out.push_back("--allow-openssl-store");
743+ if (w->allow_worker_threads) out.push_back("--allow-worker");
744+ for (const std::string& p : w->allow_fs_read) {
745+ if (!PathSafeForAllowFlag(p)) continue;
746+ out.push_back("--allow-fs-read=" + p);
747+ }
748+ for (const std::string& p : w->allow_fs_write) {
749+ if (!PathSafeForAllowFlag(p)) continue;
750+ out.push_back("--allow-fs-write=" + p);
761751 }
762752 *exec_argv_out = std::move(out);
763753}
764754
755+ } // namespace
756+
765757
766758void Worker::New(const FunctionCallbackInfo<Value>& args) {
767759 Environment* env = Environment::GetCurrent(args);
@@ -944,13 +936,16 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
944936
945937 // Only explicit execArgv (including []): clamp options + rebuild argv.
946938 // Default Worker path (clone parent) is left unchanged.
947- if (env->permission ()->enabled () && per_isolate_opts && args[2 ]->IsArray ()) {
939+
940+ // // Explicit execArgv only (including []). Default Worker path unchanged.
941+ if (env->permission ()->enabled() && per_isolate_opts &&
942+ args[2]->IsArray()) {
948943 ClampWorkerPermissionToParent (env, per_isolate_opts.get ());
949944 RebuildExecArgvOutFromPermissionOptions (per_isolate_opts.get (),
950945 &exec_argv_out);
951946 }
952947
953- // Internal workers should not wait for inspector frontend to connect or
948+ Internal workers should not wait for inspector frontend to connect or
954949 // break on the first line of internal scripts. Module loader threads are
955950 // essential to load user codes and must not be blocked by the inspector
956951 // for internal scripts.
0 commit comments