* @brief Check whether a module import should be allowed during document restore. * * Modules already in @c sys.modules are permitted -- they were loaded by FreeCAD core or addons * during normal startup. For modules not yet loaded we use @c importlib.util.find_spec() to * locate where the module would come from without executing it, then verify that path is under * a FreeCAD module dir
| 87 | * @return @c true if the module is allowed, @c false otherwise. |
| 88 | */ |
| 89 | bool isAllowedModule(const std::string& moduleName) |
| 90 | { |
| 91 | Py::Dict sysModules(PyImport_GetModuleDict()); |
| 92 | if (sysModules.isNone()) { |
| 93 | return false; |
| 94 | } |
| 95 | |
| 96 | // 1) Already loaded? Must be safe. |
| 97 | if (sysModules.hasKey(moduleName)) { |
| 98 | return true; |
| 99 | } |
| 100 | |
| 101 | // 2) Is it *in* an already loaded module? Safe. |
| 102 | std::string::size_type dot = moduleName.find('.'); |
| 103 | if (dot != std::string::npos) { |
| 104 | std::string topLevel = moduleName.substr(0, dot); |
| 105 | if (sysModules.hasKey(topLevel)) { |
| 106 | return true; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | // 3) The complicated path. Use importlib.util.find_spec() to find the origin of the module, |
| 111 | // being careful to NOT load it (which is the code-execution vulnerability we're trying to |
| 112 | // avoid in the first place). See if it's in one of our "safe" paths, and if it is, allow it. |
| 113 | // Safe paths are a few subdirectories we recognize in the set "home", "resource", and |
| 114 | // "userData" paths. Don't allow modules from outside of these directories. Not 100% mitigation, |
| 115 | // but it's better than nothing. |
| 116 | PyObject* importlibUtil = PyImport_ImportModule("importlib.util"); |
| 117 | if (!importlibUtil) { |
| 118 | PyErr_Clear(); |
| 119 | return false; |
| 120 | } |
| 121 | Py::Module importlib(importlibUtil, true); |
| 122 | Py::Callable findSpec(importlib.getAttr("find_spec")); |
| 123 | |
| 124 | // FreeCAD adds each workbench directory to sys.path individually (e.g. .../Mod/Assembly/), |
| 125 | // so a module stored as "Assembly.JointObject" in the FCStd is actually importable as just |
| 126 | // "JointObject". Try the full name first, then the part after the first dot. |
| 127 | std::vector<std::string> namesToTry = {moduleName}; |
| 128 | if (dot != std::string::npos) { |
| 129 | namesToTry.push_back(moduleName.substr(dot + 1)); |
| 130 | } |
| 131 | Py::Object spec; |
| 132 | for (const std::string& name : namesToTry) { |
| 133 | Py::Tuple args(1); |
| 134 | args.setItem(0, Py::String(name)); |
| 135 | try { |
| 136 | spec = findSpec.apply(args); |
| 137 | } |
| 138 | catch (Py::Exception&) { |
| 139 | PyErr_Clear(); |
| 140 | continue; |
| 141 | } |
| 142 | if (!spec.isNone()) { |
| 143 | break; |
| 144 | } |
| 145 | } |
| 146 | if (spec.isNone()) { |