current commit
261022.8 · permission: reject wildcard references when dropping env
#include "permission.h"
#include "env-inl.h"
#include "node.h"
#include "node_debug.h"
#include "node_diagnostics_channel.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_file.h"

#include "permission/boolean_permission.h"
#include "permission/env_permission.h"
#include "permission/fs_permission.h"
#include "permission/permission_base.h"
#include "v8-fast-api-calls.h"
#include "v8-template.h"
#include "v8.h"

#include <memory>
#include <string>

namespace node {

using v8::CFunction;
using v8::Context;
using v8::DictionaryTemplate;
using v8::FastApiCallbackOptions;
using v8::FunctionCallbackInfo;
using v8::IntegrityLevel;
using v8::Local;
using v8::MaybeLocal;
using v8::Object;
using v8::String;
using v8::Undefined;
using v8::Value;

namespace permission {

namespace {

constexpr std::string_view GetDiagnosticsChannelName(PermissionScope scope) {
  switch (scope) {
    case PermissionScope::kFileSystem:
    case PermissionScope::kFileSystemRead:
    case PermissionScope::kFileSystemWrite:
      return "node:permission-model:fs";
    case PermissionScope::kChildProcess:
      return "node:permission-model:child";
    case PermissionScope::kWorkerThreads:
      return "node:permission-model:worker";
    case PermissionScope::kNet:
      return "node:permission-model:net";
    case PermissionScope::kInspector:
      return "node:permission-model:inspector";
    case PermissionScope::kWASI:
      return "node:permission-model:wasi";
    case PermissionScope::kAddon:
      return "node:permission-model:addon";
    case PermissionScope::kFFI:
      return "node:permission-model:ffi";
    case PermissionScope::kOpenSSLStore:
      return "node:permission-model:openssl-store";
    case PermissionScope::kEnv:
      return "node:permission-model:env";
    default:
      return {};
  }
}

Local<DictionaryTemplate> GetPermissionDiagnosticsTemplate(Environment* env) {
  auto tmpl = env->permission_diagnostic_channel_message();
  if (tmpl.IsEmpty()) {
    static constexpr std::string_view names[] = {
        "permission",
        "resource",
        "drop",
    };
    tmpl = DictionaryTemplate::New(env->isolate(), names);
    env->set_permission_diagnostic_channel_message(tmpl);
  }
  return tmpl;
}

// Returns true if the permission model removed the environment variable
// named by args[0] at startup and no warning about it has been emitted yet,
// and records that one has been. For callers that emit a more specific
// warning than the one reading the variable from process.env would.
static void TakeRemovedEnvVarWarning(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args[0]->IsString());
  Utf8Value name(env->isolate(), args[0]);
  args.GetReturnValue().Set(WasRemovedByEnvironmentScrub(name.ToStringView()) &&
                            ShouldWarnAboutRemovedEnvVar(name.ToStringView()));
}

// permission.drop('fs.read', '/tmp/')
// permission.drop('child')
static void Drop(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args[0]->IsString());

  const std::string deny_scope = Utf8Value(env->isolate(), args[0]).ToString();
  PermissionScope scope = Permission::StringToPermission(deny_scope);
  if (scope == PermissionScope::kPermissionsRoot) {
    return;
  }

  // Dropping environment variables removes them from the environment. An
  // Environment that does not own the process state shares the real process
  // environment with the embedder, and must not modify it.
  if (scope == PermissionScope::kEnv &&
      env->env_vars() == per_process::system_environment &&
      !env->owns_process_state()) {
    return THROW_ERR_INVALID_STATE(
        env,
        "Environment variables can only be dropped from an Environment that "
        "owns the process state");
  }

  if (args.Length() > 1 && !args[1]->IsUndefined()) {
    // BufferValue copies raw bytes out of a Buffer/TypedArray as-is instead
    // of forcing a (potentially lossy) UTF-8 string conversion, since paths
    // are not guaranteed to be valid UTF-8.
    BufferValue resource(env->isolate(), args[1]);
    if (resource.length() > 0) {
      // A wildcard would drop more than one variable at once. Dropping the
      // whole scope, without a reference, is the only way to do that.
      if (scope == PermissionScope::kEnv &&
          resource.ToStringView().back() == '*') {
        return THROW_ERR_INVALID_ARG_VALUE(
            env,
            "Wildcards are not supported when dropping environment variables");
      }
      env->permission()->Drop(env, scope, resource.ToStringView());
      return;
    }
  }

