Version in base suite: 1.0.0+dfsg-8+deb13u2 Base version: freecad_1.0.0+dfsg-8+deb13u2 Target version: freecad_1.0.0+dfsg-8+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/f/freecad/freecad_1.0.0+dfsg-8+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/f/freecad/freecad_1.0.0+dfsg-8+deb13u3.dsc changelog | 22 + patches/CVE-2026-34398-CVE-2026-34399.patch | 197 ++++++++++++++++ patches/CVE-2026-34789-1-81b73925.patch | 336 ++++++++++++++++++++++++++++ patches/CVE-2026-34789-2-e2dc6c81.patch | 49 ++++ patches/CVE-2026-34789-3-526a4f0d.patch | 88 +++++++ patches/CVE-2026-73233.patch | 124 ++++++++++ patches/CVE-2026-73234.patch | 227 ++++++++++++++++++ patches/CVE-2026-73235.patch | 48 ++++ patches/series | 7 9 files changed, 1098 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpcu92ui0h/freecad_1.0.0+dfsg-8+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpcu92ui0h/freecad_1.0.0+dfsg-8+deb13u3.dsc: no acceptable signature found diff -Nru freecad-1.0.0+dfsg/debian/changelog freecad-1.0.0+dfsg/debian/changelog --- freecad-1.0.0+dfsg/debian/changelog 2026-06-27 07:50:13.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/changelog 2026-08-25 08:56:38.000000000 +0000 @@ -1,3 +1,25 @@ +freecad (1.0.0+dfsg-8+deb13u3) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + * CVE-2026-34398, CVE-2026-34399: arbitrary Python code execution via + eval() on untrusted input in the BIM workbench + * CVE-2026-34789: restrict imports to modules located under FreeCAD's + own Mod and macro directories. + * CVE-2026-73233: the escaping helper in the FEM displacement + constraint task dialog neutralised quotation marks but not + backslashes, allowing Python code injection through a crafted + displacement formula. + * CVE-2026-73235: the Xerces SAX2 reader for FCStd Document.xml + resolved external entities and loaded external DTDs, allowing local + file disclosure via file: URIs and SSRF via http: URIs. + * CVE-2026-73234: PropertyFileIncluded::Restore() concatenated an + attacker-controlled file attribute from Document.xml with the + document transient path without rejecting directory components, + absolute paths or parent references, so a crafted FCStd archive + could write anywhere the user can write. + + -- Aron Xu Tue, 25 Aug 2026 16:56:38 +0800 + freecad (1.0.0+dfsg-8+deb13u2) trixie; urgency=medium * Maintaner approvided upload. diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-34398-CVE-2026-34399.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-34398-CVE-2026-34399.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-34398-CVE-2026-34399.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-34398-CVE-2026-34399.patch 2026-08-25 08:39:41.000000000 +0000 @@ -0,0 +1,197 @@ +Description: CVE-2026-34398, CVE-2026-34399: BIM: remove eval() on untrusted input + The BIM workbench passed attacker-controlled strings straight to eval(), + giving arbitrary Python code execution to anybody able to hand the victim + a crafted file. + . + CVE-2026-34398 (GHSA-8rfj-7956-6gwf): bimcommands/BimProjectManager.py + eval()s the "wpposition", "wpu", "wpv" and "wpaxis" Meta properties read + from an FCStd project template. Loading a malicious template executes the + payload. Replaced by a strict regexp-based Vector(x,y,z) parser. + . + CVE-2026-34399 (GHSA-chv4-vm6r-wjqj): bimcommands/BimTDPage.py eval()s the + "Scale" editable-text field of an SVG TechDraw template as soon as it + contains a "/". Replaced by an explicit split on "/" and two float() + conversions. + . + The same commit also removes two further eval() uses on data that is not + necessarily trusted: the colour columns of the layers manager + (bimcommands/BimLayers.py, now ast.literal_eval()) and the IfcBoolean / + IfcLogical property values in importers/exportIFC.py (now a plain string + comparison). They are carried over as well because they are part of the + same hardening. +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/9ed351cc4700db0a94c46f020c34c58bbf1bdaba +Author: Chris Hennes +Applied-Upstream: 1.1.1, https://github.com/FreeCAD/FreeCAD/commit/9ed351cc4700db0a94c46f020c34c58bbf1bdaba +Last-Update: 2026-08-25 +Note: Backported to 1.0.0. All four files of the upstream commit still exist + in 1.0.0 and all six eval() call sites are present, so no hunk was dropped. + Divergences, all caused by 1.0.0 code that upstream had already reworked + before the fix landed: + . + * BimProjectManager.py: 1.0.0 still uses the legacy + FreeCAD.DraftWorkingPlane singleton guarded by + hasattr(FreeCAD, "DraftWorkingPlane"), not + WorkingPlane.get_working_plane(), and it has no wp._handle_custom() call. + The upstream rewrite into a (key, attribute) loop is therefore not + applicable; the four assignments were kept in place and only eval() was + swapped for the new _parse_vector() helper, which is taken verbatim from + upstream. This deliberately preserves 1.0.0's pre-existing + 'if "wppos" in values' typo (upstream's loop silently changes that key to + "wpposition"); fixing it is a behaviour change unrelated to the + vulnerability, and the eval() is removed either way. The now-unused + 'from FreeCAD import Vector' import is dropped, as upstream does. + . + * BimLayers.py: 1.0.0 imports os before FreeCAD, so 'import ast' is added + at the top of that same block instead of in a separate group. + . + * exportIFC.py: 1.0.0 predates the black reformatting of this file, so the + surrounding context differs in whitespace only; the change itself is + identical to upstream. + . + The upstream commit contains no test hunks, so none had to be adapted. + . + Behaviour changes carried over unmodified from upstream (not porting + artefacts of this backport): + . + * BimTDPage.py: 'if ":" in val: val.replace(":", "/")' becomes + 'val = val.replace(":", "/")'. The original was a no-op (str.replace() + returns a new string rather than mutating val), so a template scale of + "1:100" never reached the "/" branch and was silently ignored; it is now + honoured as 0.01. This is upstream's intent, not a porting artefact. + * BimTDPage.py: the bare 'except:' clauses become + 'except (ValueError, ZeroDivisionError)' and 'except ValueError', so + exceptions raised by 'page.Scale = ...' other than those now propagate + instead of being silently swallowed. + * BimProjectManager.py: _parse_vector() raises ValueError, and the four + call sites in loadTemplate() do not catch it, so a template whose Meta + holds a malformed 'wp*' string now aborts template loading instead of + silently misbehaving. Upstream has the identical property. Values that + FreeCAD itself writes (e.g. "Vector (0.0, 0.0, 0.0)") are accepted by + the regexp, so ordinary templates are unaffected. +--- +--- a/src/Mod/BIM/bimcommands/BimLayers.py ++++ b/src/Mod/BIM/bimcommands/BimLayers.py +@@ -22,6 +22,7 @@ + + """Layers manager for FreeCAD""" + ++import ast + import os + import FreeCAD + import FreeCADGui +@@ -608,16 +609,16 @@ + ["Solid", "Dashed", "Dotted", "Dashdot"][editor.currentIndex()], + ) + elif index.column() == 4: # Line color +- model.setData(index, eval(editor.text()), QtCore.Qt.UserRole) +- model.itemFromIndex(index).setIcon(getColorIcon(eval(editor.text()))) ++ model.setData(index, ast.literal_eval(editor.text()), QtCore.Qt.UserRole) ++ model.itemFromIndex(index).setIcon(getColorIcon(ast.literal_eval(editor.text()))) + elif index.column() == 5: # Shape color +- model.setData(index, eval(editor.text()), QtCore.Qt.UserRole) +- model.itemFromIndex(index).setIcon(getColorIcon(eval(editor.text()))) ++ model.setData(index, ast.literal_eval(editor.text()), QtCore.Qt.UserRole) ++ model.itemFromIndex(index).setIcon(getColorIcon(ast.literal_eval(editor.text()))) + elif index.column() == 6: # Transparency + model.setData(index, editor.value()) + elif index.column() == 7: # Line prin color +- model.setData(index, eval(editor.text()), QtCore.Qt.UserRole) +- model.itemFromIndex(index).setIcon(getColorIcon(eval(editor.text()))) ++ model.setData(index, ast.literal_eval(editor.text()), QtCore.Qt.UserRole) ++ model.itemFromIndex(index).setIcon(getColorIcon(ast.literal_eval(editor.text()))) + + + FreeCADGui.addCommand("BIM_Layers", BIM_Layers()) +--- a/src/Mod/BIM/bimcommands/BimProjectManager.py ++++ b/src/Mod/BIM/bimcommands/BimProjectManager.py +@@ -24,10 +24,20 @@ + + + import os ++import re + import sys + import FreeCAD + import FreeCADGui + ++ ++def _parse_vector(text): ++ """Parse a Vector(x,y,z) string safely without eval().""" ++ match = re.match(r"^\s*Vector\s*\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,)]+)\s*\)\s*$", text) ++ if not match: ++ raise ValueError("Invalid Vector string: " + text) ++ return FreeCAD.Vector(float(match.group(1)), float(match.group(2)), float(match.group(3))) ++ ++ + QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP + translate = FreeCAD.Qt.translate + +@@ -609,16 +619,14 @@ + values = d.Meta + bimunit = 0 + if hasattr(FreeCAD, "DraftWorkingPlane"): +- from FreeCAD import Vector +- + if "wppos" in values: +- FreeCAD.DraftWorkingPlane.position = eval(values["wpposition"]) ++ FreeCAD.DraftWorkingPlane.position = _parse_vector(values["wpposition"]) + if "wpu" in values: +- FreeCAD.DraftWorkingPlane.u = eval(values["wpu"]) ++ FreeCAD.DraftWorkingPlane.u = _parse_vector(values["wpu"]) + if "wpv" in values: +- FreeCAD.DraftWorkingPlane.v = eval(values["wpv"]) ++ FreeCAD.DraftWorkingPlane.v = _parse_vector(values["wpv"]) + if "wpaxis" in values: +- FreeCAD.DraftWorkingPlane.axis = eval(values["wpaxis"]) ++ FreeCAD.DraftWorkingPlane.axis = _parse_vector(values["wpaxis"]) + if "unit" in values: + FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").SetInt( + "UserSchema", int(values["unit"]) +--- a/src/Mod/BIM/bimcommands/BimTDPage.py ++++ b/src/Mod/BIM/bimcommands/BimTDPage.py +@@ -82,19 +82,19 @@ + if txt in page.Template.EditableTexts: + val = page.Template.EditableTexts[txt] + if val: +- if ":" in val: +- val.replace(":", "/") ++ val = val.replace(":", "/") + if "/" in val: + try: +- page.Scale = eval(val) +- except: ++ num, den = val.split("/", 1) ++ page.Scale = float(num) / float(den) ++ except (ValueError, ZeroDivisionError): + pass + else: + break + else: + try: + page.Scale = float(val) +- except: ++ except ValueError: + pass + else: + break +--- a/src/Mod/BIM/importers/exportIFC.py ++++ b/src/Mod/BIM/importers/exportIFC.py +@@ -1658,15 +1658,15 @@ + if ptype in ["IfcLabel","IfcText","IfcIdentifier",'IfcDescriptiveMeasure']: + pass + elif ptype == "IfcBoolean": +- if pvalue in ["True", "False"]: +- pvalue = eval(pvalue) ++ if pvalue == "True": ++ pvalue = True + elif pvalue == ".T.": + pvalue = True + else: + pvalue = False + elif ptype == "IfcLogical": +- if pvalue in ["True", "False"]: +- pvalue = eval(pvalue) ++ if pvalue == "True": ++ pvalue = True + elif pvalue.upper() == "TRUE": + pvalue = True + else: diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-1-81b73925.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-1-81b73925.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-1-81b73925.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-1-81b73925.patch 2026-08-25 08:41:33.000000000 +0000 @@ -0,0 +1,336 @@ +Description: CVE-2026-34789: validate module imports in PropertyPythonObject::Restore() + A crafted FCStd document could set the "module" attribute of a serialised + PropertyPythonObject to an arbitrary Python module name. On restore that name + was handed straight to PyImport_ImportModule(), so merely opening the document + executed the module-level code of any importable module. The legacy pickle + fallback branch was worse still: it imported an attacker-named module and then + invoked one of its attributes via PyObject_CallObject(). + . + This introduces multi-stage validation of module imports when loading an FCStd + file: + . + 1) Has the module already been loaded? If so, it's OK. + 2) Is the module located in a known location? (e.g. Mod, Ext, etc.) OK. + 3) Legacy modules that are now handled by a loader module that is in a + known location (for example, femobjects._FemElementGeometry2D) are OK. + . + If the module is outside these parameters it is rejected. The fallback Pickle + code for handling files from FreeCAD 0.12 and earlier is removed outright, + since it was itself a minor vulnerability. +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/81b73925ce22610542367301d8eff4259eb9596e +Author: Chris Hennes +Applied-Upstream: 1.1.1, https://github.com/FreeCAD/FreeCAD/commit/81b73925ce22610542367301d8eff4259eb9596e +Last-Update: 2026-08-25 +Note: Backported to 1.0.0. The upstream commit does not apply because + src/App/PropertyPythonObject.cpp was reformatted and modernised between 1.0.0 + and 1.1.x; the logic added here is a line-for-line reproduction of the upstream + helpers with the following deliberate divergences: + . + * C++17 compatibility. FreeCAD 1.0.0 builds with CMAKE_CXX_STANDARD 17 by + default (cMake/FreeCAD_Helpers/CompilerChecksAndSetups.cmake) and debian/rules + does not raise it, whereas 1.1.x builds with C++20. In isUnderDirectory() the + upstream C++20 constructs std::ranges::replace() and std::string::starts_with() + are therefore replaced by std::replace() and rfind(prefix, 0) == 0, which are + exactly equivalent. Everything else in the helper (the slash-collapsing lambda, + the trailing-slash normalisation) is unchanged. + . + * 1.0.0 API spellings. Base::XMLReader::getAttribute() is not yet a template in + 1.0.0, so reader.getAttribute("module") is written + reader.getAttribute("module"); Base::Console().warning() is Warning() and + Base::PyException::reportException() is ReportException(). + . + * Brace/indent style follows the surrounding 1.0.0 code (which has not had + clang-format applied), so the single-statement if bodies in Restore() keep + their 1.0.0 form. + . + No upstream hunk was dropped: the anonymous-namespace helpers, the removal of + loadPickle() and of the pickle branch in Restore(), the removal of the + load_pickle flag, and the declaration removal in PropertyPythonObject.h are all + carried over. The upstream commit contains no tests. This is part 1 of 3: the + CVE-2026-34789 fix is only complete with CVE-2026-34789-2-e2dc6c81.patch and + CVE-2026-34789-3-526a4f0d.patch, and all three must travel together, because + applying this one alone leaves the import allowlist on an intermediate upstream + state that silently drops PropertyPythonObject data for classes living under + the macro directories. +--- +--- a/src/App/PropertyPythonObject.cpp ++++ b/src/App/PropertyPythonObject.cpp +@@ -23,8 +23,10 @@ + + #include "PreCompiled.h" + ++#include + #include +-#include ++#include ++#include + + #include + #include +@@ -32,12 +34,156 @@ + #include + #include + ++#include "Application.h" + #include "PropertyPythonObject.h" + #include "DocumentObject.h" + + + using namespace App; + ++namespace { ++ ++/** ++ * @brief Check whether a path starts with a given directory prefix. ++ * ++ * @param[in] filePath The file path to check. ++ * @param[in] directory The directory prefix to match against. ++ * @return @c true if @p filePath starts with @p directory. ++ */ ++bool isUnderDirectory(std::string filePath, std::string directory) ++{ ++ std::replace(filePath.begin(), filePath.end(), '\\', '/'); ++ std::replace(directory.begin(), directory.end(), '\\', '/'); ++ // Collapse repeated slashes (e.g. home path "build/debug//" + "Mod") ++ auto collapseSlashes = [](std::string& s) { ++ auto out = s.begin(); ++ for (auto it = s.begin(); it != s.end(); ++it) { ++ if (*it == '/' && out != s.begin() && *(out - 1) == '/') { ++ continue; ++ } ++ *out++ = *it; ++ } ++ s.erase(out, s.end()); ++ }; ++ collapseSlashes(filePath); ++ collapseSlashes(directory); ++ if (!directory.empty() && directory.back() != '/') { ++ directory += '/'; ++ } ++ return filePath.rfind(directory, 0) == 0; ++} ++ ++/** ++ * @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 directory. This prevents a crafted FCStd from importing arbitrary modules ++ * (whose __init__.py could run malicious code on import) while still allowing ++ * legitimate lazy-loaded FreeCAD workbench modules to restore. ++ * ++ * @param[in] moduleName The fully qualified Python module name to check. ++ * @return @c true if the module is allowed, @c false otherwise. ++ */ ++bool isAllowedModule(const std::string& moduleName) ++{ ++ Py::Dict sysModules(PyImport_GetModuleDict()); ++ if (sysModules.isNone()) { ++ return false; ++ } ++ ++ // 1) Already loaded? Must be safe. ++ if (sysModules.hasKey(moduleName)) { ++ return true; ++ } ++ ++ // 2) Is it *in* an already loaded module? Safe. ++ std::string::size_type dot = moduleName.find('.'); ++ if (dot != std::string::npos) { ++ std::string topLevel = moduleName.substr(0, dot); ++ if (sysModules.hasKey(topLevel)) { ++ return true; ++ } ++ } ++ ++ // 3) The complicated path. Use importlib.util.find_spec() to find the origin of the module, ++ // being careful to NOT load it (which is the code-execution vulnerability we're trying to ++ // avoid in the first place). See if it's in one of our "safe" paths, and if it is, allow it. ++ // Safe paths are a few subdirectories we recognize in the set "home", "resource", and ++ // "userData" paths. Don't allow modules from outside of these directories. Not 100% mitigation, ++ // but it's better than nothing. ++ PyObject* importlibUtil = PyImport_ImportModule("importlib.util"); ++ if (!importlibUtil) { ++ PyErr_Clear(); ++ return false; ++ } ++ Py::Module importlib(importlibUtil, true); ++ Py::Callable findSpec(importlib.getAttr("find_spec")); ++ ++ // FreeCAD adds each workbench directory to sys.path individually (e.g. .../Mod/Assembly/), ++ // so a module stored as "Assembly.JointObject" in the FCStd is actually importable as just ++ // "JointObject". Try the full name first, then the part after the first dot. ++ std::vector namesToTry = {moduleName}; ++ if (dot != std::string::npos) { ++ namesToTry.push_back(moduleName.substr(dot + 1)); ++ } ++ Py::Object spec; ++ for (const std::string& name : namesToTry) { ++ Py::Tuple args(1); ++ args.setItem(0, Py::String(name)); ++ try { ++ spec = findSpec.apply(args); ++ } ++ catch (Py::Exception&) { ++ PyErr_Clear(); ++ continue; ++ } ++ if (!spec.isNone()) { ++ break; ++ } ++ } ++ if (spec.isNone()) { ++ return false; ++ } ++ ++ std::string home = Application::getHomePath(); ++ std::string userData = Application::getUserAppDataDir(); ++ std::string resource = Application::getResourceDir(); ++ ++ auto isUnderFreeCAD = [&](const std::string& path) { ++ return isUnderDirectory(path, home + "Mod") ++ || isUnderDirectory(path, home + "Ext") ++ || isUnderDirectory(path, resource + "Mod") ++ || isUnderDirectory(path, resource + "Ext") ++ || isUnderDirectory(path, userData + "Mod"); ++ }; ++ ++ // Get the origin (i.e. the file path) from the spec. ++ Py::Object origin = spec.getAttr("origin"); ++ if (!origin.isNone() && origin.isString()) { ++ return isUnderFreeCAD(Py::String(origin).as_std_string()); ++ } ++ ++ // No origin -- this could be a built-in module (why is an FCStd trying to load this? Very ++ // suspicious, block it) or a synthetic module from a FreeCAD migration finder like ++ // FemMigrateApp (which we will allow). Check whether the spec's loader itself comes from a ++ // FreeCAD module directory. ++ if (!spec.hasAttr("loader") || spec.getAttr("loader").isNone()) { ++ return false; ++ } ++ Py::Object loader = spec.getAttr("loader"); ++ auto loaderType = loader.type(); ++ if (!loaderType.hasAttr("__module__")) { ++ return false; ++ } ++ std::string loaderModuleName = Py::String(loaderType.getAttr("__module__")).as_std_string(); ++ Py::Object loaderMod = sysModules.getItem(loaderModuleName); ++ return isUnderFreeCAD(Py::String(loaderMod.getAttr("__file__")).as_std_string()); ++} ++ ++} // anonymous namespace ++ + + TYPESYSTEM_SOURCE(App::PropertyPythonObject , App::Property) + +@@ -173,31 +319,6 @@ + } + } + +-void PropertyPythonObject::loadPickle(const std::string& str) +-{ +- // find the custom attributes and restore them +- Base::PyGILStateLocker lock; +- try { +- std::string buffer = str; +- boost::regex pickle(R"(S'(\w+)'.+S'(\w+)'\n)"); +- boost::match_results what; +- std::string::const_iterator start, end; +- start = buffer.begin(); +- end = buffer.end(); +- while (boost::regex_search(start, end, what, pickle)) { +- std::string key = std::string(what[1].first, what[1].second); +- std::string val = std::string(what[2].first, what[2].second); +- this->object.setAttr(key, Py::String(val)); +- buffer = std::string(what[2].second, end); +- start = buffer.begin(); +- end = buffer.end(); +- } +- } +- catch (Py::Exception&) { +- Base::PyException e; // extract the Python error text +- e.ReportException(); +- } +-} + + std::string PropertyPythonObject::encodeValue(const std::string& str) const + { +@@ -334,7 +455,6 @@ + } + else { + bool load_json=false; +- bool load_pickle=false; + bool load_failed=false; + std::string buffer = reader.getAttribute("value"); + if (reader.hasAttribute("encoded") && +@@ -347,20 +467,24 @@ + + Base::PyGILStateLocker lock; + try { +- boost::regex pickle(R"(^\(i(\w+)\n(\w+)\n)"); +- boost::match_results what; +- std::string::const_iterator start, end; +- start = buffer.begin(); +- end = buffer.end(); + if (reader.hasAttribute("module") && reader.hasAttribute("class")) { +- Py::Module mod(PyImport_ImportModule(reader.getAttribute("module")),true); ++ std::string moduleName = reader.getAttribute("module"); ++ if (!isAllowedModule(moduleName)) { ++ Base::Console().Warning( ++ "PropertyPythonObject::Restore: blocked import of module '%s' during" ++ " document restore. Only modules from FreeCAD or installed addons" ++ " are permitted.\n", ++ moduleName.c_str()); ++ throw Py::ImportError("module not permitted: " + moduleName); ++ } ++ Py::Module mod(PyImport_ImportModule(moduleName.c_str()),true); + if (mod.isNull()) + throw Py::Exception(); +- PyObject* cls = mod.getAttr(reader.getAttribute("class")).ptr(); ++ std::string className = reader.getAttribute("class"); ++ PyObject* cls = mod.getAttr(className).ptr(); + if (!cls) { + std::stringstream s; +- s << "Module " << reader.getAttribute("module") +- << " has no class " << reader.getAttribute("class"); ++ s << "Module " << moduleName << " has no class " << className; + throw Py::AttributeError(s.str()); + } + if (PyType_Check(cls)) { +@@ -371,16 +495,6 @@ + } + load_json = true; + } +- else if (boost::regex_search(start, end, what, pickle)) { +- std::string name = std::string(what[1].first, what[1].second); +- std::string type = std::string(what[2].first, what[2].second); +- Py::Module mod(PyImport_ImportModule(name.c_str()),true); +- if (mod.isNull()) +- throw Py::Exception(); +- this->object = PyObject_CallObject(mod.getAttr(type).ptr(), nullptr); +- load_pickle = true; +- buffer = std::string(what[2].second, end); +- } + else if (reader.hasAttribute("json")) { + load_json = true; + } +@@ -395,8 +509,6 @@ + aboutToSetValue(); + if (load_json) + this->fromString(buffer); +- else if (load_pickle) +- this->loadPickle(buffer); + else if (!load_failed) + Base::Console().Warning("PropertyPythonObject::Restore: unsupported serialisation: %s\n", buffer.c_str()); + restoreObject(reader); +--- a/src/App/PropertyPythonObject.h ++++ b/src/App/PropertyPythonObject.h +@@ -75,7 +75,6 @@ + void restoreObject(Base::XMLReader &reader); + std::string encodeValue(const std::string& str) const; + std::string decodeValue(const std::string& str) const; +- void loadPickle(const std::string& str); + Py::Object object; + }; + diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-2-e2dc6c81.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-2-e2dc6c81.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-2-e2dc6c81.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-2-e2dc6c81.patch 2026-08-25 08:22:20.000000000 +0000 @@ -0,0 +1,49 @@ +From e2dc6c8172673642c6856b8b3a5a6accefb18279 Mon Sep 17 00:00:00 2001 +From: Chris Hennes +Date: Mon, 20 Apr 2026 22:54:05 -0500 +Subject: [PATCH] App: Use __ModDirs__ as the authoritative list of mods to + load from (#29068) + +(cherry picked from commit 454db6ed438c19bdec7659b3ef7bf4213ace34da) +--- + src/App/PropertyPythonObject.cpp | 22 ++++++++++++++-------- + 1 file changed, 14 insertions(+), 8 deletions(-) + +diff --git a/src/App/PropertyPythonObject.cpp b/src/App/PropertyPythonObject.cpp +index b90bafda85..c764f235c3 100644 +--- a/src/App/PropertyPythonObject.cpp ++++ b/src/App/PropertyPythonObject.cpp +@@ -147,16 +147,22 @@ bool isAllowedModule(const std::string& moduleName) + return false; + } + +- std::string home = Application::getHomePath(); +- std::string userData = Application::getUserAppDataDir(); +- std::string resource = Application::getResourceDir(); ++ // Use FreeCAD.__ModDirs__ as the authoritative list of allowed module directories. ++ // This is populated during startup by FreeCADInit.py and includes built-in workbenches, ++ // user addons, and any additional configured module paths. ++ Py::Module freecad(PyImport_ImportModule("FreeCAD"), true); ++ if (!freecad.hasAttr("__ModDirs__")) { ++ throw Py::RuntimeError("FreeCAD.__ModDirs__ not set -- FreeCADInit.py has not run yet"); ++ } ++ Py::List modDirs(freecad.getAttr("__ModDirs__")); + + auto isUnderFreeCAD = [&](const std::string& path) { +- return isUnderDirectory(path, home + "Mod") +- || isUnderDirectory(path, home + "Ext") +- || isUnderDirectory(path, resource + "Mod") +- || isUnderDirectory(path, resource + "Ext") +- || isUnderDirectory(path, userData + "Mod"); ++ for (int i = 0; i < static_cast(modDirs.size()); ++i) { ++ if (isUnderDirectory(path, Py::String(modDirs[i]).as_std_string())) { ++ return true; ++ } ++ } ++ return false; + }; + + // Get the origin (i.e. the file path) from the spec. +-- +2.47.3 + diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-3-526a4f0d.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-3-526a4f0d.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-3-526a4f0d.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-34789-3-526a4f0d.patch 2026-08-25 08:40:04.000000000 +0000 @@ -0,0 +1,88 @@ +From 526a4f0d78bb98ce554f4c1273f01f1f7eb63595 Mon Sep 17 00:00:00 2001 +From: Frank Martinez +Date: Sun, 14 Jun 2026 08:58:01 -0500 +Subject: [PATCH] CVE-2026-34789 part 3/3: Fix 30706: Allow Macro dirs as + sources of PropertyPythonObject - Alt + +This is the third and final part of the CVE-2026-34789 hardening and must be +applied together with CVE-2026-34789-1-81b73925.patch and +CVE-2026-34789-2-e2dc6c81.patch. Do not drop it as "not a security fix": it +repairs a user-visible regression that the first two patches introduce, and +without it the security fix lands on an intermediate upstream state that was +never released. + +The import allowlist installed by parts 1 and 2 only accepts modules whose +origin lies under a directory listed in FreeCAD.__ModDirs__, and the macro +directories are not in that list. A PropertyPythonObject whose class lives in +a module under the user macro directory (App.getUserMacroDir(True)), the legacy +macro directory (App.getUserMacroDir(False)) or FreeCAD.getHomePath()+"Macro" +is therefore refused on restore: the property is restored as None and the only +feedback is a "blocked import of module ..." warning in the report view, so +opening and re-saving such a document silently discards data. This patch +exports those three directories as FreeCAD.__MacroDirs__ from FreeCADInit.py +and adds them to the allowlist consulted by isAllowedModule(). + +Fixes upstream issue 30706. + +(cherry picked from commit d87781b80aada2c17d757de04b59b58266d227e9) + +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/526a4f0d78bb98ce554f4c1273f01f1f7eb63595 +Applied-Upstream: 1.1.2, https://github.com/FreeCAD/FreeCAD/commit/526a4f0d78bb98ce554f4c1273f01f1f7eb63595 +Last-Update: 2026-08-25 +Note: Byte-preserved cherry-pick; the diff bodies are unmodified. The Subject + line has been prefixed with "CVE-2026-34789 part 3/3:" so the patch cannot be + mistaken for an unrelated bug fix, and the paragraphs above were added to the + commit message; the upstream subject text itself is otherwise unchanged. The + only edit to the diff is the retargeting of the two @@ hunk offsets, because + 1.0.0 has fewer preceding lines in both files: + src/App/FreeCADInit.py 172 -> 150 and src/App/PropertyPythonObject.cpp + 150 -> 151 (the latter counted against the state produced by parts 1 and 2). + Every context line and every added line is identical to upstream, including + the CRLF line endings both files carry, so "git show + 526a4f0d78bb98ce554f4c1273f01f1f7eb63595" yields the same diff bodies. The + hunks contain no C++20 construct, so the C++17 substitutions that part 1 + needed (std::replace for std::ranges::replace, rfind(p, 0) == 0 for + starts_with) have no counterpart here. The upstream commit contains no tests. +--- + src/App/FreeCADInit.py | 1 + + src/App/PropertyPythonObject.cpp | 12 +++++++----- + 2 files changed, 8 insertions(+), 5 deletions(-) + +diff --git a/src/App/FreeCADInit.py b/src/App/FreeCADInit.py +index 657dfed472..c060afd060 100644 +--- a/src/App/FreeCADInit.py ++++ b/src/App/FreeCADInit.py +@@ -150,6 +150,7 @@ def InitApplications(): + + # to have all the module-paths available in FreeCADGuiInit.py: + FreeCAD.__ModDirs__ = list(ModDict.values()) ++ FreeCAD.__MacroDirs__ = list({os.path.realpath(MacroDir), os.path.realpath(MacroStd), SystemWideMacroDir}) + + # this allows importing with: + # from FreeCAD.Module import package +diff --git a/src/App/PropertyPythonObject.cpp b/src/App/PropertyPythonObject.cpp +index c764f235c3..a6a1c6e6e4 100644 +--- a/src/App/PropertyPythonObject.cpp ++++ b/src/App/PropertyPythonObject.cpp +@@ -151,14 +151,16 @@ bool isAllowedModule(const std::string& moduleName) + // This is populated during startup by FreeCADInit.py and includes built-in workbenches, + // user addons, and any additional configured module paths. + Py::Module freecad(PyImport_ImportModule("FreeCAD"), true); +- if (!freecad.hasAttr("__ModDirs__")) { +- throw Py::RuntimeError("FreeCAD.__ModDirs__ not set -- FreeCADInit.py has not run yet"); ++ if (!freecad.hasAttr("__ModDirs__") or !freecad.hasAttr("__MacroDirs__")) { ++ throw Py::RuntimeError("FreeCAD.__ModDirs__ or FreeCAD.__MacroDirs__ not set -- FreeCADInit.py has not run yet"); + } +- Py::List modDirs(freecad.getAttr("__ModDirs__")); ++ Py::List allowedDirs(freecad.getAttr("__ModDirs__")); ++ allowedDirs.extend(freecad.getAttr("__MacroDirs__")); ++ const int allowedDirsSize = static_cast(allowedDirs.size()); + + auto isUnderFreeCAD = [&](const std::string& path) { +- for (int i = 0; i < static_cast(modDirs.size()); ++i) { +- if (isUnderDirectory(path, Py::String(modDirs[i]).as_std_string())) { ++ for (int i = 0; i < allowedDirsSize; ++i) { ++ if (isUnderDirectory(path, Py::String(allowedDirs[i]).as_std_string())) { + return true; + } + } diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-73233.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-73233.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-73233.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-73233.patch 2026-08-25 08:39:54.000000000 +0000 @@ -0,0 +1,124 @@ +Description: CVE-2026-73233: FEM: Switch to direct C++ set of text values + The FEM Displacement Constraint task dialog interpolated the user-supplied + displacement formula strings into a Python command string that was then + executed via Gui::Command::doCommand(). The escaping helpers + (get_xFormula()/get_yFormula()/get_zFormula()) only escaped double quotes and + left backslashes untouched, so a formula ending in a backslash (or containing + \" sequences) escapes the generated Python string literal and injects + arbitrary Python that runs with FreeCAD's privileges as soon as the user + accepts the dialog. + . + This drops the broken escaping and stops round-tripping the free-form text + through generated Python: the three *DisplacementFormula properties are now + set directly on the C++ object, which cannot be escaped out of. +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/3f60d202a8246958232e2fbc74ba38a83483b74e +Author: Chris Hennes +Applied-Upstream: 1.1.2, https://github.com/FreeCAD/FreeCAD/commit/3f60d202a8246958232e2fbc74ba38a83483b74e +Bug-Debian: https://bugs.debian.org/1144349 +Last-Update: 2026-08-25 +Note: Backported to 1.0.0. The upstream commit does not apply textually + because src/Mod/Fem/Gui/TaskFemConstraintDisplacement.cpp was reformatted + wholesale after 1.0.0 (upstream 25c3ba7338 "All: Reformat according to new + standard") and because 1.0.0 still writes QString::fromLatin1("...") where + 1.1.x uses QStringLiteral("..."). The change is therefore re-applied by hand + against 1.0.0's formatting; the content is semantically identical to + upstream, with no dropped hunks, though it is not hunk for hunk: upstream + 3f60d202a8 has 3 unified hunks, this backport has 2, because upstream's + hunk 2 (adding the constraint pointer) and hunk 3 (the doCommand + replacements) are adjacent in 1.0.0's layout and merge into one hunk under + diff's context overlap: + . + * get_xFormula()/get_yFormula()/get_zFormula() return the line-edit text + verbatim instead of quote-escaping it; + * TaskDlgFemConstraintDisplacement::accept() obtains the + Fem::ConstraintDisplacement* (the header is already included in 1.0.0) + and calls xDisplacementFormula.setValue() / yDisplacementFormula / + zDisplacementFormula directly instead of issuing doCommand(). + . + Cosmetic divergences from upstream, content otherwise unchanged: + . + * upstream declares the constraint pointer as 'auto* constraint = ...' + on one line; this backport spells out the type as an explicit + two-line 'Fem::ConstraintDisplacement* constraint = ...' declaration + instead of using 'auto*'. + . + The Gui::Command/doCommand API is unchanged between 1.0.0 and 1.1.2, and the + direct-property-set idiom used here is already the established idiom in 1.0.0 + (e.g. src/Mod/Fem/Gui/TaskFemConstraintHeatflux.cpp, + TaskFemConstraintBearing.cpp), so no new API is pulled in. The remaining + doCommand() calls are left alone: they interpolate only + Quantity::getSafeUserString() output and hard-coded True/False, which are not + attacker-controlled free-form text. The upstream commit carries no tests. + . + Behaviour change to be aware of: the formula assignments no longer appear in + the Python console / macro recording, exactly as upstream. +--- +--- a/src/Mod/Fem/Gui/TaskFemConstraintDisplacement.cpp ++++ b/src/Mod/Fem/Gui/TaskFemConstraintDisplacement.cpp +@@ -408,23 +408,17 @@ + + std::string TaskFemConstraintDisplacement::get_xFormula() const + { +- QString xFormula = ui->DisplacementXFormulaLE->text(); +- xFormula.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")); +- return xFormula.toStdString(); ++ return ui->DisplacementXFormulaLE->text().toStdString(); + } + + std::string TaskFemConstraintDisplacement::get_yFormula() const + { +- QString yFormula = ui->DisplacementYFormulaLE->text(); +- yFormula.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")); +- return yFormula.toStdString(); ++ return ui->DisplacementYFormulaLE->text().toStdString(); + } + + std::string TaskFemConstraintDisplacement::get_zFormula() const + { +- QString zFormula = ui->DisplacementZFormulaLE->text(); +- zFormula.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")); +- return zFormula.toStdString(); ++ return ui->DisplacementZFormulaLE->text().toStdString(); + } + + bool TaskFemConstraintDisplacement::get_dispxfree() const +@@ -518,32 +512,27 @@ + std::string name = ConstraintView->getObject()->getNameInDocument(); + const TaskFemConstraintDisplacement* parameterDisplacement = + static_cast(parameter); ++ Fem::ConstraintDisplacement* constraint = ++ static_cast(ConstraintView->getObject()); + + try { + Gui::Command::doCommand(Gui::Command::Doc, + "App.ActiveDocument.%s.xDisplacement = \"%s\"", + name.c_str(), + parameterDisplacement->get_spinxDisplacement().c_str()); +- Gui::Command::doCommand(Gui::Command::Doc, +- "App.ActiveDocument.%s.xDisplacementFormula = \"%s\"", +- name.c_str(), +- parameterDisplacement->get_xFormula().c_str()); ++ // Formula fields are free-form user text and must never be interpolated into a ++ // Python command; set the property directly to avoid code injection. ++ constraint->xDisplacementFormula.setValue(parameterDisplacement->get_xFormula()); + Gui::Command::doCommand(Gui::Command::Doc, + "App.ActiveDocument.%s.yDisplacement = \"%s\"", + name.c_str(), + parameterDisplacement->get_spinyDisplacement().c_str()); +- Gui::Command::doCommand(Gui::Command::Doc, +- "App.ActiveDocument.%s.yDisplacementFormula = \"%s\"", +- name.c_str(), +- parameterDisplacement->get_yFormula().c_str()); ++ constraint->yDisplacementFormula.setValue(parameterDisplacement->get_yFormula()); + Gui::Command::doCommand(Gui::Command::Doc, + "App.ActiveDocument.%s.zDisplacement = \"%s\"", + name.c_str(), + parameterDisplacement->get_spinzDisplacement().c_str()); +- Gui::Command::doCommand(Gui::Command::Doc, +- "App.ActiveDocument.%s.zDisplacementFormula = \"%s\"", +- name.c_str(), +- parameterDisplacement->get_zFormula().c_str()); ++ constraint->zDisplacementFormula.setValue(parameterDisplacement->get_zFormula()); + Gui::Command::doCommand(Gui::Command::Doc, + "App.ActiveDocument.%s.xRotation = \"%s\"", + name.c_str(), diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-73234.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-73234.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-73234.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-73234.patch 2026-08-25 08:54:43.000000000 +0000 @@ -0,0 +1,227 @@ +Description: CVE-2026-73234: prevent directory traversal in included file + PropertyFileIncluded::Restore() took the "file" and "data" attributes of a + element straight from Document.xml and concatenated them with + the document transient path without any validation. Directory components, + absolute paths and ".." sequences were all accepted, so a crafted .FCStd + archive could make FreeCAD write attacker-controlled content anywhere the + user running FreeCAD can write. + . + Save() always emits a bare basename (via FileInfo::fileName()), so any name + carrying a directory separator, an absolute path or a "."/".." reference is + by definition malicious. Reject those with a Base::FileException. +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/f19b18b7d93729a29a90e96e0ae192b5d054b86d +Author: Chris Hennes +Applied-Upstream: 1.1.2, https://github.com/FreeCAD/FreeCAD/commit/f19b18b7d93729a29a90e96e0ae192b5d054b86d +Last-Update: 2026-08-25 +Note: Backported to 1.0.0. The src/App/PropertyFile.cpp changes are byte-identical + to upstream; only the surrounding context differs, because 1.0.0 still uses the + non-template Base::XMLReader::getAttribute("file") and the older + "Base::XMLReader &reader" formatting, and has three (not two) blank lines after + the "using namespace std;" block. The tests/src/App/CMakeLists.txt hunk had to be + hand-recreated: 1.0.0 registers the App unit tests through + "target_sources(Tests_run PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/)" rather than + upstream's "add_executable(App_tests_run ...)" list, so the new source file is + added in that form instead. The new test file tests/src/App/PropertyFile.cpp is + taken from upstream with one deliberate change; every API it uses + (tests::initApplication() from InitApplication.h, App::VarSet, + PropertyContainer::addDynamicProperty(), Base::XMLReader(const char*, + std::istream&), Document::TransientDir) already exists in 1.0.0. 1.0.0 builds + with CMAKE_CXX_STANDARD 17, so the upstream C++20-only "#include " and + "std::ranges::replace(transientDir, '\\', '/')" in the acceptsPlainBasename + test are dropped/replaced by + "std::replace(transientDir.begin(), transientDir.end(), '\\', '/')", which is + available in C++17 and behaves identically for this call; the upstream + "#include " line is kept unchanged since std::replace needs it too. + No hunks were dropped. +--- +diff --git a/src/App/PropertyFile.cpp b/src/App/PropertyFile.cpp +--- a/src/App/PropertyFile.cpp ++++ b/src/App/PropertyFile.cpp +@@ -42,6 +42,27 @@ + + + ++namespace ++{ ++/** ++ * @brief Check that an embedded file name from a restored document is a plain basename. ++ * ++ * PropertyFileIncluded::Save() always stores basenames (via FileInfo::fileName()), so a ++ * document that carries a name with any directory component, an absolute path, or a ++ * ./.. reference is malicious. ++ * ++ * @param[in] name The file name taken from the document XML. ++ * @return @c true if @p name is a safe basename, @c false if it must be rejected. ++ */ ++bool isPlainFileName(const std::string& name) ++{ ++ if (name == "." || name == "..") { ++ return false; ++ } ++ return Base::FileInfo(name).fileName() == name; ++} ++} // namespace ++ + //************************************************************************** + // PropertyFileIncluded + //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +@@ -395,6 +416,10 @@ + if (reader.hasAttribute("file")) { + string file (reader.getAttribute("file") ); + if (!file.empty()) { ++ if (!isPlainFileName(file)) { ++ throw Base::FileException( ++ "PropertyFileIncluded::Restore(): rejected unsafe embedded file name"); ++ } + // initiate a file read + reader.addFile(file.c_str(),this); + // is in the document transient path +@@ -408,6 +433,10 @@ + else if (reader.hasAttribute("data")) { + string file (reader.getAttribute("data") ); + if (!file.empty()) { ++ if (!isPlainFileName(file)) { ++ throw Base::FileException( ++ "PropertyFileIncluded::Restore(): rejected unsafe embedded file name"); ++ } + // is in the document transient path + aboutToSetValue(); + _cValue = getDocTransientPath() + "/" + file; +diff --git a/tests/src/App/CMakeLists.txt b/tests/src/App/CMakeLists.txt +--- a/tests/src/App/CMakeLists.txt ++++ b/tests/src/App/CMakeLists.txt +@@ -20,6 +20,7 @@ + ${CMAKE_CURRENT_SOURCE_DIR}/MappedName.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Metadata.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ProjectFile.cpp ++ ${CMAKE_CURRENT_SOURCE_DIR}/PropertyFile.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Property.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/PropertyExpressionEngine.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/StringHasher.cpp +diff --git a/tests/src/App/PropertyFile.cpp b/tests/src/App/PropertyFile.cpp +new file mode 100644 +--- /dev/null ++++ b/tests/src/App/PropertyFile.cpp +@@ -0,0 +1,122 @@ ++// SPDX-License-Identifier: LGPL-2.1-or-later ++ ++#include ++ ++#include ++#include ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include "InitApplication.h" ++ ++namespace ++{ ++void restoreFileIncluded(App::PropertyFileIncluded& prop, const std::string& fileIncludedElement) ++{ ++ std::string xml = "\n"; ++ xml += "\n"; ++ xml += fileIncludedElement; ++ xml += "\n\n"; ++ ++ std::stringstream data(xml); ++ Base::XMLReader reader("Document.xml", data); ++ prop.Restore(reader); ++} ++} // namespace ++ ++// Regression tests for GHSA-5vqh-3v38-jw2r: a crafted .FCStd must not be able to escape the ++// document transient directory through the "file" or "data" attributes of a FileIncluded element. ++class PropertyFileIncludedTest: public ::testing::Test ++{ ++protected: ++ static void SetUpTestSuite() ++ { ++ tests::initApplication(); ++ } ++ void SetUp() override ++ { ++ _doc = App::GetApplication().newDocument("PropertyFileIncludedTest"); ++ _object = _doc->addObject("App::VarSet", "VarSet"); ++ _property = static_cast( ++ _object->addDynamicProperty("App::PropertyFileIncluded", "File") ++ ); ++ } ++ void TearDown() override ++ { ++ App::GetApplication().closeDocument(_doc->getName()); ++ } ++ App::Document* _doc {nullptr}; ++ App::DocumentObject* _object {nullptr}; ++ App::PropertyFileIncluded* _property {nullptr}; ++}; ++ ++TEST_F(PropertyFileIncludedTest, rejectsParentTraversalInFileAttribute) ++{ ++ EXPECT_THROW( ++ restoreFileIncluded(*_property, ""), ++ Base::FileException ++ ); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, rejectsBackslashTraversalInFileAttribute) ++{ ++ EXPECT_THROW( ++ restoreFileIncluded(*_property, ""), ++ Base::FileException ++ ); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, rejectsAbsolutePathInFileAttribute) ++{ ++ EXPECT_THROW( ++ restoreFileIncluded(*_property, ""), ++ Base::FileException ++ ); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, rejectsSubdirectoryInFileAttribute) ++{ ++ EXPECT_THROW( ++ restoreFileIncluded(*_property, ""), ++ Base::FileException ++ ); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, rejectsDotDotInFileAttribute) ++{ ++ EXPECT_THROW(restoreFileIncluded(*_property, ""), Base::FileException); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, rejectsParentTraversalInDataAttribute) ++{ ++ EXPECT_THROW( ++ restoreFileIncluded(*_property, ""), ++ Base::FileException ++ ); ++ EXPECT_TRUE(std::string(_property->getValue()).empty()); ++} ++ ++TEST_F(PropertyFileIncludedTest, acceptsPlainBasename) ++{ ++ // A legitimate basename (as always produced by Save()) must still restore and resolve inside ++ // the document transient directory. ++ EXPECT_NO_THROW(restoreFileIncluded(*_property, "")); ++ ++ // getDocTransientPath() normalizes backslashes to forward slashes, so normalize the expected ++ // transient directory the same way before comparing. ++ std::string transientDir = _doc->TransientDir.getValue(); ++ std::replace(transientDir.begin(), transientDir.end(), '\\', '/'); ++ EXPECT_EQ(std::string(_property->getValue()), transientDir + "/PartShape.brp"); ++} diff -Nru freecad-1.0.0+dfsg/debian/patches/CVE-2026-73235.patch freecad-1.0.0+dfsg/debian/patches/CVE-2026-73235.patch --- freecad-1.0.0+dfsg/debian/patches/CVE-2026-73235.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/CVE-2026-73235.patch 2026-08-25 08:42:00.000000000 +0000 @@ -0,0 +1,48 @@ +Description: CVE-2026-73235: harden FCStd Reader against XXE + Base::XMLReader parses Document.xml from a .FCStd archive using a Xerces + SAX2 XMLReader without disabling external entity resolution or external DTD + loading. A crafted .FCStd whose Document.xml declares an external entity + (e.g. a DOCTYPE with a SYSTEM identifier using a "file:" or "http:" URI) can + therefore make Xerces read an arbitrary local file, or issue a + server-side HTTP(S) request, and have the resolved content delivered to + XMLReader::characters(), disclosing local files or enabling SSRF as soon as + the document is opened. + . + This sets fgXercesDisableDefaultEntityResolution and disables + fgXercesLoadExternalDTD on the parser so external entities and external DTDs + are no longer resolved. +Origin: backport, https://github.com/FreeCAD/FreeCAD/commit/7d1b8f5806db578db99feb348e55a6b0eaff7c73 +Author: Chris Hennes +Applied-Upstream: 1.1.2, https://github.com/FreeCAD/FreeCAD/commit/7d1b8f5806db578db99feb348e55a6b0eaff7c73 +Bug-Debian: https://bugs.debian.org/1144349 +Last-Update: 2026-08-25 +Note: Backported to 1.0.0. The two parser->setFeature() calls are + byte-identical to upstream; only the hunk context differs, because 1.0.0 + guards the include block with "#ifndef _PreComp_" / "#include " / + XMLReaderFactory.hpp, whereas upstream's tree at this commit has + "#include " / XMLReaderFactory.hpp / Attributes.hpp with no PCH + guard, and the @@ line ranges were retargeted accordingly. The diff hunk + content lines below use CRLF line endings, matching src/Base/Reader.cpp on + disk; only the diff/hunk marker lines (diff --git, ---, +++, @@) use LF. + No hunks were dropped. +--- +diff --git a/src/Base/Reader.cpp b/src/Base/Reader.cpp +--- a/src/Base/Reader.cpp ++++ b/src/Base/Reader.cpp +@@ -26,6 +26,7 @@ + #ifndef _PreComp_ + #include + #include ++#include + #endif + + #include +@@ -75,6 +76,8 @@ Base::XMLReader::XMLReader(const char* FileName, std::istream& str) + parser->setContentHandler(this); + parser->setLexicalHandler(this); + parser->setErrorHandler(this); ++ parser->setFeature(XMLUni::fgXercesDisableDefaultEntityResolution, true); ++ parser->setFeature(XMLUni::fgXercesLoadExternalDTD, false); + + try { + StdInputSource file(str, _File.filePath().c_str()); diff -Nru freecad-1.0.0+dfsg/debian/patches/series freecad-1.0.0+dfsg/debian/patches/series --- freecad-1.0.0+dfsg/debian/patches/series 2026-06-09 08:29:01.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/series 2026-08-25 08:55:18.000000000 +0000 @@ -13,3 +13,10 @@ 1110-GL_MULTISAMPLE.patch 2080-force-xcb-on-wayland.patch 1100-cam-fanuc-post-fix.patch +CVE-2026-34398-CVE-2026-34399.patch +CVE-2026-34789-1-81b73925.patch +CVE-2026-34789-2-e2dc6c81.patch +CVE-2026-34789-3-526a4f0d.patch +CVE-2026-73233.patch +CVE-2026-73235.patch +CVE-2026-73234.patch