Skip to content

Commit e1558cf

Browse files
committed
permission: clamp Worker grants to parent for explicit execArgv
SEMVER-MAJOR: when the parent has the Permission Model enabled, a Worker with explicit execArgv (including []) cannot obtain wider permission-related grants than the parent. Clamp EnvironmentOptions, then rebuild exec_argv_out so CreateEnvironment enables Permission with the clamped allow lists. Signed-off-by: yunshingng <yunshingng25@gmail.com>
1 parent 76bb3f7 commit e1558cf

4 files changed

Lines changed: 413 additions & 0 deletions

File tree

‎doc/api/permissions.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ If you find a potential security vulnerability, please refer to our
2828

2929
### Permission Model
3030

31+
<!-- worker-execargv-permission-ceiling -->
32+
When the Permission Model is enabled in the parent process, creating a
33+
`worker_threads.Worker` with an explicit `execArgv` option (including an empty
34+
array) no longer allows the worker to obtain a wider permission-related grant
35+
set than the parent. Permission-related grants on the worker are limited to a
36+
subset of the parent's grants. Non-permission `execArgv` flags are unaffected.
37+
This is a breaking change relative to earlier releases where `execArgv: []`
38+
could drop the parent's Permission Model grants.
39+
40+
3141
<!-- YAML
3242
added: v20.0.0
3343
changes:

