Repository: electron/electron
Pull request: fix: ensure `TraverseParent` bails on resource path exit

Backport of #46100

See that PR for details.


Notes: none

Changed files:
filename: patches/node/.patches
patch: @@ -45,3 +45,4 @@ build_change_crdtp_protocoltypetraits_signatures_to_avoid_conflict.patch
 test_make_eval_snapshot_tests_more_flexible.patch
 build_option_to_use_custom_inspector_protocol_path.patch
 feat_add_oom_error_callback_in_node_isolatesettings.patch
+fix_ensure_traverseparent_bails_on_resource_path_exit.patch

filename: patches/node/fix_ensure_traverseparent_bails_on_resource_path_exit.patch
patch: @@ -0,0 +1,68 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shelley Vohr <shelley.vohr@gmail.com>
+Date: Tue, 18 Mar 2025 10:41:27 +0100
+Subject: fix: ensure TraverseParent bails on resource path exit
+
+Electron's security model requires that we do not traverse outside of the
+resource path. This commit ensures that the TraverseParent function
+bails out if the parent path is outside of the resource path.
+
+diff --git a/src/node_modules.cc b/src/node_modules.cc
+index 210a01d24e11764dc9fc37a77b354f11383693f8..4e9f70a1c41b44d2a1863b778d4f1e37279178d9 100644
+--- a/src/node_modules.cc
++++ b/src/node_modules.cc
+@@ -290,8 +290,41 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
+     Realm* realm, const std::filesystem::path& check_path) {
+   std::filesystem::path current_path = check_path;
+   auto env = realm->env();
++  Isolate* isolate = env->isolate();
+   const bool is_permissions_enabled = env->permission()->enabled();
+ 
++  // Get the resources path with trailing slash.
++  std::string resources_path;
++  {
++    HandleScope handle_scope(isolate);
++    Local<Value> resources_path_value;
++    Local<Object> process_object = env->process_object();
++
++    Local<String> resources_path_key =
++        String::NewFromUtf8Literal(isolate, "resourcesPath");
++    if (process_object->Get(env->context(), resources_path_key)
++            .ToLocal(&resources_path_value) &&
++        resources_path_value->IsString()) {
++      resources_path = *String::Utf8Value(isolate, resources_path_value);
++      if (!resources_path.empty() && !resources_path.ends_with(kPathSeparator)) {
++        resources_path += kPathSeparator;
++      }
++    }
++  }
++
++  auto starts_with = [](const std::string& str, const std::string& prefix) -> bool {
++    if (prefix.size() > str.size()) return false;
++    return std::equal(
++        prefix.begin(), prefix.end(), str.begin(),
++        [](char a, char b) {
++          return std::tolower(static_cast<unsigned char>(a)) ==
++                std::tolower(static_cast<unsigned char>(b));
++        });
++  };
++
++  bool did_original_path_start_with_resources_path = starts_with(check_path.
++      generic_string(), resources_path);
++
+   do {
+     current_path = current_path.parent_path();
+ 
+@@ -310,6 +343,12 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
+       return nullptr;
+     }
+ 
++    // If current path is outside the resources path, bail.
++    if (did_original_path_start_with_resources_path &&
++        !starts_with(current_path.generic_string(), resources_path)) {
++      return nullptr;
++    }
++
+     // Check if the path ends with `/node_modules`
+     if (current_path.generic_string().ends_with("/node_modules")) {
+       return nullptr;