  env->permission()->Drop(env, scope, "");
}

// permission.has('fs.in', '/tmp/')
// permission.has('fs.in')
static void Has(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args[0]->IsString());

  const std::string deny_scope = Utf8Value(env->isolate(), args[0]).ToString();
  PermissionScope scope = Permission::StringToPermission(deny_scope);
  if (scope == PermissionScope::kPermissionsRoot) {
    return args.GetReturnValue().Set(false);
  }

  if (args.Length() > 1 && !args[1]->IsUndefined()) {
    // BufferValue copies raw bytes out of a Buffer/TypedArray as-is instead
    // of forcing a (potentially lossy) UTF-8 string conversion, since paths
    // are not guaranteed to be valid UTF-8.
    BufferValue resource(env->isolate(), args[1]);
    if (resource.length() == 0) {
      args.GetReturnValue().Set(false);
      return;
    }
    return args.GetReturnValue().Set(
        env->permission()->is_granted(env, scope, resource.ToStringView()));
  }

  return args.GetReturnValue().Set(env->permission()->is_granted(env, scope));
}

static bool FastHas(Local<Value> receiver,
                    Local<Value> scope_arg,
                    // NOLINTNEXTLINE(runtime/references) This is V8 api.
                    FastApiCallbackOptions& options) {
  TRACK_V8_FAST_API_CALL("permission.has");
  auto isolate = options.isolate;
  v8::HandleScope handle_scope(isolate);
  auto context = isolate->GetCurrentContext();

  Environment* env = Environment::GetCurrent(context);

  Local<String> str;
  if (!scope_arg->ToString(context).ToLocal(&str)) {
    return false;
  }
  Utf8Value utf8_scope(isolate, str);
  PermissionScope scope =
      Permission::StringToPermission(utf8_scope.ToStringView());
  if (scope == PermissionScope::kPermissionsRoot) {
    return false;
  }

  return env->permission()->is_granted(env, scope);
}

static bool FastHasResource(
    Local<Value> receiver,
    Local<Value> scope_arg,
    Local<Value> resource_arg,
    // NOLINTNEXTLINE(runtime/references) This is V8 api.
    FastApiCallbackOptions& options) {
  TRACK_V8_FAST_API_CALL("permission.has");
  auto isolate = options.isolate;
  v8::HandleScope handle_scope(isolate);
  auto context = isolate->GetCurrentContext();

  Environment* env = Environment::GetCurrent(context);

  Local<String> str;
  if (!scope_arg->ToString(context).ToLocal(&str)) {
    return false;
  }
  Utf8Value utf8_scope(isolate, str);
  PermissionScope scope =
      Permission::StringToPermission(utf8_scope.ToStringView());
  if (scope == PermissionScope::kPermissionsRoot) {
    return false;
  }

  // The JS wrapper always calls this with 2 arguments, passing undefined
  // when no resource was given, so this path (unlike Has()) has to check
  // for that explicitly and fall back to the scope-only check.
  if (resource_arg->IsUndefined()) {
    return env->permission()->is_granted(env, scope);
  }

  // BufferValue is constructed directly from resource_arg (not from a
  // pre-converted String) so that a Buffer/TypedArray's raw bytes are
  // copied as-is instead of going through a lossy UTF-8 string conversion.
  BufferValue resource(isolate, resource_arg);
  if (resource.length() == 0) {
    return false;
  }

  return env->permission()->is_granted(env, scope, resource.ToStringView());
}

static CFunction fast_has_methods_[] = {CFunction::Make(FastHas),
                                        CFunction::Make(FastHasResource)};

}  // namespace

