diff --git a/node.gyp b/node.gyp index 368b621b1239b0..0d9aff397cae91 100644 --- a/node.gyp +++ b/node.gyp @@ -481,8 +481,10 @@ ], 'node_sqlite_sources': [ 'src/node_sqlite.cc', + 'src/node_sqlite_vfs.cc', 'src/node_webstorage.cc', 'src/node_sqlite.h', + 'src/node_sqlite_vfs.h', 'src/node_webstorage.h', ], 'node_ffi_sources': [ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index c3b7d279f4dca0..2aad6bf3416b88 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -488,6 +488,7 @@ class BackupJob : public ThreadPoolWork { std::string source_db, std::string destination_name, std::string dest_db, + std::shared_ptr permission_vfs, int pages, Local progressFunc) : ThreadPoolWork(env, "node_sqlite3.BackupJob"), @@ -496,7 +497,8 @@ class BackupJob : public ThreadPoolWork { pages_(pages), source_db_(std::move(source_db)), destination_name_(std::move(destination_name)), - dest_db_(std::move(dest_db)) { + dest_db_(std::move(dest_db)), + permission_vfs_(std::move(permission_vfs)) { resolver_.Reset(env->isolate(), resolver); progressFunc_.Reset(env->isolate(), progressFunc); } @@ -508,7 +510,8 @@ class BackupJob : public ThreadPoolWork { destination_name_.c_str(), &dest_, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, - nullptr); + permission_vfs_ == nullptr ? nullptr + : permission_vfs_->name().c_str()); Local resolver = Local::New(env()->isolate(), resolver_); if (backup_status_ != SQLITE_OK) { @@ -609,6 +612,8 @@ class BackupJob : public ThreadPoolWork { sqlite3_close_v2(dest_); dest_ = nullptr; } + + permission_vfs_.reset(); } private: @@ -651,6 +656,7 @@ class BackupJob : public ThreadPoolWork { std::string source_db_; std::string destination_name_; std::string dest_db_; + std::shared_ptr permission_vfs_; }; UserDefinedFunction::UserDefinedFunction(Environment* env, @@ -935,12 +941,62 @@ void DatabaseSync::MemoryInfo(MemoryTracker* tracker) const { "open_config", sizeof(open_config_), "DatabaseOpenConfiguration"); } +namespace { + +bool IsSQLiteMemoryLocation(std::string_view location) { + return location == ":memory:" || + (location.starts_with("file:") && + (SQLitePathForPermission(location) == ":memory:" || + SQLiteUriParameterEquals(location, "mode", "memory"))); +} + +bool IsSQLiteReadOnlyLocation(std::string_view location, bool read_only) { + return read_only || + (location.starts_with("file:") && + (SQLiteUriParameterEquals(location, "mode", "ro") || + SQLiteUriBooleanParameter(location, "immutable"))); +} + +} // namespace + bool DatabaseSync::Open() { if (IsOpen()) { THROW_ERR_INVALID_STATE(env(), "database is already open"); return false; } + // Permission checks: in-memory databases do not access the filesystem. + // SQLite URI modes are handled before the VFS is created so that the + // user-facing error remains Node's permission error. + std::string_view db_location = open_config_.location(); + if (!IsSQLiteMemoryLocation(db_location)) { + const std::string db_path = SQLitePathForPermission(db_location); + const bool read_only = + IsSQLiteReadOnlyLocation(db_location, open_config_.get_read_only()); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env(), + permission::PermissionScope::kFileSystemRead, + db_path, + false); + if (!read_only) { + THROW_IF_INSUFFICIENT_PERMISSIONS( + env(), + permission::PermissionScope::kFileSystemWrite, + db_path, + false); + } + } + + if (env()->permission()->enabled()) { + permission_vfs_ = std::make_shared(env()); + if (!permission_vfs_->Register()) { + THROW_ERR_SQLITE_ERROR(env()->isolate(), + "Unable to register the SQLite permission VFS"); + permission_vfs_.reset(); + return false; + } + } + // sqlite3_open_v2() assigns a database handle even when it fails. Such a // handle is in a "sick" state and may only be used to retrieve the error // and must then be released with sqlite3_close(). Close and reset the @@ -959,12 +1015,19 @@ bool DatabaseSync::Open() { int flags = open_config_.get_read_only() ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; - int r = sqlite3_open_v2(open_config_.location().c_str(), - &connection_, - flags | default_flags, - nullptr); + int r = sqlite3_open_v2( + open_config_.location().c_str(), + &connection_, + flags | default_flags, + permission_vfs_ == nullptr ? nullptr : permission_vfs_->name().c_str()); CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); + if (permission_vfs_ != nullptr) { + r = sqlite3_set_authorizer( + connection_, DatabaseSync::AuthorizerCallback, this); + CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); + } + r = sqlite3_db_config(connection_, SQLITE_DBCONFIG_DQS_DML, static_cast(open_config_.get_enable_dqs()), @@ -1088,10 +1151,21 @@ std::optional ValidateDatabasePath(Environment* env, constexpr auto has_null_bytes = [](std::string_view str) { return str.find('\0') != std::string_view::npos; }; + const auto validate_uri_vfs = [&](std::string value) + -> std::optional { + if (env->permission()->enabled() && HasSQLiteVfsUriParameter(value)) { + THROW_ERR_INVALID_ARG_VALUE( + env->isolate(), + "The \"%s\" argument must not specify a SQLite VFS.", + field_name); + return std::nullopt; + } + return value; + }; if (path->IsString()) { Utf8Value location(env->isolate(), path.As()); if (!has_null_bytes(location.ToStringView())) { - return location.ToString(); + return validate_uri_vfs(location.ToString()); } } else if (path->IsUint8Array()) { Local buffer = path.As(); @@ -1100,7 +1174,8 @@ std::optional ValidateDatabasePath(Environment* env, auto data = static_cast(buffer->Buffer()->Data()) + byteOffset; if (std::find(data, data + byteLength, 0) == data + byteLength) { - return std::string(reinterpret_cast(data), byteLength); + return validate_uri_vfs( + std::string(reinterpret_cast(data), byteLength)); } } else if (path->IsObject()) { // When is URL auto url = path.As(); @@ -1116,7 +1191,7 @@ std::optional ValidateDatabasePath(Environment* env, return std::nullopt; } - return location_value.ToString(); + return validate_uri_vfs(location_value.ToString()); } } } @@ -1439,6 +1514,7 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { int r = sqlite3_close_v2(db->connection_); CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); db->connection_ = nullptr; + db->permission_vfs_.reset(); } void DatabaseSync::Dispose(const v8::FunctionCallbackInfo& args) { @@ -2229,6 +2305,7 @@ void Backup(const FunctionCallbackInfo& args) { std::move(source_db), dest_path.value(), std::move(dest_db), + db->PermissionVFS(), rate, progressFunc); db->AddBackup(job); @@ -2464,9 +2541,13 @@ void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo& args) { Isolate* isolate = env->isolate(); if (args[0]->IsNull()) { - // Clear the authorizer - sqlite3_set_authorizer(db->connection_, nullptr, nullptr); db->object()->SetInternalField(kAuthorizerCallback, Null(isolate)); + if (db->permission_vfs_ == nullptr) { + sqlite3_set_authorizer(db->connection_, nullptr, nullptr); + } else { + sqlite3_set_authorizer( + db->connection_, DatabaseSync::AuthorizerCallback, db); + } return; } @@ -2495,16 +2576,19 @@ int DatabaseSync::AuthorizerCallback(void* user_data, const char* param3, const char* param4) { DatabaseSync* db = static_cast(user_data); - Environment* env = db->env(); - Isolate* isolate = env->isolate(); - HandleScope handle_scope(isolate); - Local context = env->context(); + if (db->permission_vfs_ != nullptr && action_code == SQLITE_ATTACH && + param1 != nullptr && HasSQLiteVfsUriParameter(param1)) { + return SQLITE_DENY; + } Local cb = db->object()->GetInternalField(kAuthorizerCallback).template As(); + if (!cb->IsFunction()) return SQLITE_OK; - CHECK(cb->IsFunction()); - + Environment* env = db->env(); + Isolate* isolate = env->isolate(); + HandleScope handle_scope(isolate); + Local context = env->context(); Local callback = cb.As(); LocalVector js_argv( diff --git a/src/node_sqlite.h b/src/node_sqlite.h index e7281ed266af5d..4141cc96bbb12c 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -6,12 +6,14 @@ #include "base_object.h" #include "lru_cache-inl.h" #include "node_mem.h" +#include "node_sqlite_vfs.h" #include "sqlite3.h" #include "util.h" #include #include #include +#include #include #include #include @@ -220,6 +222,9 @@ class DatabaseSync : public BaseObject { return open_config_.get_allow_unknown_named_params(); } sqlite3* Connection(); + std::shared_ptr PermissionVFS() const { + return permission_vfs_; + } // In some situations, such as when using custom functions, it is possible // that SQLite reports an error while JavaScript already has a pending @@ -241,6 +246,7 @@ class DatabaseSync : public BaseObject { bool enable_load_extension_; sqlite3* connection_; bool ignore_next_sqlite_error_; + std::shared_ptr permission_vfs_; std::set backups_; std::set sessions_; diff --git a/src/node_sqlite_vfs.cc b/src/node_sqlite_vfs.cc new file mode 100644 index 00000000000000..489023f5067897 --- /dev/null +++ b/src/node_sqlite_vfs.cc @@ -0,0 +1,860 @@ +#include "node_sqlite_vfs.h" + +#include "env-inl.h" +#include "permission/permission.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace node { +namespace sqlite { + +namespace { + +std::atomic next_vfs_id{0}; + +int HexValue(const char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +void DecodeUriComponent(std::string_view component, std::string* decoded) { + decoded->clear(); + decoded->reserve(component.size()); + for (size_t i = 0; i < component.size(); ++i) { + if (component[i] == '%' && i + 2 < component.size()) { + const int high = HexValue(component[i + 1]); + const int low = HexValue(component[i + 2]); + if (high >= 0 && low >= 0) { + const char value = static_cast((high << 4) | low); + if (value == '\0') break; + decoded->push_back(value); + i += 2; + continue; + } + } + decoded->push_back(component[i]); + } +} + +bool GetSQLiteUriParameter(std::string_view path, + std::string_view parameter, + bool use_last, + std::string* value) { + if (!path.starts_with("file:")) return false; + + const size_t query_start = path.find('?', 5); + const size_t fragment_start = path.find('#', 5); + if (query_start == std::string_view::npos || + (fragment_start != std::string_view::npos && + fragment_start < query_start)) { + return false; + } + + const size_t query_end = fragment_start == std::string_view::npos + ? path.size() + : fragment_start; + size_t parameter_start = query_start + 1; + std::string decoded_key; + std::string decoded_value; + bool found = false; + while (parameter_start <= query_end) { + size_t parameter_end = path.find('&', parameter_start); + if (parameter_end == std::string_view::npos || parameter_end > query_end) { + parameter_end = query_end; + } + + const size_t equals = path.find('=', parameter_start); + const size_t key_end = equals == std::string_view::npos || + equals > parameter_end + ? parameter_end + : equals; + DecodeUriComponent(path.substr(parameter_start, key_end - parameter_start), + &decoded_key); + if (decoded_key == parameter) { + if (equals == std::string_view::npos || equals > parameter_end) { + value->clear(); + } else { + DecodeUriComponent(path.substr(equals + 1, parameter_end - equals - 1), + &decoded_value); + *value = decoded_value; + } + found = true; + if (!use_last) return true; + } + + if (parameter_end == query_end) break; + parameter_start = parameter_end + 1; + } + return found; +} + +bool EqualsIgnoreCase(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + for (size_t i = 0; i < left.size(); ++i) { + if (std::tolower(static_cast(left[i])) != + std::tolower(static_cast(right[i]))) { + return false; + } + } + return true; +} + +bool IsSidecarPath(std::string_view path, std::string* database_path) { + constexpr std::string_view suffixes[] = {"-journal", "-wal", "-shm"}; + for (const std::string_view suffix : suffixes) { + if (path.size() > suffix.size() && path.ends_with(suffix)) { + *database_path = path.substr(0, path.size() - suffix.size()); + return true; + } + } + + // SQLite names super-journals as -mj. A + // permission granted for the database also covers its super-journal. + const size_t marker = path.rfind("-mj"); + if (marker != std::string_view::npos && marker > 0 && + path.size() > marker + 3) { + *database_path = path.substr(0, marker); + return true; + } + + return false; +} + +sqlite3_filename CreateReadOnlyShmFilename(sqlite3_filename name) { + std::vector parameters = {"readonly_shm", "1"}; + for (int index = 0;; ++index) { + const char* key = sqlite3_uri_key(name, index); + if (key == nullptr) break; + if (strcmp(key, "readonly_shm") == 0) continue; + + const char* value = sqlite3_uri_parameter(name, key); + parameters.push_back(key); + parameters.push_back(value == nullptr ? "" : value); + } + + return sqlite3_create_filename(sqlite3_filename_database(name), + sqlite3_filename_journal(name), + sqlite3_filename_wal(name), + static_cast(parameters.size() / 2), + parameters.data()); +} + +std::string CanonicalPath(SQLitePermissionVFS* vfs, const char* path) { + if (path == nullptr) return {}; + + // SQLite normally passes a full pathname to xOpen, but xAccess and xDelete + // may receive names produced by SQLite itself. Canonicalize those names + // before consulting Node's path permission tree. + std::string result(static_cast(vfs->parent()->mxPathname) + 1, '\0'); + if (vfs->parent()->xFullPathname(vfs->parent(), + path, + static_cast(result.size()), + result.data()) != SQLITE_OK) { + return {}; + } + result.resize(strlen(result.c_str())); + return result; +} + +struct PermissionFile { + sqlite3_file base; + SQLitePermissionVFS* vfs; + sqlite3_filename parent_name; + bool can_read; + bool can_write; + bool can_write_sidecar; + bool read_only_shm; + sqlite3_io_methods methods; +}; + +sqlite3_file* RealFile(PermissionFile* file) { + return reinterpret_cast( + reinterpret_cast(file) + sizeof(PermissionFile)); +} + +PermissionFile* PermissionFileFromBase(sqlite3_file* file) { + return reinterpret_cast(file); +} + +int PermissionClose(sqlite3_file* file); +int PermissionRead(sqlite3_file*, void*, int, sqlite3_int64); +int PermissionWrite(sqlite3_file*, const void*, int, sqlite3_int64); +int PermissionTruncate(sqlite3_file*, sqlite3_int64); +int PermissionSync(sqlite3_file*, int); +int PermissionFileSize(sqlite3_file*, sqlite3_int64*); +int PermissionLock(sqlite3_file*, int); +int PermissionUnlock(sqlite3_file*, int); +int PermissionCheckReservedLock(sqlite3_file*, int*); +int PermissionFileControl(sqlite3_file*, int, void*); +int PermissionSectorSize(sqlite3_file*); +int PermissionDeviceCharacteristics(sqlite3_file*); +int PermissionShmMap(sqlite3_file*, int, int, int, void volatile**); +int PermissionShmLock(sqlite3_file*, int, int, int); +void PermissionShmBarrier(sqlite3_file*); +int PermissionShmUnmap(sqlite3_file*, int); +int PermissionFetch(sqlite3_file*, sqlite3_int64, int, void**); +int PermissionUnfetch(sqlite3_file*, sqlite3_int64, void*); + +const sqlite3_io_methods kPermissionIoMethods = { + 3, + PermissionClose, + PermissionRead, + PermissionWrite, + PermissionTruncate, + PermissionSync, + PermissionFileSize, + PermissionLock, + PermissionUnlock, + PermissionCheckReservedLock, + PermissionFileControl, + PermissionSectorSize, + PermissionDeviceCharacteristics, + PermissionShmMap, + PermissionShmLock, + PermissionShmBarrier, + PermissionShmUnmap, + PermissionFetch, + PermissionUnfetch, +}; + +bool HasReadPermission(const PermissionFile* file) { + return file->can_read; +} + +bool HasWritePermission(const PermissionFile* file) { + return file->can_write; +} + +int PermissionClose(sqlite3_file* file) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + const int result = real->pMethods == nullptr + ? SQLITE_OK + : real->pMethods->xClose(real); + sqlite3_free_filename(permission_file->parent_name); + permission_file->parent_name = nullptr; + file->pMethods = nullptr; + return result; +} + +int PermissionRead(sqlite3_file* file, + void* buffer, + int amount, + sqlite3_int64 offset) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasReadPermission(permission_file)) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xRead(real, buffer, amount, offset); +} + +int PermissionWrite(sqlite3_file* file, + const void* buffer, + int amount, + sqlite3_int64 offset) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasWritePermission(permission_file)) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xWrite(real, buffer, amount, offset); +} + +int PermissionTruncate(sqlite3_file* file, sqlite3_int64 size) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasWritePermission(permission_file)) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xTruncate(real, size); +} + +int PermissionSync(sqlite3_file* file, int flags) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasWritePermission(permission_file)) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xSync(real, flags); +} + +int PermissionFileSize(sqlite3_file* file, sqlite3_int64* size) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xFileSize(real, size); +} + +int PermissionLock(sqlite3_file* file, int lock) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xLock(real, lock); +} + +int PermissionUnlock(sqlite3_file* file, int lock) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xUnlock(real, lock); +} + +int PermissionCheckReservedLock(sqlite3_file* file, int* result) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xCheckReservedLock(real, result); +} + +bool FileControlRequiresWrite(int operation) { + switch (operation) { + case SQLITE_FCNTL_SIZE_HINT: + case SQLITE_FCNTL_OVERWRITE: + case SQLITE_FCNTL_SYNC: + case SQLITE_FCNTL_COMMIT_PHASETWO: + case SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: + case SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: + case SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: + return true; + default: + return false; + } +} + +int PermissionFileControl(sqlite3_file* file, int operation, void* argument) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (FileControlRequiresWrite(operation) && + !HasWritePermission(permission_file)) { + return SQLITE_PERM; + } + + if (operation == SQLITE_FCNTL_SET_LOCKPROXYFILE && argument != nullptr) { + const std::string_view proxy_path = static_cast(argument); + const bool allowed = + proxy_path == ":auto:" + ? permission_file->vfs->AllowsTemporaryFile(true, true) + : permission_file->vfs->AllowsPath(proxy_path, true, true); + if (!allowed) return SQLITE_PERM; + } + + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xFileControl(real, operation, argument); +} + +int PermissionSectorSize(sqlite3_file* file) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xSectorSize(real); +} + +int PermissionDeviceCharacteristics(sqlite3_file* file) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + return real->pMethods->xDeviceCharacteristics(real); +} + +int PermissionShmMap(sqlite3_file* file, + int page, + int page_size, + int extend, + void volatile** result) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasReadPermission(permission_file)) return SQLITE_PERM; + + // OS VFS implementations can open or create the SHM file read-write even + // when extend is false. A read-only clone of the SQLite filename is used + // when the caller has no permission to write sidecars. + if (!permission_file->can_write_sidecar && + !permission_file->read_only_shm) { + return SQLITE_PERM; + } + + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xShmMap == nullptr) return SQLITE_NOTFOUND; + return real->pMethods->xShmMap(real, page, page_size, extend, result); +} + +int PermissionShmLock(sqlite3_file* file, int offset, int amount, int flags) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xShmLock == nullptr) return SQLITE_NOTFOUND; + return real->pMethods->xShmLock(real, offset, amount, flags); +} + +void PermissionShmBarrier(sqlite3_file* file) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xShmBarrier != nullptr) { + real->pMethods->xShmBarrier(real); + } +} + +int PermissionShmUnmap(sqlite3_file* file, int delete_flag) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (delete_flag && !permission_file->can_write_sidecar) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xShmUnmap == nullptr) return SQLITE_NOTFOUND; + return real->pMethods->xShmUnmap(real, delete_flag); +} + +int PermissionFetch(sqlite3_file* file, + sqlite3_int64 offset, + int amount, + void** result) { + PermissionFile* permission_file = PermissionFileFromBase(file); + if (!HasReadPermission(permission_file)) return SQLITE_PERM; + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xFetch == nullptr) { + *result = nullptr; + return SQLITE_OK; + } + return real->pMethods->xFetch(real, offset, amount, result); +} + +int PermissionUnfetch(sqlite3_file* file, sqlite3_int64 offset, void* page) { + PermissionFile* permission_file = PermissionFileFromBase(file); + sqlite3_file* real = RealFile(permission_file); + if (real->pMethods->xUnfetch == nullptr) return SQLITE_OK; + return real->pMethods->xUnfetch(real, offset, page); +} + +int PermissionVfsOpen(sqlite3_vfs* vfs, + sqlite3_filename name, + sqlite3_file* file, + int flags, + int* out_flags) { + return static_cast(vfs->pAppData)->Open( + name, file, flags, out_flags); +} + +int PermissionVfsDelete(sqlite3_vfs* vfs, const char* name, int sync_dir) { + return static_cast(vfs->pAppData)->Delete(name, + sync_dir); +} + +int PermissionVfsAccess(sqlite3_vfs* vfs, + const char* name, + int flags, + int* result) { + return static_cast(vfs->pAppData)->Access(name, + flags, + result); +} + +int PermissionVfsFullPathname(sqlite3_vfs* vfs, + const char* name, + int length, + char* output) { + return static_cast(vfs->pAppData)->FullPathname( + name, length, output); +} + +void* PermissionVfsDlOpen(sqlite3_vfs* vfs, const char* name) { + return static_cast(vfs->pAppData)->DlOpen(name); +} + +void PermissionVfsDlError(sqlite3_vfs* vfs, int length, char* message) { + static_cast(vfs->pAppData)->DlError(length, message); +} + +SQLiteDlSym PermissionVfsDlSym(sqlite3_vfs* vfs, + void* handle, + const char* symbol) { + return static_cast(vfs->pAppData)->DlSym(handle, + symbol); +} + +void PermissionVfsDlClose(sqlite3_vfs* vfs, void* handle) { + static_cast(vfs->pAppData)->DlClose(handle); +} + +int PermissionVfsRandomness(sqlite3_vfs* vfs, int length, char* output) { + return static_cast(vfs->pAppData)->Randomness(length, + output); +} + +int PermissionVfsSleep(sqlite3_vfs* vfs, int microseconds) { + return static_cast(vfs->pAppData)->Sleep(microseconds); +} + +int PermissionVfsCurrentTime(sqlite3_vfs* vfs, double* time) { + return static_cast(vfs->pAppData)->CurrentTime(time); +} + +int PermissionVfsGetLastError(sqlite3_vfs* vfs, int length, char* message) { + auto* permission_vfs = + static_cast(vfs->pAppData); + return permission_vfs->GetLastError(length, message); +} + +int PermissionVfsCurrentTimeInt64(sqlite3_vfs* vfs, sqlite3_int64* time) { + return static_cast(vfs->pAppData)->CurrentTimeInt64( + time); +} + +int PermissionVfsSetSystemCall(sqlite3_vfs* vfs, + const char* name, + sqlite3_syscall_ptr callback) { + return static_cast(vfs->pAppData)->SetSystemCall( + name, callback); +} + +sqlite3_syscall_ptr PermissionVfsGetSystemCall(sqlite3_vfs* vfs, + const char* name) { + return static_cast(vfs->pAppData)->GetSystemCall(name); +} + +const char* PermissionVfsNextSystemCall(sqlite3_vfs* vfs, const char* name) { + return static_cast(vfs->pAppData)->NextSystemCall( + name); +} + +} // namespace + +bool HasSQLiteVfsUriParameter(std::string_view path) { + std::string value; + return GetSQLiteUriParameter(path, "vfs", false, &value); +} + +bool SQLiteUriParameterEquals(std::string_view path, + std::string_view parameter, + std::string_view value) { + std::string actual; + return GetSQLiteUriParameter(path, parameter, true, &actual) && + actual == value; +} + +bool SQLiteUriBooleanParameter(std::string_view path, + std::string_view parameter) { + std::string value; + if (!GetSQLiteUriParameter(path, parameter, false, &value)) return false; + + if (!value.empty() && std::isdigit(static_cast(value[0]))) { + return std::strtoll(value.c_str(), nullptr, 10) != 0; + } + return EqualsIgnoreCase(value, "on") || EqualsIgnoreCase(value, "yes") || + EqualsIgnoreCase(value, "true"); +} + +std::string SQLitePathForPermission(std::string_view path) { + if (!path.starts_with("file:")) return std::string(path); + + const size_t path_end = path.find_first_of("?#", 5); + std::string decoded; + DecodeUriComponent( + path.substr(5, + path_end == std::string_view::npos + ? std::string_view::npos + : path_end - 5), + &decoded); + + if (decoded.starts_with("//")) { + const size_t pathname_start = decoded.find('/', 2); + const std::string_view authority = + pathname_start == std::string::npos + ? std::string_view(decoded).substr(2) + : std::string_view(decoded).substr(2, pathname_start - 2); + if (authority.empty() || authority == "localhost") { + decoded = pathname_start == std::string::npos + ? std::string() + : decoded.substr(pathname_start); + } + } + +#ifdef _WIN32 + if (decoded.size() >= 3 && decoded[0] == '/' && + std::isalpha(static_cast(decoded[1])) && + decoded[2] == ':') { + decoded.erase(0, 1); + } +#endif + + return decoded; +} + +SQLitePermissionVFS::SQLitePermissionVFS(Environment* env) + : parent_(sqlite3_vfs_find(nullptr)), + memory_vfs_(sqlite3_vfs_find("memdb")), + permission_(env->permission()), + name_("node-permission-" + std::to_string(++next_vfs_id)) { + CHECK_NOT_NULL(parent_); + CHECK_NOT_NULL(memory_vfs_); + + vfs_.iVersion = parent_->iVersion; + vfs_.szOsFile = sizeof(PermissionFile) + + std::max(parent_->szOsFile, memory_vfs_->szOsFile); + vfs_.mxPathname = parent_->mxPathname; + vfs_.zName = name_.c_str(); + vfs_.pAppData = this; + vfs_.xOpen = PermissionVfsOpen; + vfs_.xDelete = PermissionVfsDelete; + vfs_.xAccess = PermissionVfsAccess; + vfs_.xFullPathname = PermissionVfsFullPathname; + vfs_.xDlOpen = PermissionVfsDlOpen; + vfs_.xDlError = PermissionVfsDlError; + vfs_.xDlSym = PermissionVfsDlSym; + vfs_.xDlClose = PermissionVfsDlClose; + vfs_.xRandomness = PermissionVfsRandomness; + vfs_.xSleep = PermissionVfsSleep; + vfs_.xCurrentTime = PermissionVfsCurrentTime; + vfs_.xGetLastError = PermissionVfsGetLastError; + vfs_.xCurrentTimeInt64 = parent_->xCurrentTimeInt64 == nullptr + ? nullptr + : PermissionVfsCurrentTimeInt64; + vfs_.xSetSystemCall = parent_->xSetSystemCall == nullptr + ? nullptr + : PermissionVfsSetSystemCall; + vfs_.xGetSystemCall = parent_->xGetSystemCall == nullptr + ? nullptr + : PermissionVfsGetSystemCall; + vfs_.xNextSystemCall = parent_->xNextSystemCall == nullptr + ? nullptr + : PermissionVfsNextSystemCall; +} + +SQLitePermissionVFS::~SQLitePermissionVFS() { + Unregister(); +} + +bool SQLitePermissionVFS::Register() { + if (registered_) return true; + registered_ = sqlite3_vfs_register(&vfs_, 0) == SQLITE_OK; + return registered_; +} + +void SQLitePermissionVFS::Unregister() { + if (!registered_) return; + sqlite3_vfs_unregister(&vfs_); + registered_ = false; +} + +bool SQLitePermissionVFS::AllowsPath(std::string_view path, + bool read, + bool write) const { + if (permission_->warning_only()) return true; + + const std::string canonical = CanonicalPath( + const_cast(this), std::string(path).c_str()); + if (canonical.empty()) return false; + + const auto allowed = [&](std::string_view candidate) { + return (!read || permission_->is_granted_no_side_effects( + permission::PermissionScope::kFileSystemRead, + candidate)) && + (!write || permission_->is_granted_no_side_effects( + permission::PermissionScope::kFileSystemWrite, + candidate)); + }; + + return allowed(canonical); +} + +bool SQLitePermissionVFS::AllowsSidecarPath(std::string_view path, + bool read, + bool write) const { + if (AllowsPath(path, read, write)) return true; + + const std::string canonical = CanonicalPath( + const_cast(this), std::string(path).c_str()); + if (canonical.empty()) return false; + + std::string database_path; + return IsSidecarPath(canonical, &database_path) && + AllowsPath(database_path, read, write); +} + +bool SQLitePermissionVFS::AllowsTemporaryFile(bool read, bool write) const { + if (permission_->warning_only()) return true; + + // SQLite does not provide a pathname for anonymous temporary files. Disk + // storage is allowed only with global permissions. Otherwise xOpen uses + // SQLite's memory VFS. + return (!read || permission_->is_granted_no_side_effects( + permission::PermissionScope::kFileSystemRead, "")) && + (!write || permission_->is_granted_no_side_effects( + permission::PermissionScope::kFileSystemWrite, "")); +} + +int SQLitePermissionVFS::Open(sqlite3_filename name, + sqlite3_file* file, + int flags, + int* out_flags) { + auto* permission_file = reinterpret_cast(file); + memset(permission_file, 0, vfs_.szOsFile); + permission_file->vfs = this; + + const bool read = + (flags & (SQLITE_OPEN_READONLY | SQLITE_OPEN_READWRITE)) != 0; + const bool immutable = name != nullptr && (flags & SQLITE_OPEN_MAIN_DB) && + sqlite3_uri_boolean(name, "immutable", 0) != 0; + bool write = (flags & SQLITE_OPEN_READWRITE) != 0 && !immutable; + int parent_flags = flags; + sqlite3_vfs* selected_vfs = parent_; + bool can_write_sidecar = write; + bool read_only_shm = false; + bool allowed; + + if (flags & SQLITE_OPEN_MEMORY) { + selected_vfs = memory_vfs_; + allowed = true; + } else if (name == nullptr) { + allowed = AllowsTemporaryFile(read, write); + if (!allowed) { + selected_vfs = memory_vfs_; + allowed = true; + } + } else { + const char* permission_name = name; + const bool is_journal = + flags & (SQLITE_OPEN_MAIN_JOURNAL | SQLITE_OPEN_WAL); + if (is_journal) { + permission_name = sqlite3_filename_database(name); + sqlite3_file* database_file = sqlite3_database_file_object(name); + if (database_file != nullptr && + !PermissionFileFromBase(database_file)->can_write_sidecar) { + write = false; + parent_flags &= ~(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + parent_flags |= SQLITE_OPEN_READONLY; + } + } + + allowed = permission_name != nullptr && + !HasSQLiteVfsUriParameter(permission_name) && + ((flags & SQLITE_OPEN_SUPER_JOURNAL) + ? AllowsSidecarPath(permission_name, read, write) + : AllowsPath(permission_name, read, write)); + if (allowed && permission_name != nullptr && + (flags & SQLITE_OPEN_MAIN_DB)) { + can_write_sidecar = AllowsPath(permission_name, false, true); + if (!can_write_sidecar) { + permission_file->parent_name = CreateReadOnlyShmFilename(name); + if (permission_file->parent_name == nullptr) return SQLITE_NOMEM; + read_only_shm = true; + } + } + } + + if (!allowed) return SQLITE_PERM; + + sqlite3_filename parent_name = permission_file->parent_name == nullptr + ? name + : permission_file->parent_name; + sqlite3_file* real = RealFile(permission_file); + const int result = selected_vfs->xOpen( + selected_vfs, parent_name, real, parent_flags, out_flags); + if (real->pMethods == nullptr) { + sqlite3_free_filename(permission_file->parent_name); + permission_file->parent_name = nullptr; + file->pMethods = nullptr; + return result; + } + + permission_file->can_read = read; + permission_file->can_write = write && + (out_flags == nullptr || + (*out_flags & SQLITE_OPEN_READONLY) == 0); + permission_file->can_write_sidecar = can_write_sidecar; + permission_file->read_only_shm = read_only_shm; + permission_file->methods = kPermissionIoMethods; + permission_file->methods.iVersion = std::min( + permission_file->methods.iVersion, real->pMethods->iVersion); + if (permission_file->methods.iVersion < 2) { + permission_file->methods.xShmMap = nullptr; + permission_file->methods.xShmLock = nullptr; + permission_file->methods.xShmBarrier = nullptr; + permission_file->methods.xShmUnmap = nullptr; + } + if (permission_file->methods.iVersion < 3) { + permission_file->methods.xFetch = nullptr; + permission_file->methods.xUnfetch = nullptr; + } + file->pMethods = &permission_file->methods; + return result; +} + +int SQLitePermissionVFS::Delete(const char* name, int sync_dir) { + if (name == nullptr || !AllowsSidecarPath(name, false, true)) { + return SQLITE_PERM; + } + return parent_->xDelete(parent_, name, sync_dir); +} + +int SQLitePermissionVFS::Access(const char* name, int flags, int* result) { + if (name == nullptr) { + *result = 0; + return SQLITE_OK; + } + + const bool read = true; + const bool write = flags == SQLITE_ACCESS_READWRITE; + if (!AllowsSidecarPath(name, read, write)) { + *result = 0; + return SQLITE_OK; + } + return parent_->xAccess(parent_, name, flags, result); +} + +int SQLitePermissionVFS::FullPathname(const char* name, + int length, + char* output) { + return parent_->xFullPathname(parent_, name, length, output); +} + +void* SQLitePermissionVFS::DlOpen(const char* name) { + if (!permission_->warning_only()) return nullptr; + return parent_->xDlOpen == nullptr + ? nullptr + : parent_->xDlOpen(parent_, name); +} + +void SQLitePermissionVFS::DlError(int length, char* message) { + if (parent_->xDlError != nullptr) { + parent_->xDlError(parent_, length, message); + } +} + +SQLiteDlSym SQLitePermissionVFS::DlSym(void* handle, const char* symbol) { + return parent_->xDlSym == nullptr + ? nullptr + : parent_->xDlSym(parent_, handle, symbol); +} + +void SQLitePermissionVFS::DlClose(void* handle) { + if (parent_->xDlClose != nullptr) parent_->xDlClose(parent_, handle); +} + +int SQLitePermissionVFS::Randomness(int length, char* output) { + return parent_->xRandomness(parent_, length, output); +} + +int SQLitePermissionVFS::Sleep(int microseconds) { + return parent_->xSleep(parent_, microseconds); +} + +int SQLitePermissionVFS::CurrentTime(double* time) { + return parent_->xCurrentTime(parent_, time); +} + +int SQLitePermissionVFS::GetLastError(int length, char* message) { + return parent_->xGetLastError == nullptr + ? SQLITE_OK + : parent_->xGetLastError(parent_, length, message); +} + +int SQLitePermissionVFS::CurrentTimeInt64(sqlite3_int64* time) { + return parent_->xCurrentTimeInt64(parent_, time); +} + +int SQLitePermissionVFS::SetSystemCall(const char* name, + sqlite3_syscall_ptr callback) { + return parent_->xSetSystemCall(parent_, name, callback); +} + +sqlite3_syscall_ptr SQLitePermissionVFS::GetSystemCall(const char* name) { + return parent_->xGetSystemCall(parent_, name); +} + +const char* SQLitePermissionVFS::NextSystemCall(const char* name) { + return parent_->xNextSystemCall(parent_, name); +} + +} // namespace sqlite +} // namespace node diff --git a/src/node_sqlite_vfs.h b/src/node_sqlite_vfs.h new file mode 100644 index 00000000000000..559d2487a0acb9 --- /dev/null +++ b/src/node_sqlite_vfs.h @@ -0,0 +1,89 @@ +#ifndef SRC_NODE_SQLITE_VFS_H_ +#define SRC_NODE_SQLITE_VFS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "sqlite3.h" + +#include +#include + +namespace node { + +class Environment; + +namespace permission { +class Permission; +} + +namespace sqlite { + +using SQLiteDlSym = void (*)(void); + +// Returns true if path contains a SQLite URI parameter named "vfs". +// URI-selected VFSes must not be allowed to bypass the permission VFS. +bool HasSQLiteVfsUriParameter(std::string_view path); +// Returns true if a SQLite URI parameter has the specified decoded value. +bool SQLiteUriParameterEquals(std::string_view path, + std::string_view parameter, + std::string_view value); +// Returns true if the first matching SQLite URI parameter is a true boolean. +bool SQLiteUriBooleanParameter(std::string_view path, + std::string_view parameter); +// Returns the decoded filesystem path portion of a SQLite URI filename. Plain +// filenames are returned unchanged. +std::string SQLitePathForPermission(std::string_view path); + +class SQLitePermissionVFS { + public: + explicit SQLitePermissionVFS(Environment* env); + ~SQLitePermissionVFS(); + + SQLitePermissionVFS(const SQLitePermissionVFS&) = delete; + SQLitePermissionVFS& operator=(const SQLitePermissionVFS&) = delete; + + bool Register(); + void Unregister(); + + const std::string& name() const { return name_; } + bool registered() const { return registered_; } + sqlite3_vfs* parent() const { return parent_; } + + int Open(sqlite3_filename name, + sqlite3_file* file, + int flags, + int* out_flags); + int Delete(const char* name, int sync_dir); + int Access(const char* name, int flags, int* result); + int FullPathname(const char* name, int length, char* output); + void* DlOpen(const char* name); + void DlError(int length, char* message); + SQLiteDlSym DlSym(void* handle, const char* symbol); + void DlClose(void* handle); + int Randomness(int length, char* output); + int Sleep(int microseconds); + int CurrentTime(double* time); + int GetLastError(int length, char* message); + int CurrentTimeInt64(sqlite3_int64* time); + int SetSystemCall(const char* name, sqlite3_syscall_ptr callback); + sqlite3_syscall_ptr GetSystemCall(const char* name); + const char* NextSystemCall(const char* name); + + bool AllowsPath(std::string_view path, bool read, bool write) const; + bool AllowsSidecarPath(std::string_view path, bool read, bool write) const; + bool AllowsTemporaryFile(bool read, bool write) const; + + private: + sqlite3_vfs vfs_{}; + sqlite3_vfs* parent_ = nullptr; + sqlite3_vfs* memory_vfs_ = nullptr; + permission::Permission* permission_ = nullptr; + std::string name_; + bool registered_ = false; +}; + +} // namespace sqlite +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_NODE_SQLITE_VFS_H_ diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc index 98146cf825ad38..e6947190d3eeb7 100644 --- a/src/permission/fs_permission.cc +++ b/src/permission/fs_permission.cc @@ -271,6 +271,42 @@ bool FSPermission::is_granted(Environment* env, } } +bool FSPermission::is_granted_no_side_effects( + PermissionScope perm, const std::string_view& param) const { + // VFS callbacks can execute on SQLite's worker threads. The path passed by + // the VFS has already been made absolute, so do not call PathResolve() or + // publish diagnostics here. + std::string path(param); +#ifdef _WIN32 + if (path.starts_with("\\\\?\\")) { + path.erase(0, 4); + } + if (path.starts_with("UNC\\")) { + path.erase(0, 4); + } + if (path.starts_with("//")) { + path.erase(0, 2); + } +#endif + + switch (perm) { + case PermissionScope::kFileSystem: + return allow_all_in_ && allow_all_out_; + case PermissionScope::kFileSystemRead: + return path.empty() ? allow_all_in_ + : !deny_all_in_ && + (allow_all_in_ || + granted_in_fs_.Lookup(path, true)); + case PermissionScope::kFileSystemWrite: + return path.empty() ? allow_all_out_ + : !deny_all_out_ && + (allow_all_out_ || + granted_out_fs_.Lookup(path, true)); + default: + return false; + } +} + FSPermission::RadixTree::RadixTree() : root_node_(new Node("")) {} FSPermission::RadixTree::~RadixTree() { diff --git a/src/permission/fs_permission.h b/src/permission/fs_permission.h index 19d72ef654c9f0..5f3de5d9f9b9bf 100644 --- a/src/permission/fs_permission.h +++ b/src/permission/fs_permission.h @@ -24,6 +24,8 @@ class FSPermission final : public PermissionBase { bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const override; + bool is_granted_no_side_effects( + PermissionScope perm, const std::string_view& param) const override; struct RadixTree { struct Node { diff --git a/src/permission/permission.cc b/src/permission/permission.cc index c593502d94eb49..5c4b125be49744 100644 --- a/src/permission/permission.cc +++ b/src/permission/permission.cc @@ -223,6 +223,15 @@ void Permission::AsyncThrowAccessDenied(Environment* env, // superseding error to be thrown. } +bool Permission::is_granted_no_side_effects( + PermissionScope permission, const std::string_view& resource) const { + if (!enabled_) return true; + + const auto it = nodes_.find(permission); + if (it == nodes_.end()) return false; + return it->second->is_granted_no_side_effects(permission, resource); +} + void Permission::EnablePermissions() { if (!enabled_) { enabled_ = true; diff --git a/src/permission/permission.h b/src/permission/permission.h index 84e3ea67ed5e04..430583265289cf 100644 --- a/src/permission/permission.h +++ b/src/permission/permission.h @@ -102,6 +102,11 @@ class Permission { FORCE_INLINE bool enabled() const { return enabled_; } + // Side-effect-free permission query for native I/O callbacks. The resource + // must already be an absolute path. + bool is_granted_no_side_effects( + PermissionScope permission, const std::string_view& resource = "") const; + FORCE_INLINE bool warning_only() const { return warning_only_; } static PermissionScope StringToPermission(const std::string& perm); diff --git a/src/permission/permission_base.h b/src/permission/permission_base.h index 11f4d6c6bfa65f..eaee7af658e249 100644 --- a/src/permission/permission_base.h +++ b/src/permission/permission_base.h @@ -65,6 +65,13 @@ class PermissionBase { virtual bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const = 0; + + // Used by native I/O callbacks. Implementations must not access V8, + // publish diagnostics, or resolve relative paths in this method. + virtual bool is_granted_no_side_effects( + PermissionScope perm, const std::string_view& param = "") const { + return false; + } }; } // namespace permission diff --git a/test/fixtures/permission/sqlite.db b/test/fixtures/permission/sqlite.db new file mode 100644 index 00000000000000..2321b9eac82af6 Binary files /dev/null and b/test/fixtures/permission/sqlite.db differ diff --git a/test/parallel/test-permission-sqlite.js b/test/parallel/test-permission-sqlite.js new file mode 100644 index 00000000000000..1068e0d639b7b2 --- /dev/null +++ b/test/parallel/test-permission-sqlite.js @@ -0,0 +1,370 @@ +// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process +'use strict'; + +const common = require('../common'); +common.skipIfSQLiteMissing(); + +const { isMainThread } = require('node:worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('node:assert'); +const fs = require('node:fs'); +const { spawnSync } = require('node:child_process'); +const { DatabaseSync } = require('node:sqlite'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const dbPath = fixtures.path('permission', 'sqlite.db'); +const deniedPath = tmpdir.resolve('denied.sqlite'); +const backupPath = tmpdir.resolve('backup.sqlite'); +const allowedBackupPath = tmpdir.resolve('allowed-backup.sqlite'); +const sidecarDbPath = tmpdir.resolve('sidecars.sqlite'); +const walDbPath = tmpdir.resolve('read-only-wal.sqlite'); +const suffixDbPath = dbPath + '-wal'; + +fs.rmSync(suffixDbPath, { force: true }); + +function run(code, permissions, env = {}) { + return spawnSync( + process.execPath, + [ + '--permission', + ...permissions, + '--eval', code.replace(/\n/g, ' '), + ], + { + encoding: 'utf8', + env: { + ...process.env, + DB_PATH: dbPath, + ...env, + }, + }, + ); +} + +const readOnly = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { readOnly: true }); + db.close();`; + +const readWrite = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH); + db.close();`; + +{ + const { status, stderr } = run( + readOnly, + ['--allow-fs-read=' + deniedPath], + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /Access to this API has been restricted/); +} + +{ + // A read-write database requires both read and write permission. + const { status, stderr } = run( + readWrite, + ['--allow-fs-read=' + dbPath], + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /Access to this API has been restricted/); +} + +{ + const { status, stderr } = run( + readWrite, + ['--allow-fs-write=' + dbPath], + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /Access to this API has been restricted/); +} + +{ + const { status, stderr } = run( + readWrite, + [ + '--allow-fs-read=' + dbPath, + '--allow-fs-write=' + dbPath, + ], + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // Delayed opening performs the same checks as opening in the constructor. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { open: false }); + db.open();`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + deniedPath, + '--allow-fs-write=' + deniedPath, + ], + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /Access to this API has been restricted/); +} + +{ + // In-memory databases never require filesystem permission. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(':memory:'); + db.close();`; + const { status, stderr } = run(code, []); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // SQLite URI filenames and URI access modes remain supported. + const code = `const { pathToFileURL } = require('node:url'); + const { DatabaseSync } = require('node:sqlite'); + const url = pathToFileURL(process.env.DB_PATH); + url.searchParams.set('mode', 'ro'); + const db = new DatabaseSync(url); + db.close();`; + const { status, stderr } = run( + code, + ['--allow-fs-read=' + dbPath], + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync('file:shared?mode=memory&cache=shared'); + db.exec('CREATE TABLE data(value)'); + db.close();`; + const { status, stderr } = run(code, []); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // SQLite processes repeated mode parameters in order. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync('file:unused?mode=ro&mode=memory'); + db.close();`; + const { status, stderr } = run(code, []); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // Immutable URI databases are read-only and do not require write access. + const code = `const { pathToFileURL } = require('node:url'); + const { DatabaseSync } = require('node:sqlite'); + const url = pathToFileURL(process.env.DB_PATH); + url.searchParams.set('immutable', '1'); + const db = new DatabaseSync(url); + db.close();`; + const { status, stderr } = run( + code, + ['--allow-fs-read=' + dbPath], + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // A URI cannot select an unwrapped VFS while permissions are enabled. + const code = `const { pathToFileURL } = require('node:url'); + const { DatabaseSync } = require('node:sqlite'); + const url = pathToFileURL(process.env.DB_PATH); + url.search = '?' + process.env.QUERY; + new DatabaseSync(url);`; + for (const query of ['%76fs=unix', 'vfs%00ignored=unix']) { + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + dbPath, + '--allow-fs-write=' + dbPath, + ], + { QUERY: query }, + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /must not specify a SQLite VFS/); + } +} + +{ + // Attached databases use the permission VFS too. A filename that resembles + // a sidecar does not inherit permission when opened as a main database. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH); + db.prepare('ATTACH DATABASE ? AS outside').run(process.env.ATTACH_PATH);`; + for (const attachPath of [deniedPath, suffixDbPath]) { + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + dbPath, + '--allow-fs-write=' + dbPath, + ], + { ATTACH_PATH: attachPath }, + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /unable to open database/); + } + assert.strictEqual(fs.existsSync(suffixDbPath), false); +} + +{ + // URI VFS overrides on ATTACH cannot bypass the permission VFS, even if + // the public authorizer callback is cleared. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(':memory:'); + db.setAuthorizer(null); + db.exec("ATTACH 'file::memory:?%76fs=memdb' AS outside");`; + const { status, stderr } = run(code, []); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /not authorized/); +} + +{ + // Backup destinations are opened through the permission VFS. + const code = `(async () => { + const { backup, DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { readOnly: true }); + try { + await backup(db, process.env.BACKUP_PATH); + process.exitCode = 1; + } catch { + process.exitCode = 0; + } + })();`; + const { status, stderr } = run( + code, + ['--allow-fs-read=' + dbPath], + { BACKUP_PATH: backupPath }, + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); + assert.strictEqual(fs.existsSync(backupPath), false); +} + +{ + // Backup destinations that resemble sidecars still require exact access. + const code = `(async () => { + const { backup, DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { readOnly: true }); + try { + await backup(db, process.env.BACKUP_PATH); + process.exitCode = 1; + } catch { + process.exitCode = 0; + } + })();`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + dbPath, + '--allow-fs-write=' + dbPath, + ], + { BACKUP_PATH: suffixDbPath }, + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); + assert.strictEqual(fs.existsSync(suffixDbPath), false); +} + +{ + // Backup I/O on worker threads remains available when permissions allow it. + const code = `(async () => { + const { backup, DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { readOnly: true }); + await backup(db, process.env.BACKUP_PATH); + db.close(); + })();`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + dbPath, + '--allow-fs-read=' + allowedBackupPath, + '--allow-fs-write=' + allowedBackupPath, + ], + { BACKUP_PATH: allowedBackupPath }, + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); + assert.strictEqual(fs.existsSync(allowedBackupPath), true); +} + +{ + // A database grant also covers SQLite-managed journal, WAL, and SHM files. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH); + db.exec('PRAGMA journal_mode=WAL; CREATE TABLE data(value)'); + db.close();`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + sidecarDbPath, + '--allow-fs-write=' + sidecarDbPath, + ], + { DB_PATH: sidecarDbPath }, + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // A read-only connection can consume an existing WAL without write access. + const writer = new DatabaseSync(walDbPath); + writer.exec(` + PRAGMA journal_mode=WAL; + PRAGMA wal_autocheckpoint=0; + CREATE TABLE data(value); + INSERT INTO data VALUES (1); + `); + + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH, { readOnly: true }); + db.prepare('SELECT * FROM data').all(); + db.close();`; + const { status, stderr } = run( + code, + ['--allow-fs-read=' + walDbPath], + { DB_PATH: walDbPath }, + ); + writer.close(); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // Internal temporary files fall back to the memory VFS when only + // path-specific filesystem permissions are available. + const code = `const { DatabaseSync } = require('node:sqlite'); + const db = new DatabaseSync(process.env.DB_PATH); + db.exec(\`PRAGMA temp_store=FILE; + PRAGMA temp.cache_size=1; + CREATE TEMP TABLE temp_data(value); + WITH RECURSIVE counter(value) AS ( + VALUES(1) UNION ALL + SELECT value + 1 FROM counter WHERE value < 1000 + ) + INSERT INTO temp_data SELECT randomblob(10000) FROM counter;\`); + db.close();`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + sidecarDbPath, + '--allow-fs-write=' + sidecarDbPath, + ], + { DB_PATH: sidecarDbPath }, + ); + assert.strictEqual(status, 0, `stderr: ${stderr}`); +} + +{ + // Empty filenames create anonymous temporary databases. A path-specific + // grant cannot authorize their initial database location. + const code = `const { DatabaseSync } = require('node:sqlite'); + new DatabaseSync('');`; + const { status, stderr } = run( + code, + [ + '--allow-fs-read=' + deniedPath, + '--allow-fs-write=' + deniedPath, + ], + ); + assert.strictEqual(status, 1, `stderr: ${stderr}`); + assert.match(stderr, /Access to this API has been restricted/); +}