‎doc/api/worker_threads.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,13 @@ if (isMainThread) {
15611561
15621562
### `new Worker(filename[, options])`
15631563
1564+
<!-- worker-execargv-permission-ceiling -->
1565+
**Permission Model (breaking):** If the parent process runs with the
1566+
Permission Model enabled, an explicit `execArgv` (including `[]`) does not
1567+
disable or exceed the parent's permission-related grants. See the Permission
1568+
Model documentation.
1569+
1570+
15641571
<!-- YAML
15651572
added: v10.5.0
15661573
changes:

‎src/node_worker.cc‎

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
#include "node_profiling.h"
1212
#include "node_snapshot_builder.h"
1313
#include "permission/permission.h"
14+
#include "path.h"
1415
#include "util-inl.h"
1516
#include "v8-cppgc.h"
1617
#include "v8-profiler.h"
18+
#include <string>
19+
#include <string_view>
20+
#include <vector>
1721

1822
#include <memory>
1923
#include <string>
@@ -504,6 +508,220 @@ Worker::~Worker() {
504508
Debug(this, "Worker %llu destroyed", thread_id_.id);
505509
}
506510

511+
512+
// SEMVER-MAJOR: Permission-Model ceiling for Worker explicit execArgv.
513+
// Explicit execArgv (including []) must not exceed parent grants. Clamp
514+
// EnvironmentOptions, then rebuild exec_argv_out for CreateEnvironment.
515+
516+
static bool WorkerConfiguredPermission(const EnvironmentOptions* w) {
517+
if (w->permission || w->permission_audit) {
518+
return true;
519+
}
520+
if (!w->allow_fs_read.empty() || !w->allow_fs_write.empty()) {
521+
return true;
522+
}
523+
return w->allow_addons || w->allow_inspector || w->allow_child_process ||
524+
w->allow_net || w->allow_wasi || w->allow_ffi ||
525+
w->allow_openssl_store || w->allow_worker_threads;
526+
}
527+
528+
static void ApplyParentPermissionCeiling(EnvironmentOptions* w,
529+
const EnvironmentOptions* parent) {
530+
w->permission = true;
531+
w->permission_audit = parent->permission_audit;
532+
w->allow_addons = parent->allow_addons;
533+
w->allow_inspector = parent->allow_inspector;
534+
w->allow_child_process = parent->allow_child_process;
535+
w->allow_net = parent->allow_net;
536+
w->allow_wasi = parent->allow_wasi;
537+
w->allow_ffi = parent->allow_ffi;
538+
w->allow_openssl_store = parent->allow_openssl_store;
539+
w->allow_worker_threads = parent->allow_worker_threads;
540+
w->allow_fs_read = parent->allow_fs_read;
541+
w->allow_fs_write = parent->allow_fs_write;
542+
}
543+
544+
static void NormalizePathForCompare(std::string* s) {
545+
while (s->size() > 1 && (s->back() == '/' || s->back() == '\\')) {
546+
s->pop_back();
547+
}
548+
#ifdef _WIN32
549+
for (char& c : *s) {
550+
if (c >= 'A' && c <= 'Z') {
551+
c = static_cast<char>(c - 'A' + 'a');
552+
}
553+
if (c == '/') {
554+
c = '\\';
555+
}
556+
}
557+
#endif
558+
}
559+
560+
static std::string ResolveForCompare(Environment* env, const std::string& in) {
561+
if (in.empty() || in == "*") {
562+
return in;
563+
}
564+
std::string resolved =
565+
PathResolve(env, std::vector<std::string_view>{std::string_view(in)});
566+
if (resolved.empty()) {
567+
resolved = in;
568+
}
569+
NormalizePathForCompare(&resolved);
570+
return resolved;
571+
}
572+
573+
static bool PathCoveredByParentEntry(Environment* env,
574+
const std::string& parent_raw,
575+
const std::string& requested_raw) {
576+
if (parent_raw == "*") {
577+
return true;
578+
}
579+
const std::string parent = ResolveForCompare(env, parent_raw);
580+
const std::string requested = ResolveForCompare(env, requested_raw);
581+
if (parent.empty()) {
582+
return false;
583+
}
584+
if (requested == parent) {
585+
return true;
586+
}
587+
if (requested.size() <= parent.size()) {
588+
return false;
589+
}
590+
if (requested.compare(0, parent.size(), parent) != 0) {
591+
return false;
592+
}
593+
const char next = requested[parent.size()];
594+
return next == '/' || next == '\\';
595+
}
596+
597+
static bool ParentListHasWildcard(const std::vector<std::string>& parent) {
598+
for (const std::string& p : parent) {
599+
if (p == "*") {
600+
return true;
601+
}
602+
}
603+
return false;
604+
}
605+
606+
static void FilterPathListToParentSubset(
607+
Environment* env,
608+
EnvironmentOptions* w,
609+
std::vector<std::string>* worker,
610+
const std::vector<std::string>& parent) {
611+
if (worker == nullptr) {
612+
return;
613+
}
614+
if (worker->empty()) {
615+
if (w->permission || w->permission_audit) {
616+
return;
617+
}
618+
*worker = parent;
619+
return;
620+
}
621+
if (ParentListHasWildcard(parent)) {
622+
return;
623+
}
624+
std::vector<std::string> out;
625+
out.reserve(worker->size());
626+
for (const std::string& wpath : *worker) {
627+
if (wpath == "*") {
628+
continue;
629+
}
630+
for (const std::string& p : parent) {
631+
if (PathCoveredByParentEntry(env, p, wpath)) {
632+
out.push_back(wpath);
633+
break;
634+
}
635+
}
636+
}
637+
*worker = std::move(out);
638+
}
639+
640+
static void IntersectPermissionGrants(Environment* env,
641+
EnvironmentOptions* w,
642+
const EnvironmentOptions* parent) {
643+
w->permission = true;
644+
w->permission_audit = w->permission_audit || parent->permission_audit;
645+
w->allow_addons = w->allow_addons && parent->allow_addons;
646+
w->allow_inspector = w->allow_inspector && parent->allow_inspector;
647+
w->allow_child_process =
648+
w->allow_child_process && parent->allow_child_process;
649+
w->allow_net = w->allow_net && parent->allow_net;
650+
w->allow_wasi = w->allow_wasi && parent->allow_wasi;
651+
w->allow_ffi = w->allow_ffi && parent->allow_ffi;
652+
w->allow_openssl_store =
653+
w->allow_openssl_store && parent->allow_openssl_store;
654+
w->allow_worker_threads =
655+
w->allow_worker_threads && parent->allow_worker_threads;
656+
FilterPathListToParentSubset(env, w, &w->allow_fs_read, parent->allow_fs_read);
657+
FilterPathListToParentSubset(
658+
env, w, &w->allow_fs_write, parent->allow_fs_write);
659+
}
660+
661+
static void ClampWorkerPermissionToParent(Environment* env,
662+
PerIsolateOptions* worker_opts) {
663+
if (worker_opts == nullptr || !env->permission()->enabled()) {
664+
return;
665+
}
666+
EnvironmentOptions* parent =
667+
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 void RebuildExecArgvOutFromPermissionOptions(
680+
PerIsolateOptions* worker_opts, std::vector<std::string>* exec_argv_out) {
681+
if (worker_opts == nullptr || exec_argv_out == nullptr) {
682+
return;
683+
}
684+
EnvironmentOptions* w = worker_opts->get_per_env_options();
685+
if (w == nullptr || !w->permission) {
686+
return;
687+
}
688+
std::vector<std::string> out;
689+
out.emplace_back("");
690+
auto is_perm = [](const std::string& a) {
691+
return a == "--permission" || a == "--permission-audit" ||
692+
a.rfind("--allow-fs-read", 0) == 0 ||
693+
a.rfind("--allow-fs-write", 0) == 0 || a == "--allow-addons" ||
694+
a == "--allow-inspector" || a == "--allow-child-process" ||
695+
a == "--allow-net" || a == "--allow-wasi" || a == "--allow-ffi" ||
696+
a == "--allow-openssl-store" || a == "--allow-worker";
697+
};
698+
for (size_t i = 1; i < exec_argv_out->size(); i++) {
699+
if (!is_perm((*exec_argv_out)[i])) {
700+
out.push_back((*exec_argv_out)[i]);
701+
}
702+
}
703+
out.push_back("--permission");
704+
if (w->permission_audit) {
705+
out.push_back("--permission-audit");
706+
}
707+
if (w->allow_addons) out.push_back("--allow-addons");
708+
if (w->allow_inspector) out.push_back("--allow-inspector");
709+
if (w->allow_child_process) out.push_back("--allow-child-process");
710+
if (w->allow_net) out.push_back("--allow-net");
711+
if (w->allow_wasi) out.push_back("--allow-wasi");
712+
if (w->allow_ffi) out.push_back("--allow-ffi");
713+
if (w->allow_openssl_store) out.push_back("--allow-openssl-store");
714+
if (w->allow_worker_threads) out.push_back("--allow-worker");
715+
for (const std::string& path : w->allow_fs_read) {
716+
out.push_back("--allow-fs-read=" + path);
717+
}
718+
for (const std::string& path : w->allow_fs_write) {
719+
out.push_back("--allow-fs-write=" + path);
720+
}
721+
*exec_argv_out = std::move(out);
722+
}
723+
724+
507725
void Worker::New(const FunctionCallbackInfo<Value>& args) {
508726
Environment* env = Environment::GetCurrent(args);
509727
THROW_IF_INSUFFICIENT_PERMISSIONS(
@@ -683,6 +901,14 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
683901
per_isolate_opts = env->isolate_data()->options()->Clone();
684902
}
685903

904+
if (env->permission()->enabled() && per_isolate_opts) {
905+
ClampWorkerPermissionToParent(env, per_isolate_opts.get());
906+
if (args[2]->IsArray()) {
907+
RebuildExecArgvOutFromPermissionOptions(per_isolate_opts.get(),
908+
&exec_argv_out);
909+
}
910+
}
911+
686912
// Internal workers should not wait for inspector frontend to connect or
687913
// break on the first line of internal scripts. Module loader threads are
688914
// essential to load user codes and must not be blocked by the inspector

0 commit comments

Comments
 (0)