Skip to content

Commit 0040e35

Browse files
committed
permission: clamp Worker grants for explicit execArgv (SEMVER-MAJOR)
When the parent runs with the Permission Model, an explicit Worker execArgv (including []) cannot drop or exceed the parent's permission-related grants. - No worker permission flags → apply parent grant ceiling - Worker permission flags set → intersect (restrict OK, escalate no) - Rebuild permission argv from clamped options; drop space-form path tokens - Default Worker (omit execArgv) unchanged Signed-off-by: yunshingng <yunshingng25@gmail.com>
1 parent 4b08879 commit 0040e35

2 files changed

Lines changed: 141 additions & 142 deletions

File tree

src/node_worker.cc

Lines changed: 124 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -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
766758
void 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

Comments
 (0)