From d5d950c2b9869874875dcf279b7b0cf48a31e418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 24 Aug 2026 22:11:49 +0000 Subject: [PATCH] xslt filter: set every libxslt transformation option explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libxslt leaves each option unset on a fresh transformation context and takes its own default for it. The service registers no libxml input handlers, and a stylesheet only has to produce the result tree, so set all of them and allow back local file reads alone, which is how a stylesheet picks up a second file beside the one it converts. Signed-off-by: Caolán McNamara Change-Id: I58abfc3919e26683d3fe2de72362f6c453eef8cf Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/9939 Tested-by: Jenkins CPCI Reviewed-by: Noel Grandin (cherry picked from commit 02da3650b1bb71170112a480c7fa0220a2a34490) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/210434 Reviewed-by: Xisco Fauli Tested-by: Xisco Fauli --- filter/qa/cppunit/data/xslt/document.xslt | 16 ++++ filter/qa/cppunit/xslt-test.cxx | 79 ++++++++++++++++++- .../source/xsltfilter/LibXSLTTransformer.cxx | 22 +++++- 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 filter/qa/cppunit/data/xslt/document.xslt diff --git a/filter/qa/cppunit/data/xslt/document.xslt b/filter/qa/cppunit/data/xslt/document.xslt new file mode 100644 index 000000000000..712df80e850d --- /dev/null +++ b/filter/qa/cppunit/data/xslt/document.xslt @@ -0,0 +1,16 @@ + + + + + + + + written + + + + + diff --git a/filter/qa/cppunit/xslt-test.cxx b/filter/qa/cppunit/xslt-test.cxx index 3ef87ad118ee..82c10aa4465f 100644 --- a/filter/qa/cppunit/xslt-test.cxx +++ b/filter/qa/cppunit/xslt-test.cxx @@ -44,27 +44,38 @@ class XsltFilterTest public: void testXsltCopyOld(); void testXsltCopyNew(); + void testXsltOutputStreamOnly(); CPPUNIT_TEST_SUITE(XsltFilterTest); CPPUNIT_TEST(testXsltCopyOld); CPPUNIT_TEST(testXsltCopyNew); + CPPUNIT_TEST(testXsltOutputStreamOnly); CPPUNIT_TEST_SUITE_END(); }; class Listener : public ::cppu::WeakImplHelper { public: - Listener() : m_bDone(false) {} + /// Pass false for bFailOnError when the transformation is meant to fail. + Listener(bool bFailOnError = true) + : m_bDone(false), m_bFailOnError(bFailOnError), m_bError(false) {} void wait() { std::unique_lock g(m_mutex); m_cond.wait(g, [this]() { return m_bDone; }); } + bool hadError() { + std::scoped_lock g(m_mutex); + return m_bError; + } + private: std::mutex m_mutex; std::condition_variable m_cond; bool m_bDone; + bool m_bFailOnError; + bool m_bError; virtual void SAL_CALL disposing(const lang::EventObject&) noexcept override {} virtual void SAL_CALL started() noexcept override {} @@ -72,9 +83,16 @@ private: virtual void SAL_CALL terminated() noexcept override { notifyDone(); } virtual void SAL_CALL error(const uno::Any& e) override { + { + std::scoped_lock g(m_mutex); + m_bError = true; + } notifyDone(); // set on error too, otherwise main thread waits forever SAL_WARN("filter.xslt", e); - CPPUNIT_FAIL("exception while in XSLT"); + // A failure thrown here would land on the transformation thread, so record it and let + // the test body check hadError. + if (m_bFailOnError) + CPPUNIT_FAIL("exception while in XSLT"); } void notifyDone() { @@ -198,6 +216,63 @@ void XsltFilterTest::testXsltCopyOld() osl_removeFile(tempURL.pData); } +// The transformation writes to the output stream it is given, and nowhere else. +void XsltFilterTest::testXsltOutputStreamOnly() +{ + OUString tempDirURL; + osl_getTempDirURL(&tempDirURL.pData); + oslFileHandle tempFile; + OUString tempURL; + osl::File::RC rc = osl::File::createTempFile(nullptr, &tempFile, &tempURL); + CPPUNIT_ASSERT_EQUAL(osl::FileBase::E_None, rc); + osl_closeFile(tempFile); // close it so xSFA can open it on WNT + + OUString writtenURL(tempDirURL + "/xslt-write-test.txt"); + osl_removeFile(writtenURL.pData); + + OUString source( + m_directories.getURLFromSrc(u"/filter/source/xsltfilter/xsltfilter.component")); + uno::Sequence args{ + uno::Any(beans::NamedValue(u"StylesheetURL"_ustr, + uno::Any(m_directories.getURLFromSrc(u"/filter/qa/cppunit/data/xslt/document.xslt")))), + uno::Any(beans::NamedValue(u"SourceURL"_ustr, uno::Any(source))), + uno::Any(beans::NamedValue(u"TargetURL"_ustr, uno::Any(tempURL))), + uno::Any(beans::NamedValue(u"TargetBaseURL"_ustr, uno::Any(tempDirURL + "/"))), + uno::Any(beans::NamedValue(u"SystemType"_ustr, uno::Any(OUString()))), + uno::Any(beans::NamedValue(u"PublicType"_ustr, uno::Any(OUString()))) + }; + + uno::Reference xSFA = + ucb::SimpleFileAccess::create(getComponentContext()); + + uno::Reference xIn = xSFA->openFileRead(source); + uno::Reference xOut = xSFA->openFileWrite(tempURL); + + rtl::Reference xListener = new Listener(false); + + uno::Reference xXslt( + xml::xslt::XSLTTransformer::create(getComponentContext(), args)); + + xXslt->addListener(xListener); + xXslt->setInputStream(xIn); + xXslt->setOutputStream(xOut); + + xXslt->start(); + + xListener->wait(); + + xIn->closeInput(); + xOut->closeOutput(); + xXslt->terminate(); + + CPPUNIT_ASSERT_MESSAGE("the transformation wrote outside its output stream", + !xSFA->exists(writtenURL)); + CPPUNIT_ASSERT_MESSAGE("the transformation was not reported as failed", + xListener->hadError()); + + osl_removeFile(tempURL.pData); +} + CPPUNIT_TEST_SUITE_REGISTRATION(XsltFilterTest); } diff --git a/filter/source/xsltfilter/LibXSLTTransformer.cxx b/filter/source/xsltfilter/LibXSLTTransformer.cxx index 4defe3ff160d..2282ff0e9b3e 100644 --- a/filter/source/xsltfilter/LibXSLTTransformer.cxx +++ b/filter/source/xsltfilter/LibXSLTTransformer.cxx @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -307,6 +308,7 @@ namespace XSLT xsltDebugDumpExtensions(NULL); #endif std::optional oh(std::in_place, m_transformer->getComponentContext()); + xsltSecurityPrefsPtr securityPrefs = nullptr; if (styleSheet) { xsltTransformContextPtr tcontext = xsltNewTransformContext( @@ -317,8 +319,23 @@ namespace XSLT } oh->registercontext(tcontext); xsltQuoteUserParams(tcontext, params.data()); - result = xsltApplyStylesheetUser(styleSheet, doc, nullptr, nullptr, nullptr, - tcontext); + // A stylesheet builds the result tree and nothing else. Refuse every option libxslt + // has, then allow back the local reads, which is how a stylesheet picks up a second + // file that sits beside the one it converts. + securityPrefs = xsltNewSecurityPrefs(); + if (securityPrefs) + { + for (auto eOption : { XSLT_SECPREF_READ_FILE, XSLT_SECPREF_WRITE_FILE, + XSLT_SECPREF_CREATE_DIRECTORY, XSLT_SECPREF_READ_NETWORK, + XSLT_SECPREF_WRITE_NETWORK }) + xsltSetSecurityPrefs(securityPrefs, eOption, xsltSecurityForbid); + xsltSetSecurityPrefs(securityPrefs, XSLT_SECPREF_READ_FILE, xsltSecurityAllow); + } + if (securityPrefs && xsltSetCtxtSecurityPrefs(securityPrefs, tcontext) == 0) + result = xsltApplyStylesheetUser(styleSheet, doc, nullptr, nullptr, nullptr, + tcontext); + else + m_transformer->error(u"No security preferences were applied"_ustr); } if (result) @@ -351,6 +368,7 @@ namespace XSLT std::swap(m_tcontext, tcontext); } xsltFreeTransformContext(tcontext); + xsltFreeSecurityPrefs(securityPrefs); xmlFreeDoc(doc); xmlFreeDoc(result); }