#define V(Name, label, _, __)                                                  \
  if (perm == PermissionScope::k##Name) return env->Name##_permission_string();
v8::Local<v8::String> Permission::PermissionToString(Environment* env,
                                                     PermissionScope perm) {
  PERMISSIONS(V)
  UNREACHABLE();
}
#undef V

#define V(Name, label, _, __)                                                  \
  if (perm == label) return PermissionScope::k##Name;
PermissionScope Permission::StringToPermission(std::string_view perm) {
  PERMISSIONS(V)
  return PermissionScope::kPermissionsRoot;
}
#undef V

Permission::Permission() : enabled_(false), warning_only_(false) {
  auto fs = std::make_shared<FSPermission>();
#define V(Name, _, __, ___)                                                    \
  nodes_[static_cast<size_t>(PermissionScope::k##Name)] = fs;
  FILESYSTEM_PERMISSIONS(V)
#undef V
#define V(Name, _, __, ___)                                                    \
  nodes_[static_cast<size_t>(PermissionScope::k##Name)] =                      \
      std::make_shared<DenyOnlyPermission>();
  CHILD_PROCESS_PERMISSIONS(V)
  WORKER_THREADS_PERMISSIONS(V)
  INSPECTOR_PERMISSIONS(V)
  WASI_PERMISSIONS(V)
  ADDON_PERMISSIONS(V)
  FFI_PERMISSIONS(V)
  OPENSSL_STORE_PERMISSIONS(V)
#undef V
#define V(Name, _, __, ___)                                                    \
  nodes_[static_cast<size_t>(PermissionScope::k##Name)] =                      \
      std::make_shared<AllowRevokePermission>();
  NET_PERMISSIONS(V)
#undef V
  env_permission_ = std::make_shared<EnvPermission>();
#define V(Name, _, __, ___)                                                    \
  nodes_[static_cast<size_t>(PermissionScope::k##Name)] = env_permission_;
  ENV_PERMISSIONS(V)
#undef V
}

const char* GetErrorFlagSuggestion(node::permission::PermissionScope perm) {
  switch (perm) {
#define V(Name, _, __, Flag)                                                   \
  case node::permission::PermissionScope::k##Name:                             \
    return Flag[0] != '\0' ? "Use " Flag " to manage permissions." : "";
    PERMISSIONS(V)
#undef V
    default:
      return "";
  }
}

MaybeLocal<Value> CreateAccessDeniedError(Environment* env,
                                          PermissionScope perm,
                                          std::string_view res) {
  const char* suggestion = GetErrorFlagSuggestion(perm);
  Local<Object> err = ERR_ACCESS_DENIED(
      env->isolate(), "Access to this API has been restricted. %s", suggestion);

  Local<Value> resource_string;
  Local<Value> perm_string = Permission::PermissionToString(env, perm);
  if (!ToV8Value(env->context(), res, env->isolate())
           .ToLocal(&resource_string) ||
      err->Set(env->context(), env->permission_string(), perm_string)
          .IsNothing() ||
      err->Set(env->context(), env->resource_string(), resource_string)
          .IsNothing()) {
    return MaybeLocal<Value>();
  }
  return err;
}

void Permission::ThrowAccessDenied(Environment* env,
                                   PermissionScope perm,
                                   std::string_view res) {
  Local<Value> err;
  if (CreateAccessDeniedError(env, perm, res).ToLocal(&err)) {
    env->isolate()->ThrowException(err);
  }
  // If ToLocal returned false, then v8 will have scheduled a
  // superseding error to be thrown.
}

void Permission::AsyncThrowAccessDenied(Environment* env,
                                        fs::FSReqBase* req_wrap,
                                        PermissionScope perm,
                                        std::string_view res) {
  Local<Value> err;
  if (CreateAccessDeniedError(env, perm, res).ToLocal(&err)) {
    return req_wrap->Reject(err);
  }
  // If ToLocal returned false, then v8 will have scheduled a
  // superseding error to be thrown.
}

void Permission::EnablePermissions() {
  if (!enabled_) {
    enabled_ = true;
  }
}

void Permission::EnableWarningOnly() {
  if (!warning_only_) {
    warning_only_ = true;
  }
}

bool Permission::is_granted_quiet(Environment* env,
                                  PermissionScope permission,
                                  std::string_view res) const {
  if (!enabled_) [[likely]] {
    return true;
  }
  CHECK(permission != PermissionScope::kPermissionsRoot &&
        permission != PermissionScope::kPermissionsCount);
  auto& perm_node = nodes_[static_cast<size_t>(permission)];
  if (!perm_node || !perm_node->is_granted(env, permission, res)) {
    return false;
  }
#if defined(__linux__)
  // /proc/<pid>/environ exposes the environment a process was started with,
  // including variables that the env scope does not grant access to. Other
  // processes' files are always denied: a child process is granted every
  // variable in the environment its parent hands it, and must not reach the
  // environments the parent could not.
  if (permission == PermissionScope::kFileSystemRead && !res.empty() &&
      IsProcEnvironReadDenied(res, env_permission_->granted_all())) {
    return false;
  }
#endif  // defined(__linux__)
  return true;
}

bool Permission::is_scope_granted(Environment* env,
                                  PermissionScope permission,
                                  std::string_view res) const {
  const bool result = is_granted_quiet(env, permission, res);
  if (!result) Publish(env, permission, res, /*dropped=*/false);
  return result;
}

void Permission::PublishDenied(Environment* env,
                               PermissionScope permission,
                               std::string_view res) const {
  Publish(env, permission, res, /*dropped=*/false);
}

void Permission::Publish(Environment* env,
                         PermissionScope scope,
                         std::string_view res,
                         bool dropped) const {
  // A subscriber's own checks must not publish recursively
  if (publishing_) return;
  auto ch = GetOrCreateChannel(env, scope);
  if (!ch || !ch->HasSubscribers()) return;
  publishing_ = true;
  v8::Isolate* isolate = env->isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = env->context();
  v8::MaybeLocal<v8::Value> values[] = {
      PermissionToString(env, scope),
      ToV8Value(context, res),
      dropped ? v8::True(isolate).As<v8::Value>()
              : Undefined(isolate).As<v8::Value>(),
  };
  ch->Publish(
      env, GetPermissionDiagnosticsTemplate(env)->NewInstance(context, values));
  publishing_ = false;
}

BaseObjectPtr<diagnostics_channel::Channel> Permission::GetOrCreateChannel(
    Environment* env, PermissionScope scope) const {
  CHECK(scope != PermissionScope::kPermissionsRoot &&
        scope != PermissionScope::kPermissionsCount);
  auto& weak_ch = channels_[static_cast<size_t>(scope)];
  // Promote weak ref to strong for the duration of this call.
  BaseObjectPtr<diagnostics_channel::Channel> ptr(weak_ch.get());
  if (ptr) return ptr;
  auto channel_name = GetDiagnosticsChannelName(scope);
  if (auto ch = diagnostics_channel::Channel::Get(env, channel_name)) {
    weak_ch = BaseObjectWeakPtr<diagnostics_channel::Channel>(ch.get());
    return ch;
  }
  return {};
}

void Permission::Apply(Environment* env,
                       std::span<const std::string> allow,
                       PermissionScope scope) {
  auto& perm_node = nodes_[static_cast<size_t>(scope)];
  if (perm_node) {
    perm_node->Apply(env, allow, scope);
  }
}

void Permission::Drop(Environment* env,
                      PermissionScope scope,
                      std::string_view param) {
  CHECK(scope != PermissionScope::kPermissionsRoot &&
        scope != PermissionScope::kPermissionsCount);
  auto& perm_node = nodes_[static_cast<size_t>(scope)];
  if (perm_node) {
    perm_node->Drop(env, scope, param);
  }

  // Publish to diagnostics channel so observers can track drops
  Publish(env, scope, param, /*dropped=*/true);
}

void Initialize(Local<Object> target,
                Local<Value> unused,
                Local<Context> context,
                void* priv) {
  SetFastMethodNoSideEffect(
      context, target, "has", Has, {fast_has_methods_, 2});
  SetMethod(context, target, "drop", Drop);
  SetMethod(
      context, target, "takeRemovedEnvVarWarning", TakeRemovedEnvVarWarning);

  target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust();
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
  registry->Register(Has);
  for (const CFunction& method : fast_has_methods_) {
    registry->Register(method);
  }
  registry->Register(Drop);
  registry->Register(TakeRemovedEnvVarWarning);
}

}  // namespace permission
}  // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(permission, node::permission::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(permission,
                                node::permission::RegisterExternalReferences)