diff --git a/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml new file mode 100644 index 0000000000..b36872d387 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml @@ -0,0 +1,178 @@ +// +// ScriptPermissions.cpp +// libraries/script-engine/src/ScriptPermissions.cpp +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Based on EntityScriptQMLWhitelist.qml +// Created by Kalila L. on 2019.12.05 | realities.dev | somnilibertas@gmail.com +// Copyright 2019 Kalila L. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Security settings for the script engines + +import Hifi 1.0 as Hifi +import QtQuick 2.8 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HiFiControls +import PerformanceEnums 1.0 +import "../../../windows" + + +Rectangle { + id: parentBody; + + function getWhitelistAsText() { + var whitelist = Settings.getValue("private/scriptPermissionGetAvatarURLSafeURLs"); + var arrayWhitelist = whitelist.split(",").join("\n"); + return arrayWhitelist; + } + + function setWhitelistAsText(whitelistText) { + Settings.setValue("private/scriptPermissionGetAvatarURLSafeURLs", whitelistText.text); + + var originalSetString = whitelistText.text; + var originalSet = originalSetString.split(' ').join(''); + + var check = Settings.getValue("private/scriptPermissionGetAvatarURLSafeURLs"); + var arrayCheck = check.split(",").join("\n"); + + setWhitelistSuccess(arrayCheck === originalSet); + } + + function setWhitelistSuccess(success) { + if (success) { + notificationText.text = "Successfully saved settings."; + } else { + notificationText.text = "Error! Settings not saved."; + } + } + + function toggleWhitelist(enabled) { + Settings.setValue("private/scriptPermissionGetAvatarURLEnable", enabled); + console.info("Toggling Protect Avatar URLs to:", enabled); + } + + function initCheckbox() { + var check = Settings.getValue("private/scriptPermissionGetAvatarURLEnable", true); + + if (check) { + whitelistEnabled.toggle(); + } + } + + + anchors.fill: parent + width: parent.width; + height: 120; + color: "#80010203"; + + HifiStylesUit.RalewayRegular { + id: titleText; + text: "Protect Avatar URLs" + // Text size + size: 24; + // Style + color: "white"; + elide: Text.ElideRight; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 60; + + CheckBox { + Component.onCompleted: { + initCheckbox(); + } + + id: whitelistEnabled; + + anchors.right: parent.right; + anchors.top: parent.top; + anchors.topMargin: 10; + onToggled: { + toggleWhitelist(whitelistEnabled.checked) + } + + Label { + text: "Enabled" + color: "white" + font.pixelSize: 18; + anchors.right: parent.left; + anchors.top: parent.top; + anchors.topMargin: 10; + } + } + } + + Rectangle { + id: textAreaRectangle; + color: "black"; + width: parent.width; + height: 250; + anchors.top: titleText.bottom; + + ScrollView { + id: textAreaScrollView + anchors.fill: parent; + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: parent.height + clip: false; + + TextArea { + id: whitelistTextArea + text: getWhitelistAsText(); + onTextChanged: notificationText.text = ""; + width: parent.width; + height: parent.height; + font.family: "Ubuntu"; + font.pointSize: 12; + color: "white"; + } + } + + Button { + id: saveChanges + anchors.topMargin: 5; + anchors.leftMargin: 20; + anchors.rightMargin: 20; + x: textAreaRectangle.x + textAreaRectangle.width - width - 15; + y: textAreaRectangle.y + textAreaRectangle.height - height; + contentItem: Text { + text: saveChanges.text + font.family: "Ubuntu"; + font.pointSize: 12; + opacity: enabled ? 1.0 : 0.3 + color: "black" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + text: "Save Changes" + onClicked: setWhitelistAsText(whitelistTextArea) + + HifiStylesUit.RalewayRegular { + id: notificationText; + text: "" + // Text size + size: 16; + // Style + color: "white"; + elide: Text.ElideLeft; + // Anchors + anchors.right: parent.left; + anchors.rightMargin: 10; + } + } + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 137457c722..431bd2cb96 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3305,7 +3305,7 @@ void Application::initializeUi() { // END PULL SAFEURLS FROM INTERFACE.JSON Settings - if (AUTHORIZED_EXTERNAL_QML_SOURCE.isParentOf(url)) { + if (QUrl(NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS).isParentOf(url)) { return true; } else { for (const auto& str : safeURLS) { diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index aae6839282..7017f2a083 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -323,6 +323,19 @@ Menu::Menu() { } }); + // Settings > Script Security + action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::ScriptSecurity); + connect(action, &QAction::triggered, [] { + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + + tablet->pushOntoStack("hifi/dialogs/security/ScriptSecurity.qml"); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } + }); + // Settings > Developer Menu addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 931c00cfd9..e0cdfdf4fd 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -190,6 +190,7 @@ namespace MenuOption { const QString RunTimingTests = "Run Timing Tests"; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString EntityScriptQMLWhitelist = "Entity Script / QML Whitelist"; + const QString ScriptSecurity = "Script Security"; const QString ShowTrackedObjects = "Show Tracked Objects"; const QString SelfieCamera = "Selfie"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 62100f9cae..dc8963689a 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -73,6 +73,7 @@ #include "MovingEntitiesOperator.h" #include "SceneScriptingInterface.h" #include "WarningsSuppression.h" +#include "ScriptPermissions.h" using namespace std; @@ -2236,6 +2237,9 @@ AttachmentData MyAvatar::loadAttachmentData(const QUrl& modelURL, const QString& return attachment; } +bool MyAvatar::isMyAvatarURLProtected() const { + return !ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL); +} int MyAvatar::parseDataFromBuffer(const QByteArray& buffer) { qCDebug(interfaceapp) << "Error: ignoring update packet for MyAvatar" diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5e0627360c..4198deba84 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2683,6 +2683,7 @@ private: void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } virtual bool isMyAvatar() const override { return true; } + virtual bool isMyAvatarURLProtected() const override; virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c71da50b1a..da48bced06 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2106,6 +2106,18 @@ const QUrl& AvatarData::getSkeletonModelURL() const { } } +QString AvatarData::getSkeletonModelURLFromScript() const { + if (isMyAvatar()) { + if (!isMyAvatarURLProtected()) { + return _skeletonModelURL.toString(); + } else { + return QString(); + } + } else { + return QString(); + } +}; + QByteArray AvatarData::packSkeletonData() const { // Send an avatar trait packet with the skeleton data before the mesh is loaded int avatarDataSize = 0; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0b2a925de0..69dd747543 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1355,7 +1355,7 @@ public: */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); - QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); } + QString getSkeletonModelURLFromScript() const; void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); } void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a07c402555..5a2cd2d225 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -204,7 +204,15 @@ bool ScriptAvatarData::getLookAtSnappingEnabled() const { // QString ScriptAvatarData::getSkeletonModelURLFromScript() const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { - return sharedAvatarData->getSkeletonModelURLFromScript(); + if (sharedAvatarData->isMyAvatar()) { + if (sharedAvatarData->isMyAvatarURLProtected()) { + return QString(); + } else { + return sharedAvatarData->getSkeletonModelURLFromScript(); + } + } else { + return QString(); + } } else { return QString(); } diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 4287da92d4..b10e09497a 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -58,6 +58,8 @@ namespace NetworkingConstants { const QString HF_PUBLIC_CDN_URL = ""; const QString HF_MARKETPLACE_CDN_HOSTNAME = ""; const QString OVERTE_CONTENT_CDN_URL = "https://content.overte.org/"; + const QString OVERTE_COMMUNITY_APPLICATIONS = { "https://more.overte.org/applications" }; + const QString OVERTE_TUTORIAL_SCRIPTS = { "https://more.overte.org/tutorial" }; #if USE_STABLE_GLOBAL_SERVICES const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.overte.org"; diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index 37ba7d0442..2caab052ec 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -542,6 +542,10 @@ QString ScriptManager::getFilename() const { return lastPart; } +QString ScriptManager::getAbsoluteFilename() const { + return _fileNameString; +} + bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index 623b51a43f..8197f26285 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -430,6 +430,13 @@ public: */ QString getFilename() const; + /** + * @brief Get the filename of the running script, with absolute path. + * + * @return QString Filename + */ + QString getAbsoluteFilename() const; + /** * @brief Underlying scripting engine * diff --git a/libraries/script-engine/src/ScriptPermissions.cpp b/libraries/script-engine/src/ScriptPermissions.cpp new file mode 100644 index 0000000000..80bdb5ef4d --- /dev/null +++ b/libraries/script-engine/src/ScriptPermissions.cpp @@ -0,0 +1,102 @@ +// +// ScriptPermissions.cpp +// libraries/script-engine/src/ScriptPermissions.cpp +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptPermissions.h" + +#include +#include + +#include "ScriptEngine.h" +#include "ScriptManager.h" +#include "Scriptable.h" + +static const bool PERMISSIONS_DEBUG_ENABLED = false; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionNames { + "Permission to get user's avatar URL" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingKeyNames { + "private/scriptPermissionGetAvatarURLSafeURLs" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableKeyNames { + "private/scriptPermissionGetAvatarURLEnable" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableDefaultValues { + true //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +bool ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission permission) { + if (permission >= ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE) { + return false; + } + int permissionIndex = static_cast(permission); + // Check if the permission checking is active + Setting::Handle isCheckingEnabled(scriptPermissionSettingEnableKeyNames[permissionIndex], scriptPermissionSettingEnableDefaultValues[permissionIndex]); + // Get the script manager: + auto engine = Scriptable::engine(); + if (!engine) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed called outside script engine for permission: " << scriptPermissionNames[permissionIndex]; + return false; + } + auto manager = engine->manager(); + if (!manager) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed called from script engine with no script manager for permission: " << scriptPermissionNames[permissionIndex]; + return false; + } + std::vector urlsToCheck; + QString scriptURL = manager->getAbsoluteFilename(); + if (scriptURL.startsWith("about:Entities")) { + // This is entity script manager, we need to find the file name of the current script instead + scriptURL = Scriptable::context()->currentFileName(); + urlsToCheck.push_back(scriptURL); + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: filename: " << scriptURL; + } + auto parentContext = Scriptable::context()->parentContext(); + while (parentContext) { + urlsToCheck.push_back(parentContext->currentFileName()); + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: parent filename: " << parentContext->currentFileName(); + } + parentContext = parentContext->parentContext(); + } + } else { + urlsToCheck.push_back(scriptURL); + } + // Check if the script is allowed: + QList safeURLPrefixes = { "file:///", "qrc:/", NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS, + NetworkingConstants::OVERTE_TUTORIAL_SCRIPTS/*, "about:console"*/}; + Setting::Handle allowedURLsSetting(scriptPermissionSettingKeyNames[permissionIndex]); + QList allowedURLs = allowedURLsSetting.get().split("\n"); + + for (auto entry : allowedURLs) { + safeURLPrefixes.push_back(entry); + } + + for (const auto& str : safeURLPrefixes) { + if (!str.isEmpty() && scriptURL.startsWith(str)) { + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex] + << " for script " << scriptURL << " accepted with rule: " << str; + } + return true; + } + } + + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex] << " for script " + << scriptURL << " rejected."; + } + return false; +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptPermissions.h b/libraries/script-engine/src/ScriptPermissions.h new file mode 100644 index 0000000000..f4b06253c5 --- /dev/null +++ b/libraries/script-engine/src/ScriptPermissions.h @@ -0,0 +1,31 @@ +// +// ScriptPermissions.h +// libraries/script-engine/src/ScriptPermissions.h +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include + +#include "SettingHandle.h" +#include "DependencyManager.h" + +class ScriptPermissions { +public: + enum class Permission { + SCRIPT_PERMISSION_GET_AVATAR_URL, + SCRIPT_PERMISSIONS_SIZE + }; + + static bool isCurrentScriptAllowed(Permission permission); + //TODO: add a function to request permission through a popup +}; + +// TODO: add ScriptPermissionsScriptingInterface, where script can check if they have permissions +// and request permissions through a tablet popup. diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 29f23afdfb..5de86d7f95 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -47,6 +47,7 @@ public: virtual void setParentID(const QUuid& parentID); virtual bool isMyAvatar() const { return false; } + virtual bool isMyAvatarURLProtected() const { return false; } // This needs to be here because both MyAvatar and AvatarData inherit from MyAvatar virtual quint16 getParentJointIndex() const { return _parentJointIndex; } virtual void setParentJointIndex(quint16 parentJointIndex);