Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [7.0.16]

[7.0.16]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.16

### Fixed

- JS registry tables and their namespace (`public:custom_endpoints.*` by default) are now read-only to JS endpoints. Apps requiring writes can opt out with `set_js_kv_namespace_restriction(restriction, false)` (#8359).

## [7.0.15]

[7.0.15]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.15
Expand Down
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,11 @@ if(BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_client.cpp
)

add_unit_test(js_test ${CMAKE_CURRENT_SOURCE_DIR}/src/js/test/js.cpp)
add_unit_test(
js_test
${CMAKE_CURRENT_SOURCE_DIR}/src/js/test/js.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/node/uvm_endorsements.cpp
)
target_link_libraries(
js_test
PRIVATE ccf_js ccf_kv ccf_endpoints ccfcrypto http_parser
Expand Down
8 changes: 8 additions & 0 deletions include/ccf/js/interpreter_cache_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "ccf/js/tx_access.h"
#include "ccf/node_subsystem_interface.h"

#include <stdexcept>

namespace ccf::js
{
namespace core
Expand Down Expand Up @@ -47,6 +49,12 @@ namespace ccf::js
// been idle the longest when the cap is reached.
virtual void set_max_cached_interpreters(size_t max) = 0;

// Discard retained interpreters without changing the cache capacity.
virtual void clear_cached_interpreters()
{
throw std::logic_error("Interpreter cache does not support clearing");
}

virtual void set_interpreter_factory(const InterpreterFactory& ip) = 0;
};
}
33 changes: 31 additions & 2 deletions include/ccf/js/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "ccf/tx_id.h"

#include <charconv>
#include <set>
#include <string>
#define FMT_HEADER_ONLY
#include <fmt/format.h>

Expand Down Expand Up @@ -43,8 +45,14 @@ namespace ccf::js
std::shared_ptr<ccf::js::AbstractInterpreterCache> interpreter_cache =
nullptr;

// App-provided restriction, see set_js_kv_namespace_restriction
ccf::js::NamespaceRestriction namespace_restriction;

const std::string registry_managed_prefix;
bool registry_tables_protected = true;

ccf::js::NamespaceRestriction get_effective_namespace_restriction() const;

using PreExecutionHook = std::function<void(ccf::js::core::Context&)>;

void do_execute_request(
Expand All @@ -64,6 +72,12 @@ namespace ccf::js
std::string modules_quickjs_bytecode_map;
std::string runtime_options_map;

/**
* Registry-managed tables, resolved at request time. Subclasses should
* extend this set for tables outside kv_prefix + ".".
*/
virtual std::set<std::string> get_registry_managed_tables() const;

public:
BaseDynamicJSEndpointRegistry(
ccf::AbstractNodeContext& context,
Expand Down Expand Up @@ -103,10 +117,15 @@ namespace ccf::js
const std::string& module_name);

/**
* Pass a function to control which maps can be accessed by JS endpoints.
* Set the JS KV restriction. By default, registry-managed tables and the
* kv_prefix + "." namespace are also read-only.
* Pass false to apply only restriction, or ({}, false) for no namespace
* restrictions. Platform permissions still apply. Clears cached
* interpreters.
*/
void set_js_kv_namespace_restriction(
const ccf::js::NamespaceRestriction& restriction);
const ccf::js::NamespaceRestriction& restriction,
bool protect_registry_tables = true);

/**
* Set options to control JS execution. Some hard limits may be applied to
Expand Down Expand Up @@ -161,6 +180,16 @@ namespace ccf::js
std::string audit_input_map;
std::string audit_info_map;

std::set<std::string> get_registry_managed_tables() const override
{
auto tables =
BaseDynamicJSEndpointRegistry::get_registry_managed_tables();
tables.insert(recent_actions_map);
tables.insert(audit_input_map);
tables.insert(audit_info_map);
return tables;
}

public:
DynamicJSEndpointRegistry(
ccf::AbstractNodeContext& context,
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ccf"
version = "7.0.15"
version = "7.0.16"
authors = [
{ name="CCF Team", email="CCF-Sec@microsoft.com" },
]
Expand Down
6 changes: 6 additions & 0 deletions src/js/interpreter_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ namespace ccf::js
lru.set_max_size(max);
}

void clear_cached_interpreters() override
{
std::lock_guard<ccf::ds::Mutex> guard(lock);
lru.clear();
}

void set_interpreter_factory(const InterpreterFactory& ip) override
{
interpreter_factory = ip;
Expand Down
65 changes: 63 additions & 2 deletions src/js/registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ namespace ccf::js

// ccf.kv.*
auto kv_extension = std::make_shared<ccf::js::extensions::KvExtension>(
&endpoint_ctx.tx, namespace_restriction);
&endpoint_ctx.tx, get_effective_namespace_restriction());
local_extensions.emplace_back(kv_extension);

// ccf.rpc.*
Expand Down Expand Up @@ -495,6 +495,7 @@ namespace ccf::js
BaseDynamicJSEndpointRegistry::BaseDynamicJSEndpointRegistry(
ccf::AbstractNodeContext& context, const std::string& kv_prefix) :
ccf::UserEndpointRegistry(context),
registry_managed_prefix(fmt::format("{}.", kv_prefix)),
modules_map(fmt::format("{}.modules", kv_prefix)),
metadata_map(fmt::format("{}.metadata", kv_prefix)),
interpreter_flush_map(fmt::format("{}.interpreter_flush", kv_prefix)),
Expand Down Expand Up @@ -715,9 +716,69 @@ namespace ccf::js
}

void BaseDynamicJSEndpointRegistry::set_js_kv_namespace_restriction(
const ccf::js::NamespaceRestriction& restriction)
const ccf::js::NamespaceRestriction& restriction,
bool protect_registry_tables)
{
// Cached KV handles retain their permissions from creation.
interpreter_cache->clear_cached_interpreters();

namespace_restriction = restriction;
registry_tables_protected = protect_registry_tables;
}

std::set<std::string> BaseDynamicJSEndpointRegistry::
get_registry_managed_tables() const
{
return {
modules_map,
metadata_map,
interpreter_flush_map,
modules_quickjs_version_map,
modules_quickjs_bytecode_map,
runtime_options_map};
}

ccf::js::NamespaceRestriction BaseDynamicJSEndpointRegistry::
get_effective_namespace_restriction() const
{
if (!registry_tables_protected)
{
return namespace_restriction;
}

return [managed_prefix = registry_managed_prefix,
managed_tables = get_registry_managed_tables(),
app_restriction = namespace_restriction](
const std::string& map_name,
std::string& explanation) -> ccf::js::KVAccessPermissions {
auto permission = ccf::js::KVAccessPermissions::READ_WRITE;

if (
map_name.starts_with(managed_prefix) ||
managed_tables.contains(map_name))
{
explanation = fmt::format(
"The {} table is managed by the endpoint registry, so is read-only "
"in JS.",
map_name);
permission = ccf::js::KVAccessPermissions::READ_ONLY;
}

if (app_restriction != nullptr)
{
std::string app_explanation;
const auto app_permission = app_restriction(map_name, app_explanation);
const auto combined =
ccf::js::intersect_access_permissions(permission, app_permission);
if (combined != permission)
{
permission = combined;
explanation = app_explanation;
}
}

return permission;
};
}

ccf::ApiResult BaseDynamicJSEndpointRegistry::set_js_runtime_options_v1(
Expand Down
Loading