desktop/source/lib/init.cxx                    |    3 
 embeddedobj/source/commonembedding/miscobj.cxx |    9 +
 include/tools/hostfilter.hxx                   |   10 ++
 sc/source/core/tool/webservicelink.cxx         |    6 +
 sc/source/ui/dataprovider/dataprovider.cxx     |    7 +
 sc/source/ui/docshell/arealink.cxx             |    7 +
 sc/source/ui/docshell/externalrefmgr.cxx       |    6 +
 sc/source/ui/docshell/tablink.cxx              |    7 +
 sd/source/ui/dlg/tpaction.cxx                  |   11 ++
 sfx2/source/appl/appopen.cxx                   |    7 +
 sfx2/source/appl/fileobj.cxx                   |    7 +
 sw/source/core/docnode/section.cxx             |    7 +
 tools/CppunitTest_tools_test.mk                |    1 
 tools/qa/cppunit/test_hostfilter.cxx           |  120 +++++++++++++++++++++++++
 tools/source/inet/hostfilter.cxx               |   68 ++++++++++++++
 15 files changed, 275 insertions(+), 1 deletion(-)

New commits:
commit f0fe37673aa6a041ccb2ac04dadcbfa9f2ac1ee5
Author:     Caolán <[email protected]>
AuthorDate: Wed Feb 25 09:06:22 2026 +0000
Commit:     Miklos Vajna <[email protected]>
CommitDate: Thu Mar 5 09:06:22 2026 +0100

    Add an advisory setAllowedExtRefPaths
    
    Not intended to apply a strict jail, but an advisory list of paths
    that are meaningful to allow links to and update from.
    
    Change-Id: I46742f05ee87194aea196e95f0a7fbb762634f5f
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/200866
    Reviewed-by: Miklos Vajna <[email protected]>
    Tested-by: Jenkins CollaboraOffice <[email protected]>

diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx
index f0ba6592bc90..d44fbb0a3750 100644
--- a/desktop/source/lib/init.cxx
+++ b/desktop/source/lib/init.cxx
@@ -8501,6 +8501,9 @@ static int lo_initialize(LibreOfficeKit* pThis, const 
char* pAppPath, const char
     if (const char* pHostExemptVerifyHost = 
::getenv("LOK_HOST_ALLOWLIST_EXEMPT_VERIFY_HOST"))
         
HostFilter::setAllowedHostsExemptVerifyHost(strncmp(pHostExemptVerifyHost,"1", 
1) == 0);
 
+    if (const char* pExtRefPaths = ::getenv("LOK_ALLOWED_EXTREF_PATHS"))
+        HostFilter::setAllowedExtRefPaths(pExtRefPaths);
+
     // What stage are we at ?
     if (pThis == nullptr)
     {
diff --git a/embeddedobj/source/commonembedding/miscobj.cxx 
b/embeddedobj/source/commonembedding/miscobj.cxx
index 2ea0dc59a07e..c1e66224db9a 100644
--- a/embeddedobj/source/commonembedding/miscobj.cxx
+++ b/embeddedobj/source/commonembedding/miscobj.cxx
@@ -28,6 +28,7 @@
 #include <com/sun/star/beans/NamedValue.hpp>
 
 #include <com/sun/star/ucb/SimpleFileAccess.hpp>
+#include <com/sun/star/io/IOException.hpp>
 #include <com/sun/star/io/TempFile.hpp>
 #include <comphelper/multicontainer2.hxx>
 #include <comphelper/storagehelper.hxx>
@@ -46,6 +47,7 @@
 #include <comphelper/diagnose_ex.hxx>
 #include <cppuhelper/supportsservice.hxx>
 #include <comphelper/sequenceashashmap.hxx>
+#include <tools/hostfilter.hxx>
 
 #include "persistence.hxx"
 
@@ -185,6 +187,13 @@ void OCommonEmbeddedObject::LinkInit_Impl(
 
     OSL_ENSURE( m_aLinkURL.getLength() && m_aLinkFilterName.getLength(), 
"Filter and URL must be provided!" );
 
+    if (HostFilter::isFileUrlForbidden(m_aLinkURL))
+    {
+        SAL_WARN("embeddedobj.common", "LinkInit_Impl: blocked file path: \"" 
<< m_aLinkURL << "\"");
+        m_aLinkURL.clear();
+        throw io::IOException(u"blocked external reference path"_ustr);
+    }
+
     m_bReadOnly = true;
     if ( m_aLinkFilterName.getLength() )
     {
diff --git a/include/tools/hostfilter.hxx b/include/tools/hostfilter.hxx
index 8e992b407bbb..423968f08cc1 100644
--- a/include/tools/hostfilter.hxx
+++ b/include/tools/hostfilter.hxx
@@ -27,6 +27,16 @@ public:
     static void setExemptVerifyHost(const OUString& rExemptVerifyHost);
 
     static bool isExemptVerifyHost(const std::u16string_view rHost);
+
+    /// A colon-separated list of directory paths that file:// external
+    /// references are allowed to reach. An empty string means:
+    /// "block all file URLs"
+    static void setAllowedExtRefPaths(const char* sPaths);
+
+    /// Return true when rFileUrl is a file:// URL that is outside any
+    /// directory registered with setAllowedExtRefPaths. Non-file URLs
+    /// are always allowed.
+    static bool isFileUrlForbidden(const OUString& rFileUrl);
 };
 
 #endif
diff --git a/sc/source/core/tool/webservicelink.cxx 
b/sc/source/core/tool/webservicelink.cxx
index c30f34300edf..ba3e6f162f83 100644
--- a/sc/source/core/tool/webservicelink.cxx
+++ b/sc/source/core/tool/webservicelink.cxx
@@ -48,6 +48,12 @@ sfx2::SvBaseLink::UpdateResult 
ScWebServiceLink::DataChanged(const OUString&, co
         return ERROR_GENERAL;
     }
 
+    if (HostFilter::isFileUrlForbidden(aURL))
+    {
+        SAL_WARN("sc.ui", "ScWebServiceLink::DataChanged: blocked file path: 
\"" << aURL << "\"");
+        return ERROR_GENERAL;
+    }
+
     css::uno::Reference<css::ucb::XSimpleFileAccess3> xFileAccess
         = 
css::ucb::SimpleFileAccess::create(comphelper::getProcessComponentContext());
     if (!xFileAccess.is())
diff --git a/sc/source/ui/dataprovider/dataprovider.cxx 
b/sc/source/ui/dataprovider/dataprovider.cxx
index dced70cca0bd..9b8149ea3b22 100644
--- a/sc/source/ui/dataprovider/dataprovider.cxx
+++ b/sc/source/ui/dataprovider/dataprovider.cxx
@@ -17,6 +17,7 @@
 #include <unotools/charclass.hxx>
 #include <tools/stream.hxx>
 #include <comphelper/processfactory.hxx>
+#include <tools/hostfilter.hxx>
 
 #include "htmldataprovider.hxx"
 #include "xmldataprovider.hxx"
@@ -32,6 +33,12 @@ namespace sc {
 
 std::unique_ptr<SvStream> DataProvider::FetchStreamFromURL(const OUString& 
rURL, OStringBuffer& rBuffer)
 {
+    if (HostFilter::isFileUrlForbidden(rURL))
+    {
+        SAL_WARN("sc.ui", "DataProvider::FetchStreamFromURL: blocked file 
path: \"" << rURL << "\"");
+        return nullptr;
+    }
+
     try
     {
         uno::Reference< ucb::XSimpleFileAccess3 > xFileAccess = 
ucb::SimpleFileAccess::create( comphelper::getProcessComponentContext() );
diff --git a/sc/source/ui/docshell/arealink.cxx 
b/sc/source/ui/docshell/arealink.cxx
index 35785cda3724..a70c23f598aa 100644
--- a/sc/source/ui/docshell/arealink.cxx
+++ b/sc/source/ui/docshell/arealink.cxx
@@ -45,6 +45,7 @@
 
 #include <scabstdlg.hxx>
 #include <clipparam.hxx>
+#include <tools/hostfilter.hxx>
 
 
 ScAreaLink::ScAreaLink( ScDocShell* pShell, OUString aFile,
@@ -232,6 +233,12 @@ bool ScAreaLink::Refresh( const OUString& rNewFile, const 
OUString& rNewFilter,
     OUString aNewUrl( ScGlobal::GetAbsDocName( rNewFile, m_pDocSh ) );
     bool bNewUrlName = (aNewUrl != aFileName);
 
+    if (HostFilter::isFileUrlForbidden(aNewUrl))
+    {
+        SAL_WARN("sc.ui", "ScAreaLink::Refresh: blocked file path: \"" << 
aNewUrl << "\"");
+        return false;
+    }
+
     std::shared_ptr<const SfxFilter> pFilter = 
m_pDocSh->GetFactory().GetFilterContainer()->GetFilter4FilterName(rNewFilter);
     if (!pFilter)
         return false;
diff --git a/sc/source/ui/docshell/externalrefmgr.cxx 
b/sc/source/ui/docshell/externalrefmgr.cxx
index a5b7fd9e6f49..2733956d716a 100644
--- a/sc/source/ui/docshell/externalrefmgr.cxx
+++ b/sc/source/ui/docshell/externalrefmgr.cxx
@@ -2552,6 +2552,12 @@ SfxObjectShellRef 
ScExternalRefManager::loadSrcDocument(sal_uInt16 nFileId, OUSt
         return nullptr;
     }
 
+    if (HostFilter::isFileUrlForbidden(aFile))
+    {
+        SAL_WARN( "sc.ui", "ScExternalRefManager::loadSrcDocument: blocked 
access to local file: \"" << aFile << "\"");
+        return nullptr;
+    }
+
     OUString aOptions = pFileData->maFilterOptions;
     if ( !pFileData->maFilterName.isEmpty() )
         rFilter = pFileData->maFilterName;      // don't overwrite stored 
filter with guessed filter
diff --git a/sc/source/ui/docshell/tablink.cxx 
b/sc/source/ui/docshell/tablink.cxx
index 09a5f674089e..fd55a465418f 100644
--- a/sc/source/ui/docshell/tablink.cxx
+++ b/sc/source/ui/docshell/tablink.cxx
@@ -49,6 +49,7 @@
 #include <global.hxx>
 #include <hints.hxx>
 #include <dociter.hxx>
+#include <tools/hostfilter.hxx>
 #include <formula/opcode.hxx>
 #include <formulaiter.hxx>
 #include <tokenarray.hxx>
@@ -151,6 +152,12 @@ bool ScTableLink::Refresh(const OUString& rNewFile, const 
OUString& rNewFilter,
     OUString aNewUrl = ScGlobal::GetAbsDocName(rNewFile, pImpl->m_pDocSh);
     bool bNewUrlName = aFileName != aNewUrl;
 
+    if (HostFilter::isFileUrlForbidden(aNewUrl))
+    {
+        SAL_WARN("sc.ui", "ScTableLink::Refresh: blocked file path: \"" << 
aNewUrl << "\"");
+        return false;
+    }
+
     std::shared_ptr<const SfxFilter> pFilter = 
pImpl->m_pDocSh->GetFactory().GetFilterContainer()->GetFilter4FilterName(rNewFilter);
     if (!pFilter)
         return false;
diff --git a/sd/source/ui/dlg/tpaction.cxx b/sd/source/ui/dlg/tpaction.cxx
index 8f3db490d4eb..125acd7a909f 100644
--- a/sd/source/ui/dlg/tpaction.cxx
+++ b/sd/source/ui/dlg/tpaction.cxx
@@ -32,6 +32,7 @@
 #include <sfx2/strings.hrc>
 #include <o3tl/safeint.hxx>
 #include <tools/debug.hxx>
+#include <tools/hostfilter.hxx>
 #include <sfx2/app.hxx>
 #include <svx/svdograf.hxx>
 #include <svl/stritem.hxx>
@@ -775,7 +776,15 @@ OUString SdTPAction::GetEditText( bool bFullDocDestination 
)
         aBaseURL = mpDoc->GetDocSh()->GetMedium()->GetBaseURL();
 
     if( !aStr.isEmpty() && aURL.GetProtocol() == INetProtocol::NotValid )
-        aURL = INetURLObject( ::URIHelper::SmartRel2Abs( 
INetURLObject(aBaseURL), aStr, URIHelper::GetMaybeFileHdl() ) );
+    {
+        INetURLObject aResult( ::URIHelper::SmartRel2Abs( 
INetURLObject(aBaseURL), aStr, URIHelper::GetMaybeFileHdl() ) );
+
+        OUString sCandidate = 
aResult.GetMainURL(INetURLObject::DecodeMechanism::NONE);
+        if (!HostFilter::isFileUrlForbidden(sCandidate))
+            aURL = aResult;
+        else
+            SAL_WARN( "sd", "SdTPAction::FillItemSet: blocked display of local 
file: \"" << sCandidate << "\"");
+    }
 
     // get adjusted file name
     aStr = aURL.GetMainURL( INetURLObject::DecodeMechanism::NONE );
diff --git a/sfx2/source/appl/appopen.cxx b/sfx2/source/appl/appopen.cxx
index 6e67f2eaa880..eaa09e3a86e6 100644
--- a/sfx2/source/appl/appopen.cxx
+++ b/sfx2/source/appl/appopen.cxx
@@ -86,6 +86,7 @@
 #include <sfx2/sfxsids.hrc>
 #include <o3tl/string_view.hxx>
 #include <openuriexternally.hxx>
+#include <tools/hostfilter.hxx>
 
 #include <officecfg/Office/ProtocolHandler.hxx>
 #include <officecfg/Office/Security.hxx>
@@ -785,6 +786,12 @@ void SfxApplication::OpenDocExec_Impl( SfxRequest& rReq )
     assert(pFileName && "SID_FILE_NAME is required");
     OUString aFileName = pFileName->GetValue();
 
+    if (HostFilter::isFileUrlForbidden(aFileName))
+    {
+        SAL_WARN("sfx.appl", "SID_OPENDOC: blocked file path: \"" << aFileName 
<< "\"");
+        return;
+    }
+
     OUString aReferer;
     const SfxStringItem* pRefererItem = 
rReq.GetArg<SfxStringItem>(SID_REFERER);
     if ( pRefererItem )
diff --git a/sfx2/source/appl/fileobj.cxx b/sfx2/source/appl/fileobj.cxx
index 2d4aa100d9d3..9b6e72c15198 100644
--- a/sfx2/source/appl/fileobj.cxx
+++ b/sfx2/source/appl/fileobj.cxx
@@ -36,6 +36,7 @@
 #include <sfx2/opengrf.hxx>
 #include <sfx2/sfxresid.hxx>
 #include <sfx2/objsh.hxx>
+#include <tools/hostfilter.hxx>
 #include "fileobj.hxx"
 #include <sfx2/strings.hrc>
 #include <vcl/svapp.hxx>
@@ -156,6 +157,12 @@ bool SvFileObject::LoadFile_Impl()
     if( bWaitForData || !bLoadAgain || xMed.is() )
         return false;
 
+    if (HostFilter::isFileUrlForbidden(sFileNm))
+    {
+        SAL_WARN("sfx.appl", "SvFileObject::LoadFile_Impl: blocked file path: 
\"" << sFileNm << "\"");
+        return false;
+    }
+
     // at the moment on the current DocShell
     xMed = new SfxMedium( sFileNm, sReferer, StreamMode::STD_READ );
     SvLinkSource::StreamToLoadFrom aStreamToLoadFrom =
diff --git a/sw/source/core/docnode/section.cxx 
b/sw/source/core/docnode/section.cxx
index 5b6dff97b067..41c855228a0e 100644
--- a/sw/source/core/docnode/section.cxx
+++ b/sw/source/core/docnode/section.cxx
@@ -63,6 +63,7 @@
 #include <unosection.hxx>
 #include <calbck.hxx>
 #include <fmtclds.hxx>
+#include <tools/hostfilter.hxx>
 #include <algorithm>
 #include <utility>
 #include "ndsect.hxx"
@@ -1165,6 +1166,12 @@ static void lcl_UpdateLinksInSect( const SwBaseLink& 
rUpdLnk, SwSectionNode& rSe
             sfx2::LinkManager::GetDisplayNames( this, nullptr, &sFileName,
                                                     &sRange, &sFilter );
 
+            if (HostFilter::isFileUrlForbidden(sFileName))
+            {
+                SAL_WARN("sw.core", "SwIntrnlSectRefLink::DataChanged: blocked 
file path: \"" << sFileName << "\"");
+                break;
+            }
+
             RedlineFlags eOldRedlineFlags = RedlineFlags::NONE;
             SfxObjectShellRef xDocSh;
             SfxObjectShellLock xLockRef;
diff --git a/tools/CppunitTest_tools_test.mk b/tools/CppunitTest_tools_test.mk
index c2087bcde594..f8bda935eff9 100644
--- a/tools/CppunitTest_tools_test.mk
+++ b/tools/CppunitTest_tools_test.mk
@@ -38,6 +38,7 @@ $(eval $(call 
gb_CppunitTest_add_exception_objects,tools_test, \
     tools/qa/cppunit/test_cpu_runtime_detection_SSE2 \
     tools/qa/cppunit/test_cpu_runtime_detection_SSSE3 \
     tools/qa/cppunit/test_Wildcard \
+    tools/qa/cppunit/test_hostfilter \
     tools/qa/cppunit/test_zcodec \
 ))
 
diff --git a/tools/qa/cppunit/test_hostfilter.cxx 
b/tools/qa/cppunit/test_hostfilter.cxx
new file mode 100644
index 000000000000..3a2a34e5bfa1
--- /dev/null
+++ b/tools/qa/cppunit/test_hostfilter.cxx
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; 
fill-column: 100 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <sal/config.h>
+
+#include <cppunit/TestAssert.h>
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+#include <tools/hostfilter.hxx>
+
+namespace
+{
+class TestHostFilter : public CppUnit::TestFixture
+{
+public:
+    void testEmptyAllowlist();
+    void testNonFileUrl();
+    void testAllowedPath();
+    void testSiblingDirectoryNotAllowed();
+    void testParentDirectoryNotAllowed();
+    void testMultiplePaths();
+    void testEncodedFileUrl();
+    void testParentDirectorySegments();
+
+    CPPUNIT_TEST_SUITE(TestHostFilter);
+    CPPUNIT_TEST(testEmptyAllowlist);
+    CPPUNIT_TEST(testNonFileUrl);
+    CPPUNIT_TEST(testAllowedPath);
+    CPPUNIT_TEST(testSiblingDirectoryNotAllowed);
+    CPPUNIT_TEST(testParentDirectoryNotAllowed);
+    CPPUNIT_TEST(testMultiplePaths);
+    CPPUNIT_TEST(testEncodedFileUrl);
+    CPPUNIT_TEST(testParentDirectorySegments);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+void TestHostFilter::testEmptyAllowlist()
+{
+    // empty string means "block all file URLs"
+    HostFilter::setAllowedExtRefPaths("");
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///home/user/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/doc.ods"_ustr));
+}
+
+void TestHostFilter::testNonFileUrl()
+{
+    HostFilter::setAllowedExtRefPaths("");
+    // non-file URLs are not subject to the extref path allowlist
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"http://example.com/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"https://example.com/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"ftp://example.com/doc.ods"_ustr));
+}
+
+void TestHostFilter::testAllowedPath()
+{
+    HostFilter::setAllowedExtRefPaths("/tmp/docs");
+    // file inside allowed directory is permitted
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/docs/sheet.ods"_ustr));
+    // file in a subdirectory is permitted
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/docs/sub/sheet.ods"_ustr));
+    // file outside allowed directory is blocked
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///home/user/doc.ods"_ustr));
+}
+
+void TestHostFilter::testSiblingDirectoryNotAllowed()
+{
+    // /tmp/user must not match /tmp/username
+    HostFilter::setAllowedExtRefPaths("/tmp/user");
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/user/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/username/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/usera/doc.ods"_ustr));
+}
+
+void TestHostFilter::testParentDirectoryNotAllowed()
+{
+    HostFilter::setAllowedExtRefPaths("/tmp/docs");
+    // parent directory is not allowed
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/secret.ods"_ustr));
+    // unrelated path is not allowed
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///var/data/doc.ods"_ustr));
+}
+
+void TestHostFilter::testMultiplePaths()
+{
+    // colon-separated list of allowed paths
+    HostFilter::setAllowedExtRefPaths("/tmp/a:/tmp/b");
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/a/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/b/doc.ods"_ustr));
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/c/doc.ods"_ustr));
+}
+
+void TestHostFilter::testEncodedFileUrl()
+{
+    HostFilter::setAllowedExtRefPaths("/tmp/my docs");
+    // percent-encoded space in URL should match allowed path with space
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/my%20docs/sheet.ods"_ustr));
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/my%20docs/sub/sheet.ods"_ustr));
+    // encoded URL outside allowed path is still blocked
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/other%20docs/sheet.ods"_ustr));
+}
+
+void TestHostFilter::testParentDirectorySegments()
+{
+    HostFilter::setAllowedExtRefPaths("/tmp/docs");
+    // .. segments that escape the allowed directory should be blocked
+    
CPPUNIT_ASSERT(HostFilter::isFileUrlForbidden(u"file:///tmp/docs/../other/sheet.ods"_ustr));
+    // .. segments that stay within the allowed directory are permitted
+    
CPPUNIT_ASSERT(!HostFilter::isFileUrlForbidden(u"file:///tmp/docs/sub/../sheet.ods"_ustr));
+}
+
+CPPUNIT_TEST_SUITE_REGISTRATION(TestHostFilter);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/tools/source/inet/hostfilter.cxx b/tools/source/inet/hostfilter.cxx
index 75ade47489af..99ddb750dd4c 100644
--- a/tools/source/inet/hostfilter.cxx
+++ b/tools/source/inet/hostfilter.cxx
@@ -8,7 +8,9 @@
  */
 
 #include <tools/hostfilter.hxx>
+#include <osl/file.hxx>
 #include <regex>
+#include <vector>
 
 static std::regex g_AllowedHostsRegex("");
 static OUString g_ExceptVerifyHost;
@@ -51,4 +53,70 @@ bool HostFilter::isExemptVerifyHost(const 
std::u16string_view rHost)
     return false;
 }
 
+static bool g_AllowedExtRefPathsConfigured = false;
+static std::vector<OUString> g_AllowedExtRefPaths;
+
+void HostFilter::setAllowedExtRefPaths(const char* sPaths)
+{
+    g_AllowedExtRefPathsConfigured = true;
+    g_AllowedExtRefPaths.clear();
+
+    if (!sPaths || sPaths[0] == '
+        return;
+
+    OString sPathList(sPaths);
+    sal_Int32 nIndex = 0;
+    do
+    {
+        OString sPath = sPathList.getToken(0, ':', nIndex);
+        if (sPath.isEmpty())
+            continue;
+
+        OUString aSysPath = OStringToOUString(sPath, RTL_TEXTENCODING_UTF8);
+        OUString aFileUrl;
+        if (osl::FileBase::getFileURLFromSystemPath(aSysPath, aFileUrl) != 
osl::FileBase::E_None)
+            continue;
+
+        // Normalize relative paths and .. segments (does not resolve symlinks)
+        OUString aNormalized;
+        if (osl::FileBase::getAbsoluteFileURL(OUString(), aFileUrl, 
aNormalized)
+            == osl::FileBase::E_None)
+        {
+            if (!aNormalized.endsWith("/"))
+                aNormalized += "/";
+            g_AllowedExtRefPaths.push_back(aNormalized);
+        }
+        else
+        {
+            if (!aFileUrl.endsWith("/"))
+                aFileUrl += "/";
+            g_AllowedExtRefPaths.push_back(aFileUrl);
+        }
+    } while (nIndex >= 0);
+}
+
+bool HostFilter::isFileUrlForbidden(const OUString& rFileUrl)
+{
+    if (!g_AllowedExtRefPathsConfigured)
+        return false;
+
+    if (!rFileUrl.startsWithIgnoreAsciiCase("file:"))
+        return false;
+
+    // Normalize relative paths and .. segments (does not resolve symlinks)
+    OUString aNormalized;
+    if (osl::FileBase::getAbsoluteFileURL(OUString(), rFileUrl, aNormalized)
+        != osl::FileBase::E_None)
+        return true;
+
+    // Case-sensitive comparison: assumes a case-sensitive filesystem (i.e. 
Linux).
+    for (const auto& rAllowed : g_AllowedExtRefPaths)
+    {
+        if (aNormalized.startsWith(rAllowed))
+            return false;
+    }
+
+    return true;
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */

Reply via email to