From e106df701ffd94fe7e85d4e18f1d32a752246f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 09:25:26 +0000 Subject: [PATCH 01/16] Own the OAuth2Handler with unique_ptr HttpSession held m_oauth2Handler as a raw OAuth2Handler* and the copy constructor and operator= both did a shallow copy of the pointer. Two HttpSession instances ended up sharing one handler, which the destructor then deleted. Switch m_oauth2Handler to std::unique_ptr and copy through a new constructo that points to the new owning session. Drop the old (unused) plain copy constructor and assigment. --- qa/libcmis/test-commons.cxx | 32 +++++++++++--------------------- src/libcmis/base-session.cxx | 2 +- src/libcmis/gdrive-session.cxx | 2 +- src/libcmis/http-session.cxx | 17 ++++++++++------- src/libcmis/http-session.hxx | 3 ++- src/libcmis/oauth2-handler.cxx | 18 ++---------------- src/libcmis/oauth2-handler.hxx | 10 +++++++--- src/libcmis/onedrive-session.cxx | 2 +- 8 files changed, 35 insertions(+), 51 deletions(-) diff --git a/qa/libcmis/test-commons.cxx b/qa/libcmis/test-commons.cxx index df271e92..f8304521 100644 --- a/qa/libcmis/test-commons.cxx +++ b/qa/libcmis/test-commons.cxx @@ -107,32 +107,22 @@ void CommonsTest::oauth2HandlerCopyTest( ) { OAuth2DataPtr data( new OAuth2Data ( "url", "token", "scope", "redirect", "clientid", "clientsecret" ) ); - HttpSession session( "user", "pass" ); - OAuth2Handler handler( &session, data ); + HttpSession source( "user", "pass" ); + HttpSession owner( "user", "pass" ); + OAuth2Handler handler( &source, data ); handler.m_access = "access"; handler.m_refresh = "refresh"; handler.m_oauth2Parser = &DummyOAuth2Parser; - { - OAuth2Handler copy; - copy = handler; - - CPPUNIT_ASSERT_EQUAL( &session, copy.m_session ); - CPPUNIT_ASSERT_EQUAL( data, copy.m_data ); - CPPUNIT_ASSERT_EQUAL( handler.m_access, copy.m_access ); - CPPUNIT_ASSERT_EQUAL( handler.m_refresh, copy.m_refresh ); - CPPUNIT_ASSERT_EQUAL( &DummyOAuth2Parser, copy.m_oauth2Parser ); - } + OAuth2Handler copy( &owner, handler ); - { - OAuth2Handler copy( handler ); - - CPPUNIT_ASSERT_EQUAL( &session, copy.m_session ); - CPPUNIT_ASSERT_EQUAL( data, copy.m_data ); - CPPUNIT_ASSERT_EQUAL( handler.m_access, copy.m_access ); - CPPUNIT_ASSERT_EQUAL( handler.m_refresh, copy.m_refresh ); - CPPUNIT_ASSERT_EQUAL( &DummyOAuth2Parser, copy.m_oauth2Parser ); - } + // m_session must follow the new owner, not the source, so that the + // copy keeps working after `source` is destroyed. + CPPUNIT_ASSERT_EQUAL( &owner, copy.m_session ); + CPPUNIT_ASSERT_EQUAL( data, copy.m_data ); + CPPUNIT_ASSERT_EQUAL( handler.m_access, copy.m_access ); + CPPUNIT_ASSERT_EQUAL( handler.m_refresh, copy.m_refresh ); + CPPUNIT_ASSERT_EQUAL( &DummyOAuth2Parser, copy.m_oauth2Parser ); } static void assertObjectTypeEquals( const ObjectType& expected, const ObjectType& actual ) diff --git a/src/libcmis/base-session.cxx b/src/libcmis/base-session.cxx index 94894407..ded61dcc 100644 --- a/src/libcmis/base-session.cxx +++ b/src/libcmis/base-session.cxx @@ -123,7 +123,7 @@ void BaseSession::setNoSSLCertificateCheck( bool noCheck ) void BaseSession::setOAuth2Data( libcmis::OAuth2DataPtr oauth2 ) { - m_oauth2Handler = new OAuth2Handler( this, oauth2 ); + m_oauth2Handler.reset( new OAuth2Handler( this, oauth2 ) ); m_oauth2Handler->setOAuth2Parser( OAuth2Providers::getOAuth2Parser( getBindingUrl( ) ) ); oauth2Authenticate( ); diff --git a/src/libcmis/gdrive-session.cxx b/src/libcmis/gdrive-session.cxx index 51553388..b794a835 100644 --- a/src/libcmis/gdrive-session.cxx +++ b/src/libcmis/gdrive-session.cxx @@ -69,7 +69,7 @@ GDriveSession::~GDriveSession() void GDriveSession::setOAuth2Data( libcmis::OAuth2DataPtr oauth2 ) { - m_oauth2Handler = new OAuth2Handler( this, oauth2 ); + m_oauth2Handler.reset( new OAuth2Handler( this, oauth2 ) ); m_oauth2Handler->setOAuth2Parser( OAuth2Providers::getOAuth2Parser( getBindingUrl( ) ) ); oauth2Authenticate( ); diff --git a/src/libcmis/http-session.cxx b/src/libcmis/http-session.cxx index 044a1539..f6f7c16c 100644 --- a/src/libcmis/http-session.cxx +++ b/src/libcmis/http-session.cxx @@ -162,7 +162,7 @@ HttpSession::HttpSession( string username, string password, bool noSslCheck, m_curlHandle( NULL ), m_CurlInitProtocolsFunction(initProtocolsFunction), m_no100Continue( false ), - m_oauth2Handler( NULL ), + m_oauth2Handler( ), m_username( username ), m_password( password ), m_authProvided( false ), @@ -185,7 +185,9 @@ HttpSession::HttpSession( const HttpSession& copy ) : m_curlHandle( NULL ), m_CurlInitProtocolsFunction( copy.m_CurlInitProtocolsFunction ), m_no100Continue( copy.m_no100Continue ), - m_oauth2Handler( copy.m_oauth2Handler ), + m_oauth2Handler( copy.m_oauth2Handler ? + new OAuth2Handler( this, *copy.m_oauth2Handler ) : + nullptr ), m_username( copy.m_username ), m_password( copy.m_password ), m_authProvided( copy.m_authProvided ), @@ -204,7 +206,7 @@ HttpSession::HttpSession( const HttpSession& copy ) : HttpSession::HttpSession( ) : m_curlHandle( NULL ), m_no100Continue( false ), - m_oauth2Handler( NULL ), + m_oauth2Handler( ), m_username( ), m_password( ), m_authProvided( false ), @@ -227,7 +229,9 @@ HttpSession& HttpSession::operator=( const HttpSession& copy ) m_curlHandle = NULL; m_CurlInitProtocolsFunction = copy.m_CurlInitProtocolsFunction; m_no100Continue = copy.m_no100Continue; - m_oauth2Handler = copy.m_oauth2Handler; + m_oauth2Handler.reset( copy.m_oauth2Handler ? + new OAuth2Handler( this, *copy.m_oauth2Handler ) : + nullptr ); m_username = copy.m_username; m_password = copy.m_password; m_authProvided = copy.m_authProvided; @@ -250,7 +254,6 @@ HttpSession::~HttpSession( ) { if ( NULL != m_curlHandle ) curl_easy_cleanup( m_curlHandle ); - delete( m_oauth2Handler ); } string& HttpSession::getUsername( ) @@ -679,7 +682,7 @@ void HttpSession::httpRunRequest( string url, vector< string > headers, bool red // If we are using OAuth2, then add the proper header with token to authenticate // Otherwise, just set the credentials normally using in libcurl options - if ( m_oauth2Handler != NULL && !m_oauth2Handler->getHttpHeader( ).empty() ) + if ( m_oauth2Handler && !m_oauth2Handler->getHttpHeader( ).empty() ) { headers_slist.reset(curl_slist_append(headers_slist.release(), m_oauth2Handler->getHttpHeader().c_str())); @@ -829,7 +832,7 @@ long HttpSession::getHttpStatus( ) void HttpSession::setOAuth2Data( libcmis::OAuth2DataPtr oauth2 ) { - m_oauth2Handler = new OAuth2Handler( this, oauth2 ); + m_oauth2Handler.reset( new OAuth2Handler( this, oauth2 ) ); } void HttpSession::oauth2Authenticate( ) diff --git a/src/libcmis/http-session.hxx b/src/libcmis/http-session.hxx index 6ee8ad8a..a4724dd4 100644 --- a/src/libcmis/http-session.hxx +++ b/src/libcmis/http-session.hxx @@ -29,6 +29,7 @@ #define _HTTP_SESSION_HXX_ #include +#include #include #include #include @@ -101,7 +102,7 @@ class HttpSession private: bool m_no100Continue; protected: - OAuth2Handler* m_oauth2Handler; + std::unique_ptr m_oauth2Handler; std::string m_username; std::string m_password; bool m_authProvided; diff --git a/src/libcmis/oauth2-handler.cxx b/src/libcmis/oauth2-handler.cxx index a340b80e..f896db39 100644 --- a/src/libcmis/oauth2-handler.cxx +++ b/src/libcmis/oauth2-handler.cxx @@ -50,8 +50,8 @@ OAuth2Handler::OAuth2Handler(HttpSession* session, libcmis::OAuth2DataPtr data) } -OAuth2Handler::OAuth2Handler( const OAuth2Handler& copy ) : - m_session( copy.m_session ), +OAuth2Handler::OAuth2Handler( HttpSession* session, const OAuth2Handler& copy ) : + m_session( session ), m_data( copy.m_data ), m_access( copy.m_access ), m_refresh( copy.m_refresh ), @@ -69,20 +69,6 @@ OAuth2Handler::OAuth2Handler( ): m_data.reset( new libcmis::OAuth2Data() ); } -OAuth2Handler& OAuth2Handler::operator=( const OAuth2Handler& copy ) -{ - if ( this != © ) - { - m_session = copy.m_session; - m_data = copy.m_data; - m_access = copy.m_access; - m_refresh = copy.m_refresh; - m_oauth2Parser = copy.m_oauth2Parser; - } - - return *this; -} - OAuth2Handler::~OAuth2Handler( ) { diff --git a/src/libcmis/oauth2-handler.hxx b/src/libcmis/oauth2-handler.hxx index bb9a3946..19916b7c 100644 --- a/src/libcmis/oauth2-handler.hxx +++ b/src/libcmis/oauth2-handler.hxx @@ -52,10 +52,14 @@ class OAuth2Handler OAuth2Handler( HttpSession* session, libcmis::OAuth2DataPtr data ); - OAuth2Handler( const OAuth2Handler& copy ); - ~OAuth2Handler( ); + // Deep-copy into a new owning session. m_session must point at the + // new owner, not at the soon-to-be-destroyed source HttpSession. + OAuth2Handler( HttpSession* session, const OAuth2Handler& copy ); + + OAuth2Handler( const OAuth2Handler& ) = delete; + OAuth2Handler& operator=( const OAuth2Handler& ) = delete; - OAuth2Handler& operator=( const OAuth2Handler& copy ); + ~OAuth2Handler( ); std::string getAuthURL(); diff --git a/src/libcmis/onedrive-session.cxx b/src/libcmis/onedrive-session.cxx index 89d5a504..6104305e 100644 --- a/src/libcmis/onedrive-session.cxx +++ b/src/libcmis/onedrive-session.cxx @@ -66,7 +66,7 @@ OneDriveSession::~OneDriveSession() void OneDriveSession::setOAuth2Data( libcmis::OAuth2DataPtr oauth2 ) { - m_oauth2Handler = new OAuth2Handler( this, oauth2 ); + m_oauth2Handler.reset( new OAuth2Handler( this, oauth2 ) ); m_oauth2Handler->setOAuth2Parser( OAuth2Providers::getOAuth2Parser( getBindingUrl( ) ) ); oauth2Authenticate( ); From 4fa188d4df1e904e28e61daac0810d4a7495314d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 09:40:02 +0000 Subject: [PATCH 02/16] Clamp base64 padding count before writing the decoded block Add a test for this. --- qa/libcmis/test-decoder.cxx | 14 ++++++++++++++ src/libcmis/xml-utils.cxx | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/qa/libcmis/test-decoder.cxx b/qa/libcmis/test-decoder.cxx index c7b47481..e44d7e56 100644 --- a/qa/libcmis/test-decoder.cxx +++ b/qa/libcmis/test-decoder.cxx @@ -61,6 +61,7 @@ class DecoderTest : public CppUnit::TestFixture void base64DecodePaddedBlockTest( ); void base64DecodeNoEqualsPaddedBlockTest( ); void base64DecodeSplitRunsTest( ); + void base64DecodeExcessPaddingTest( ); void base64EncodeSimpleBlockTest( ); void base64EncodePaddedBlockTest( ); @@ -74,6 +75,7 @@ class DecoderTest : public CppUnit::TestFixture CPPUNIT_TEST( base64DecodePaddedBlockTest ); CPPUNIT_TEST( base64DecodeNoEqualsPaddedBlockTest ); CPPUNIT_TEST( base64DecodeSplitRunsTest ); + CPPUNIT_TEST( base64DecodeExcessPaddingTest ); CPPUNIT_TEST( base64EncodeSimpleBlockTest ); CPPUNIT_TEST( base64EncodePaddedBlockTest ); CPPUNIT_TEST( base64EncodeSplitRunsTest ); @@ -183,6 +185,18 @@ void DecoderTest::base64DecodeSplitRunsTest( ) CPPUNIT_ASSERT_EQUAL( string( "pleasure." ), getActual( ) ); } +void DecoderTest::base64DecodeExcessPaddingTest( ) +{ + // A corrupt stream with more '=' than a base64 block can have. We must + // stay within the decodeBase64 buffer bounds. Ignore the block and the + // previous valid run of "pleasure." must still get output. + data->setEncoding( BASE64_ENCODING ); + string input( "cGxlYXN1cmUu========" ); + data->decode( ( void* )input.c_str( ), 1, input.size( ) ); + data->finish( ); + CPPUNIT_ASSERT_EQUAL( string( "pleasure." ), getActual( ) ); +} + void DecoderTest::base64EncodeSimpleBlockTest( ) { data->setEncoding( BASE64_ENCODING ); diff --git a/src/libcmis/xml-utils.cxx b/src/libcmis/xml-utils.cxx index 64c5ae51..6f9d2b58 100644 --- a/src/libcmis/xml-utils.cxx +++ b/src/libcmis/xml-utils.cxx @@ -182,6 +182,8 @@ namespace libcmis decoded[1] = ( m_pendingValue & 0xFF00 ) >> 8; decoded[2] = ( m_pendingValue & 0xFF ); + if ( missingBytes > 3 ) + missingBytes = 3; write( decoded, 1, 3 - missingBytes ); m_pendingRank = 0; @@ -239,6 +241,8 @@ namespace libcmis decoded[1] = ( blockValue & 0xFF00 ) >> 8; decoded[2] = ( blockValue & 0xFF ); + if ( missingBytes > 3 ) + missingBytes = 3; write( decoded, 1, 3 - missingBytes ); byteRank = 0; From 18dbfff85fa74c1d3614c1b197e7828e7fb46a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 10:24:47 +0000 Subject: [PATCH 03/16] Guard against namespace-less SOAP body children Add parseResponseNoNamespaceTest to exercise this --- qa/libcmis/test-soap.cxx | 17 +++++++++++++++++ src/libcmis/ws-soap.cxx | 7 +++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/qa/libcmis/test-soap.cxx b/qa/libcmis/test-soap.cxx index e401971b..e735408f 100644 --- a/qa/libcmis/test-soap.cxx +++ b/qa/libcmis/test-soap.cxx @@ -69,6 +69,7 @@ class SoapTest : public CppUnit::TestFixture void parseResponseTest( ); void parseResponseXmlTest( ); void parseResponseFaultTest( ); + void parseResponseNoNamespaceTest( ); // RelatedMultipart tests @@ -91,6 +92,7 @@ class SoapTest : public CppUnit::TestFixture CPPUNIT_TEST( parseResponseTest ); CPPUNIT_TEST( parseResponseXmlTest ); CPPUNIT_TEST( parseResponseFaultTest ); + CPPUNIT_TEST( parseResponseNoNamespaceTest ); CPPUNIT_TEST( serializeMultipartSimpleTest ); CPPUNIT_TEST( serializeMultipartComplexTest ); @@ -316,6 +318,21 @@ void SoapTest::parseResponseFaultTest( ) } } +void SoapTest::parseResponseNoNamespaceTest( ) +{ + SoapResponseFactory factory; + factory.setMapping( getTestMapping() ); + factory.setNamespaces( getTestNamespaces( ) ); + factory.setDetailMapping( getTestDetailMapping( ) ); + + string xml = "" + "" + ""; + + vector< SoapResponsePtr > actual = factory.parseResponse( xml ); + CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong number of responses", size_t( 0 ), actual.size( ) ); +} + void SoapTest::serializeMultipartSimpleTest( ) { string partName = "data"; diff --git a/src/libcmis/ws-soap.cxx b/src/libcmis/ws-soap.cxx index 4bf7d2fd..e62632b4 100644 --- a/src/libcmis/ws-soap.cxx +++ b/src/libcmis/ws-soap.cxx @@ -171,7 +171,8 @@ vector< SoapResponsePtr > SoapResponseFactory::parseResponse( RelatedMultipart& xmlNodePtr node = xpathObj->nodesetval->nodeTab[i]; // Is it a fault? - if ( xmlStrEqual( BAD_CAST( NS_SOAP_ENV_URL ), node->ns->href ) && + if ( node->ns && node->ns->href && + xmlStrEqual( BAD_CAST( NS_SOAP_ENV_URL ), node->ns->href ) && xmlStrEqual( BAD_CAST( "Fault" ), node->name ) ) { throw SoapFault( node, this ); @@ -191,7 +192,9 @@ SoapResponsePtr SoapResponseFactory::createResponse( xmlNodePtr node, RelatedMul { SoapResponsePtr response; - string ns( ( const char* ) node->ns->href ); + string ns; + if ( node->ns && node->ns->href ) + ns = string( ( const char* ) node->ns->href ); string name( ( const char* ) node->name ); string id = "{" + ns + "}" + name; map< string, SoapResponseCreator >::iterator it = m_mapping.find( id ); From cc57f09cfc3b971fc64733e27bcee20118ca3317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 10:15:48 +0000 Subject: [PATCH 04/16] validate BaseFileName requirements https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response See: CheckFileInfo Response properties BaseFileName: The string name of the file, including extension, without a path. Used for display in user interface (UI), and determining the extension of the file. --- src/cmis-client.cxx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cmis-client.cxx b/src/cmis-client.cxx index 943f5d51..2f9297bd 100644 --- a/src/cmis-client.cxx +++ b/src/cmis-client.cxx @@ -481,7 +481,15 @@ void CmisClient::execute( ) streamId = m_vm["stream-id"].as(); boost::shared_ptr< istream > in = document->getContentStream( streamId ); - ofstream out( document->getContentFilename().c_str() ); + string filename = document->getContentFilename(); + if ( filename.empty() || + filename == "." || filename == ".." || + filename.find( '/' ) != string::npos || + filename.find( '\\' ) != string::npos ) + { + throw CommandException( "Refusing server-supplied filename: " + filename ); + } + ofstream out( filename.c_str() ); out << in->rdbuf(); out.close(); } From 80d955d8b2c61499305ed149f597802a33a3535e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 12:30:32 +0000 Subject: [PATCH 05/16] Free xmlDoc results via shared_ptr in atom-* --- src/libcmis/atom-document.cxx | 17 ++++++++-------- src/libcmis/atom-object-type.cxx | 14 ++++++------- src/libcmis/atom-object.cxx | 35 ++++++++++++++------------------ 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/libcmis/atom-document.cxx b/src/libcmis/atom-document.cxx index dc6c8cfa..0c45dbd7 100644 --- a/src/libcmis/atom-document.cxx +++ b/src/libcmis/atom-document.cxx @@ -29,6 +29,7 @@ #include "atom-document.hxx" #include +#include #include #include @@ -277,12 +278,11 @@ libcmis::DocumentPtr AtomDocument::checkOut( ) } string respBuf = resp->getStream( )->str(); - xmlDocPtr doc = xmlReadMemory( respBuf.c_str(), respBuf.size(), checkedOutUrl.c_str(), NULL, 0 ); - if ( NULL == doc ) + std::shared_ptr< xmlDoc > doc( xmlReadMemory( respBuf.c_str(), respBuf.size(), checkedOutUrl.c_str(), NULL, 0 ), xmlFreeDoc ); + if ( !doc ) throw libcmis::Exception( "Failed to parse object infos" ); - libcmis::ObjectPtr created = getSession( )->createObjectFromEntryDoc( doc, AtomPubSession::RESULT_DOCUMENT ); - xmlFreeDoc( doc ); + libcmis::ObjectPtr created = getSession( )->createObjectFromEntryDoc( doc.get(), AtomPubSession::RESULT_DOCUMENT ); libcmis::DocumentPtr pwc = boost::dynamic_pointer_cast< libcmis::Document >( created ); if ( !pwc.get( ) ) @@ -373,16 +373,15 @@ libcmis::DocumentPtr AtomDocument::checkIn( bool isMajor, string comment, // Get the returned entry and update using it string respBuf = response->getStream( )->str( ); - xmlDocPtr doc = xmlReadMemory( respBuf.c_str(), respBuf.size(), checkInUrl.c_str(), NULL, 0 ); - if ( NULL == doc ) + std::shared_ptr< xmlDoc > doc( xmlReadMemory( respBuf.c_str(), respBuf.size(), checkInUrl.c_str(), NULL, 0 ), xmlFreeDoc ); + if ( !doc ) throw libcmis::Exception( "Failed to parse object infos" ); - libcmis::ObjectPtr newVersion = getSession( )->createObjectFromEntryDoc( doc, AtomPubSession::RESULT_DOCUMENT ); + libcmis::ObjectPtr newVersion = getSession( )->createObjectFromEntryDoc( doc.get(), AtomPubSession::RESULT_DOCUMENT ); if ( newVersion->getId( ) == getId( ) ) - refreshImpl( doc ); - xmlFreeDoc( doc ); + refreshImpl( doc.get() ); return boost::dynamic_pointer_cast< libcmis::Document >( newVersion ); } diff --git a/src/libcmis/atom-object-type.cxx b/src/libcmis/atom-object-type.cxx index 8f50d043..3c89f7b6 100644 --- a/src/libcmis/atom-object-type.cxx +++ b/src/libcmis/atom-object-type.cxx @@ -28,6 +28,7 @@ #include "atom-object-type.hxx" +#include #include #include @@ -99,8 +100,8 @@ vector< libcmis::ObjectTypePtr > AtomObjectType::getChildren( ) void AtomObjectType::refreshImpl( xmlDocPtr doc ) { - bool createdDoc = ( NULL == doc ); - if ( createdDoc ) + std::shared_ptr< xmlDoc > ownedDoc; + if ( NULL == doc ) { string pattern = m_session->getAtomRepository()->getUriTemplate( UriTemplate::TypeById ); map< string, string > vars; @@ -125,16 +126,15 @@ void AtomObjectType::refreshImpl( xmlDocPtr doc ) throw e.getCmisException( ); } - doc = xmlReadMemory( buf.c_str(), buf.size(), m_selfUrl.c_str(), NULL, 0 ); + ownedDoc.reset( xmlReadMemory( buf.c_str(), buf.size(), m_selfUrl.c_str(), NULL, 0 ), xmlFreeDoc ); - if ( NULL == doc ) + if ( !ownedDoc ) throw libcmis::Exception( "Failed to parse object infos" ); + + doc = ownedDoc.get(); } extractInfos( doc ); - - if ( createdDoc ) - xmlFreeDoc( doc ); } void AtomObjectType::extractInfos( xmlDocPtr doc ) diff --git a/src/libcmis/atom-object.cxx b/src/libcmis/atom-object.cxx index 55647455..7eff372a 100644 --- a/src/libcmis/atom-object.cxx +++ b/src/libcmis/atom-object.cxx @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -149,14 +150,13 @@ libcmis::ObjectPtr AtomObject::updateProperties( const PropertyPtrMap& propertie } string respBuf = response->getStream( )->str( ); - xmlDocPtr doc = xmlReadMemory( respBuf.c_str(), respBuf.size(), getInfosUrl().c_str(), NULL, 0 ); - if ( NULL == doc ) + std::shared_ptr< xmlDoc > doc( xmlReadMemory( respBuf.c_str(), respBuf.size(), getInfosUrl().c_str(), NULL, 0 ), xmlFreeDoc ); + if ( !doc ) throw libcmis::Exception( "Failed to parse object infos" ); - libcmis::ObjectPtr updated = getSession( )->createObjectFromEntryDoc( doc ); + libcmis::ObjectPtr updated = getSession( )->createObjectFromEntryDoc( doc.get() ); if ( updated->getId( ) == getId( ) ) - refreshImpl( doc ); - xmlFreeDoc( doc ); + refreshImpl( doc.get() ); return updated; } @@ -173,12 +173,10 @@ libcmis::AllowableActionsPtr AtomObject::getAllowableActions( ) { libcmis::HttpResponsePtr response = getSession()->httpGetRequest( link->getHref() ); string buf = response->getStream()->str(); - xmlDocPtr doc = xmlReadMemory( buf.c_str(), buf.size(), link->getHref().c_str(), NULL, 0 ); - xmlNodePtr actionsNode = xmlDocGetRootElement( doc ); + std::shared_ptr< xmlDoc > doc( xmlReadMemory( buf.c_str(), buf.size(), link->getHref().c_str(), NULL, 0 ), xmlFreeDoc ); + xmlNodePtr actionsNode = xmlDocGetRootElement( doc.get() ); if ( actionsNode ) m_allowableActions.reset( new libcmis::AllowableActions( actionsNode ) ); - - xmlFreeDoc( doc ); } catch ( CurlException& ) { @@ -191,8 +189,8 @@ libcmis::AllowableActionsPtr AtomObject::getAllowableActions( ) void AtomObject::refreshImpl( xmlDocPtr doc ) { - bool createdDoc = ( NULL == doc ); - if ( createdDoc ) + std::shared_ptr< xmlDoc > ownedDoc; + if ( NULL == doc ) { string buf; try @@ -204,11 +202,12 @@ void AtomObject::refreshImpl( xmlDocPtr doc ) throw e.getCmisException( ); } - doc = xmlReadMemory( buf.c_str(), buf.size(), getInfosUrl().c_str(), NULL, 0 ); + ownedDoc.reset( xmlReadMemory( buf.c_str(), buf.size(), getInfosUrl().c_str(), NULL, 0 ), xmlFreeDoc ); - if ( NULL == doc ) + if ( !ownedDoc ) throw libcmis::Exception( "Failed to parse object infos" ); + doc = ownedDoc.get(); } // Cleanup the structures before setting them again @@ -219,9 +218,6 @@ void AtomObject::refreshImpl( xmlDocPtr doc ) m_renditions.clear( ); extractInfos( doc ); - - if ( createdDoc ) - xmlFreeDoc( doc ); } void AtomObject::remove( bool allVersions ) @@ -302,11 +298,10 @@ void AtomObject::move( boost::shared_ptr< libcmis::Folder > source, boost::share // refresh self from response string respBuf = response->getStream( )->str( ); - xmlDocPtr doc = xmlReadMemory( respBuf.c_str(), respBuf.size(), getInfosUrl().c_str(), NULL, 0 ); - if ( NULL == doc ) + std::shared_ptr< xmlDoc > doc( xmlReadMemory( respBuf.c_str(), respBuf.size(), getInfosUrl().c_str(), NULL, 0 ), xmlFreeDoc ); + if ( !doc ) throw libcmis::Exception( "Failed to parse object infos" ); - refreshImpl( doc ); - xmlFreeDoc( doc ); + refreshImpl( doc.get() ); } string AtomObject::getInfosUrl( ) From 794bcb002c748bf4ea0a120fff4cf11e81a7babc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 14:51:26 +0000 Subject: [PATCH 06/16] Reject CR LF and NUL bytes before passing to libcurl --- qa/libcmis/test-commons.cxx | 20 ++++++++++++++++++++ src/libcmis/http-session.cxx | 21 ++++++++++++++++++++- src/libcmis/http-session.hxx | 6 ++++++ src/libcmis/sharepoint-session.cxx | 7 +++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/qa/libcmis/test-commons.cxx b/qa/libcmis/test-commons.cxx index f8304521..65553589 100644 --- a/qa/libcmis/test-commons.cxx +++ b/qa/libcmis/test-commons.cxx @@ -64,11 +64,14 @@ class CommonsTest : public CppUnit::TestFixture // Methods that should never be called void objectTypeNocallTest(); + void httpSessionCRLFInjectionTest(); + CPPUNIT_TEST_SUITE( CommonsTest ); CPPUNIT_TEST( oauth2DataCopyTest ); CPPUNIT_TEST( oauth2HandlerCopyTest ); CPPUNIT_TEST( objectTypeCopyTest ); CPPUNIT_TEST( objectTypeNocallTest ); + CPPUNIT_TEST( httpSessionCRLFInjectionTest ); CPPUNIT_TEST_SUITE_END( ); }; @@ -210,4 +213,21 @@ void CommonsTest::objectTypeNocallTest( ) } } +void CommonsTest::httpSessionCRLFInjectionTest( ) +{ + HttpSession session( "user", "pass" ); + std::string body( "hi" ); + std::istringstream is( body ); + try + { + session.httpPostRequest( "http://example.test/", + is, + "text/plain\r\nX-Injected: yes" ); + CPPUNIT_FAIL( "httpPostRequest with CRLF in contentType should throw" ); + } + catch ( const Exception& ) + { + } +} + CPPUNIT_TEST_SUITE_REGISTRATION( CommonsTest ); diff --git a/src/libcmis/http-session.cxx b/src/libcmis/http-session.cxx index f6f7c16c..07c966ad 100644 --- a/src/libcmis/http-session.cxx +++ b/src/libcmis/http-session.cxx @@ -156,6 +156,17 @@ namespace }; } +namespace libcmis { + +void rejectControlChars( const string& s, const char* what ) +{ + if ( s.find_first_of( "\r\n", 0 ) != string::npos || + s.find( '\0' ) != string::npos ) + throw Exception( string( "Invalid character in " ) + what ); +} + +} + HttpSession::HttpSession( string username, string password, bool noSslCheck, libcmis::OAuth2DataPtr oauth2, bool verbose, libcmis::CurlInitProtocolsFunction initProtocolsFunction) : @@ -665,6 +676,10 @@ void HttpSession::checkCredentials( ) void HttpSession::httpRunRequest( string url, vector< string > headers, bool redirect ) { + libcmis::rejectControlChars( url, "URL" ); + for ( vector< string >::const_iterator it = headers.begin( ); it != headers.end( ); ++it ) + libcmis::rejectControlChars( *it, "header" ); + // Redirect curl_easy_setopt( m_curlHandle, CURLOPT_FOLLOWLOCATION, redirect); @@ -684,11 +699,15 @@ void HttpSession::httpRunRequest( string url, vector< string > headers, bool red // Otherwise, just set the credentials normally using in libcurl options if ( m_oauth2Handler && !m_oauth2Handler->getHttpHeader( ).empty() ) { + string oauthHeader = m_oauth2Handler->getHttpHeader(); + libcmis::rejectControlChars( oauthHeader, "OAuth header" ); headers_slist.reset(curl_slist_append(headers_slist.release(), - m_oauth2Handler->getHttpHeader().c_str())); + oauthHeader.c_str())); } else if ( !getUsername().empty() ) { + libcmis::rejectControlChars( getUsername(), "username" ); + libcmis::rejectControlChars( getPassword(), "password" ); curl_easy_setopt( m_curlHandle, CURLOPT_HTTPAUTH, m_authMethod ); curl_easy_setopt( m_curlHandle, CURLOPT_USERNAME, getUsername().c_str() ); curl_easy_setopt( m_curlHandle, CURLOPT_PASSWORD, getPassword().c_str() ); diff --git a/src/libcmis/http-session.hxx b/src/libcmis/http-session.hxx index a4724dd4..92227a4f 100644 --- a/src/libcmis/http-session.hxx +++ b/src/libcmis/http-session.hxx @@ -46,6 +46,12 @@ class OAuth2Handler; namespace libcmis { typedef void(*CurlInitProtocolsFunction)(CURL *); + + /** Throw libcmis::Exception if s contains CR, LF or NUL. libcurl + started rejecting these in CURLOPT_URL at 7.84 and in + curl_slist_append at 7.86. + */ + void rejectControlChars(const std::string& s, const char* what); } class CurlException : public std::exception diff --git a/src/libcmis/sharepoint-session.cxx b/src/libcmis/sharepoint-session.cxx index 591d3cb3..5ef52213 100644 --- a/src/libcmis/sharepoint-session.cxx +++ b/src/libcmis/sharepoint-session.cxx @@ -191,6 +191,10 @@ Json SharePointSession::getJsonFromUrl( string url ) /* Overwriting HttpSession::httpRunRequest to add the "accept:application/json" header */ void SharePointSession::httpRunRequest( string url, vector< string > headers, bool redirect ) { + libcmis::rejectControlChars( url, "URL" ); + for ( vector< string >::const_iterator it = headers.begin( ); it != headers.end( ); ++it ) + libcmis::rejectControlChars( *it, "header" ); + // Redirect curl_easy_setopt( m_curlHandle, CURLOPT_FOLLOWLOCATION, redirect); @@ -215,6 +219,8 @@ void SharePointSession::httpRunRequest( string url, vector< string > headers, bo if ( !getUsername().empty() && !getPassword().empty() ) { + libcmis::rejectControlChars( getUsername(), "username" ); + libcmis::rejectControlChars( getPassword(), "password" ); curl_easy_setopt( m_curlHandle, CURLOPT_HTTPAUTH, m_authMethod ); curl_easy_setopt( m_curlHandle, CURLOPT_USERNAME, getUsername().c_str() ); curl_easy_setopt( m_curlHandle, CURLOPT_PASSWORD, getPassword().c_str() ); @@ -405,4 +411,5 @@ void SharePointSession::fetchDigestCodeCurl( ) string res = response->getStream( )->str( ); Json jsonRes = Json::parse( res ); m_digestCode = jsonRes["d"]["GetContextWebInformation"]["FormDigestValue"].toString( ); + libcmis::rejectControlChars( m_digestCode, "FormDigestValue" ); } From b8ef988cc02bd6e91662e0af2972803842257b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 15:45:06 +0000 Subject: [PATCH 07/16] Guard against empty fields in RelatedMultipart parsing --- qa/libcmis/test-soap.cxx | 28 ++++++++++++++++++++++++++++ src/libcmis/ws-relatedmultipart.cxx | 10 +++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/qa/libcmis/test-soap.cxx b/qa/libcmis/test-soap.cxx index e735408f..55b1e4ad 100644 --- a/qa/libcmis/test-soap.cxx +++ b/qa/libcmis/test-soap.cxx @@ -76,6 +76,7 @@ class SoapTest : public CppUnit::TestFixture void serializeMultipartSimpleTest( ); void serializeMultipartComplexTest( ); void parseMultipartTest( ); + void parseMultipartEmptyFieldsTest( ); void getStreamFromNodeXopTest( ); void getStreamFromNodeBase64Test( ); @@ -97,6 +98,7 @@ class SoapTest : public CppUnit::TestFixture CPPUNIT_TEST( serializeMultipartSimpleTest ); CPPUNIT_TEST( serializeMultipartComplexTest ); CPPUNIT_TEST( parseMultipartTest ); + CPPUNIT_TEST( parseMultipartEmptyFieldsTest ); CPPUNIT_TEST( getStreamFromNodeXopTest ); CPPUNIT_TEST( getStreamFromNodeBase64Test ); @@ -460,6 +462,32 @@ void SoapTest::parseMultipartTest( ) CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong part2 part content", part2Content, actualPart2->getContent( ) ); } +void SoapTest::parseMultipartEmptyFieldsTest( ) +{ + // Body, contentType params and Content-ID can all be empty in a + // bogus response. + string boundary = "abc"; + + { + // Empty start= and an empty body. + string contentType = "multipart/related; start=\"\"; boundary=\"" + boundary + "\""; + RelatedMultipart multipart( "", contentType ); + CPPUNIT_ASSERT_EQUAL( string( ), multipart.getStartId( ) ); + } + + { + // A part with an empty Content-Id and an empty body. + string body = "\r\n--" + boundary + "\r\n" + + "Content-Id: \r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "\r\n--" + boundary + "--\r\n"; + string contentType = "multipart/related; boundary=\"" + boundary + "\""; + RelatedMultipart multipart( body, contentType ); + CPPUNIT_ASSERT_EQUAL( boundary, multipart.getBoundary( ) ); + } +} + void SoapTest::getStreamFromNodeXopTest( ) { // Create the test multipart diff --git a/src/libcmis/ws-relatedmultipart.cxx b/src/libcmis/ws-relatedmultipart.cxx index 87a4f1ad..f36cf6d0 100644 --- a/src/libcmis/ws-relatedmultipart.cxx +++ b/src/libcmis/ws-relatedmultipart.cxx @@ -97,7 +97,7 @@ RelatedMultipart::RelatedMultipart( const string& body, const string& contentTyp { string name = param.substr( 0, eqPos ); string value = param.substr( eqPos + 1 ); - if ( value[0] == '"' && value[value.length() - 1] == '"' ) + if ( value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"' ) value = value.substr( 1, value.length( ) - 2 ); name = libcmis::trim( name ); @@ -106,7 +106,7 @@ RelatedMultipart::RelatedMultipart( const string& body, const string& contentTyp { m_startId = value; // Remove the '<' '>' around the id if any - if ( m_startId[0] == '<' && m_startId[m_startId.size()-1] == '>' ) + if ( m_startId.length() >= 2 && m_startId[0] == '<' && m_startId[m_startId.size()-1] == '>' ) m_startId = m_startId.substr( 1, m_startId.size() - 2 ); } else if ( name == "boundary" ) @@ -127,7 +127,7 @@ RelatedMultipart::RelatedMultipart( const string& body, const string& contentTyp if ( boost::starts_with( bodyFixed, "--" + m_boundary + "\r\n" ) ) bodyFixed = "\r\n" + bodyFixed; - if ( bodyFixed[bodyFixed.length() - 1 ] != '\n' ) + if ( bodyFixed.empty() || bodyFixed[bodyFixed.length() - 1 ] != '\n' ) bodyFixed += '\n'; string lineEnd( "\n" ); @@ -153,7 +153,7 @@ RelatedMultipart::RelatedMultipart( const string& body, const string& contentTyp if ( !cid.empty() && !type.empty( ) ) { // Remove potential \r at the end of the body part - if ( partBody[partBody.length() - 1] == '\r' ) + if ( !partBody.empty() && partBody[partBody.length() - 1] == '\r' ) partBody.pop_back(); RelatedPartPtr relatedPart( new RelatedPart( name, type, partBody ) ); @@ -185,7 +185,7 @@ RelatedMultipart::RelatedMultipart( const string& body, const string& contentTyp { cid = libcmis::trim( headerValue ); // Remove the '<' '>' around the id if any - if ( cid[0] == '<' && cid[cid.size()-1] == '>' ) + if ( cid.length() >= 2 && cid[0] == '<' && cid[cid.size()-1] == '>' ) cid = cid.substr( 1, cid.size() - 2 ); } else if ( headerName == "Content-Type" ) From d78556952e1928865fce5d10dfbde1b8ac5e5c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 15:52:36 +0000 Subject: [PATCH 08/16] RAII the scratch buffers in libcmis-c Replace the bare new char[]/delete[] with a std::vector so the buffer is freed on every exit path. --- src/libcmis-c/document.cxx | 23 ++++++++++------------- src/libcmis-c/folder.cxx | 7 +++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/libcmis-c/document.cxx b/src/libcmis-c/document.cxx index 74d04d91..05f296b2 100644 --- a/src/libcmis-c/document.cxx +++ b/src/libcmis-c/document.cxx @@ -148,15 +148,14 @@ void libcmis_document_getContentStream( boost::shared_ptr< istream > stream = doc->getContentStream( ); stream->seekg( 0 ); - int bufSize = 2048; - char* buf = new char[ bufSize ]; + size_t bufSize = 2048; + std::vector< char > buf( bufSize ); while ( !stream->eof( ) ) { - stream->read( buf, bufSize ); + stream->read( buf.data(), bufSize ); size_t read = stream->gcount( ); - writeFn( ( const void * )buf, size_t( 1 ), read, userData ); + writeFn( ( const void * )buf.data(), size_t( 1 ), read, userData ); } - delete[] buf; } } catch ( const libcmis::Exception& e ) @@ -203,14 +202,13 @@ void libcmis_document_setContentStream( boost::shared_ptr< std::ostream > stream( new stringstream( ) ); size_t bufSize = 2048; - char* buf = new char[ bufSize ]; + std::vector< char > buf( bufSize ); size_t read = 0; do { - read = readFn( ( void * )buf, size_t( 1 ), bufSize, userData ); - stream->write( buf, read ); + read = readFn( ( void * )buf.data(), size_t( 1 ), bufSize, userData ); + stream->write( buf.data(), read ); } while ( read == bufSize ); - delete[] buf; DocumentPtr doc = dynamic_pointer_cast< libcmis::Document >( document->handle ); if ( doc ) @@ -358,14 +356,13 @@ libcmis_DocumentPtr libcmis_document_checkIn( boost::shared_ptr< std::ostream > stream( new stringstream( ) ); size_t bufSize = 2048; - char * buf = new char[ bufSize ]; + std::vector< char > buf( bufSize ); size_t read = 0; do { - read = readFn( ( void * )buf, size_t( 1 ), bufSize, userData ); - stream->write( buf, read ); + read = readFn( ( void * )buf.data(), size_t( 1 ), bufSize, userData ); + stream->write( buf.data(), read ); } while ( read == bufSize ); - delete[] buf; // Create the property map PropertyPtrMap propertiesMap; diff --git a/src/libcmis-c/folder.cxx b/src/libcmis-c/folder.cxx index 8d7555a0..c34be9a1 100644 --- a/src/libcmis-c/folder.cxx +++ b/src/libcmis-c/folder.cxx @@ -276,14 +276,13 @@ libcmis_DocumentPtr libcmis_folder_createDocument( boost::shared_ptr< std::ostream > stream( new stringstream( ) ); size_t bufSize = 2048; - char* buf = new char[ bufSize ]; + std::vector< char > buf( bufSize ); size_t read = 0; do { - read = readFn( ( void * )buf, size_t( 1 ), bufSize, userData ); - stream->write( buf, read ); + read = readFn( ( void * )buf.data(), size_t( 1 ), bufSize, userData ); + stream->write( buf.data(), read ); } while ( read == bufSize ); - delete[] buf; // Create the property map PropertyPtrMap propertiesMap; From 47dd2e7fa3ad4d91e31bf7836c00a5f8038a2852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 15:58:23 +0000 Subject: [PATCH 09/16] Free the xmlDoc on the reader-NULL early return Add the missing xmlFreeDoc call before each early return. --- src/libcmis/oauth2-providers.cxx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libcmis/oauth2-providers.cxx b/src/libcmis/oauth2-providers.cxx index 1c8d3cc7..b9a366b2 100644 --- a/src/libcmis/oauth2-providers.cxx +++ b/src/libcmis/oauth2-providers.cxx @@ -126,7 +126,11 @@ int OAuth2Providers::parseResponse ( const char* response, string& post, string& HTML_PARSE_NOWARNING | HTML_PARSE_RECOVER | HTML_PARSE_NOERROR ); if ( doc == NULL ) return 0; xmlTextReaderPtr reader = xmlReaderWalker( doc ); - if ( reader == NULL ) return 0; + if ( reader == NULL ) + { + xmlFreeDoc( doc ); + return 0; + } bool readInputField = false; bool bIsRightForm = false; @@ -209,7 +213,11 @@ string OAuth2Providers::parseCode( const char* response ) HTML_PARSE_NOWARNING | HTML_PARSE_RECOVER | HTML_PARSE_NOERROR ); if ( doc == NULL ) return authCode; xmlTextReaderPtr reader = xmlReaderWalker( doc ); - if ( reader == NULL ) return authCode; + if ( reader == NULL ) + { + xmlFreeDoc( doc ); + return authCode; + } while ( true ) { From a9a893665130f5945dd955e0107a7c2410a431cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 16:04:48 +0000 Subject: [PATCH 10/16] Guard against NULL from xmlGetProp / xmlNodeGetContent Both functions can return NULL --- src/libcmis/allowable-actions.cxx | 7 +++++-- src/libcmis/atom-document.cxx | 7 +++++-- src/libcmis/property.cxx | 7 +++++-- src/libcmis/repository.cxx | 4 ++++ src/libcmis/ws-relatedmultipart.cxx | 18 +++++++++++------- src/libcmis/ws-requests.cxx | 4 ++++ src/libcmis/ws-soap.cxx | 29 +++++++++++++++++++---------- src/libcmis/xml-utils.cxx | 7 +++++-- 8 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/libcmis/allowable-actions.cxx b/src/libcmis/allowable-actions.cxx index 533069c6..62fe51b5 100644 --- a/src/libcmis/allowable-actions.cxx +++ b/src/libcmis/allowable-actions.cxx @@ -54,8 +54,11 @@ namespace libcmis try { xmlChar* content = xmlNodeGetContent( node ); - m_enabled = parseBool( string( ( char* )content ) ); - xmlFree( content ); + if ( content ) + { + m_enabled = parseBool( string( ( char* )content ) ); + xmlFree( content ); + } } catch ( const Exception& ) { diff --git a/src/libcmis/atom-document.cxx b/src/libcmis/atom-document.cxx index 0c45dbd7..a78b67d3 100644 --- a/src/libcmis/atom-document.cxx +++ b/src/libcmis/atom-document.cxx @@ -467,8 +467,11 @@ void AtomDocument::extractInfos( xmlDocPtr doc ) { xmlNodePtr contentNd = xpathObj->nodesetval->nodeTab[0]; xmlChar* src = xmlGetProp( contentNd, BAD_CAST( "src" ) ); - m_contentUrl = string( ( char* ) src ); - xmlFree( src ); + if ( src ) + { + m_contentUrl = string( ( char* ) src ); + xmlFree( src ); + } } xmlXPathFreeObject( xpathObj ); } diff --git a/src/libcmis/property.cxx b/src/libcmis/property.cxx index 41c181bb..8e4c8d1f 100644 --- a/src/libcmis/property.cxx +++ b/src/libcmis/property.cxx @@ -214,8 +214,11 @@ namespace libcmis if ( xmlStrEqual( child->name, BAD_CAST( "value" ) ) ) { xmlChar* content = xmlNodeGetContent( child ); - values.push_back( string( ( char * ) content ) ); - xmlFree( content ); + if ( content ) + { + values.push_back( string( ( char * ) content ) ); + xmlFree( content ); + } } } property.reset( new Property( propType, values ) ); diff --git a/src/libcmis/repository.cxx b/src/libcmis/repository.cxx index 0a7cbe6a..331127e4 100644 --- a/src/libcmis/repository.cxx +++ b/src/libcmis/repository.cxx @@ -132,6 +132,8 @@ namespace libcmis string localName( ( char* ) child->name ); xmlChar* content = xmlNodeGetContent( child ); + if ( content == NULL ) + continue; string value( ( char* )content ); xmlFree( content ); @@ -247,6 +249,8 @@ namespace libcmis string localName( ( char* ) child->name ); xmlChar* content = xmlNodeGetContent( child ); + if ( content == NULL ) + continue; string value( ( char* )content ); xmlFree( content ); diff --git a/src/libcmis/ws-relatedmultipart.cxx b/src/libcmis/ws-relatedmultipart.cxx index f36cf6d0..f1df59a8 100644 --- a/src/libcmis/ws-relatedmultipart.cxx +++ b/src/libcmis/ws-relatedmultipart.cxx @@ -320,6 +320,8 @@ boost::shared_ptr< istream > getStreamFromNode( xmlNodePtr node, RelatedMultipar { // Get the content from the multipart xmlChar* value = xmlGetProp( child, BAD_CAST( "href" ) ); + if ( value == NULL ) + continue; string href( ( char* )value ); xmlFree( value ); // Get the Content ID from the href (cid:content-id) @@ -340,14 +342,16 @@ boost::shared_ptr< istream > getStreamFromNode( xmlNodePtr node, RelatedMultipar if ( stream.get( ) == NULL ) { xmlChar* content = xmlNodeGetContent( node ); + if ( content ) + { + stream.reset( new stringstream( ) ); + libcmis::EncodedData decoder( stream.get( ) ); + decoder.setEncoding( "base64" ); + decoder.decode( ( void* )content, 1, xmlStrlen( content ) ); + decoder.finish( ); - stream.reset( new stringstream( ) ); - libcmis::EncodedData decoder( stream.get( ) ); - decoder.setEncoding( "base64" ); - decoder.decode( ( void* )content, 1, xmlStrlen( content ) ); - decoder.finish( ); - - xmlFree( content ); + xmlFree( content ); + } } return stream; } diff --git a/src/libcmis/ws-requests.cxx b/src/libcmis/ws-requests.cxx index 8235c0b1..83eacf4a 100644 --- a/src/libcmis/ws-requests.cxx +++ b/src/libcmis/ws-requests.cxx @@ -48,6 +48,8 @@ CmisSoapFaultDetail::CmisSoapFaultDetail( xmlNodePtr node ) : for ( xmlNodePtr child = node->children; child; child = child->next ) { xmlChar* content = xmlNodeGetContent( child ); + if ( content == NULL ) + continue; string value( ( char * )content ); xmlFree( content ); @@ -155,6 +157,8 @@ SoapResponsePtr GetRepositoriesResponse::create( xmlNodePtr node, RelatedMultipa for ( xmlNodePtr repoChild = child->children; repoChild; repoChild = repoChild->next ) { xmlChar* content = xmlNodeGetContent( repoChild ); + if ( content == NULL ) + continue; string value( ( char* ) content ); xmlFree( content ); diff --git a/src/libcmis/ws-soap.cxx b/src/libcmis/ws-soap.cxx index e62632b4..39590650 100644 --- a/src/libcmis/ws-soap.cxx +++ b/src/libcmis/ws-soap.cxx @@ -52,20 +52,29 @@ SoapFault::SoapFault( xmlNodePtr node, SoapResponseFactory* factory ) : if ( xmlStrEqual( child->name, BAD_CAST( "faultcode" ) ) ) { xmlChar* content = xmlNodeGetContent( child ); - xmlChar* prefix = NULL; - xmlChar* localName = xmlSplitQName2( content, &prefix ); - if (localName == NULL) - localName = xmlStrdup( content ); - m_faultcode = string( ( char* )localName ); - xmlFree( content ); - xmlFree( prefix ); - xmlFree( localName ); + if ( content ) + { + xmlChar* prefix = NULL; + xmlChar* localName = xmlSplitQName2( content, &prefix ); + if (localName == NULL) + localName = xmlStrdup( content ); + if ( localName ) + { + m_faultcode = string( ( char* )localName ); + xmlFree( localName ); + } + xmlFree( content ); + xmlFree( prefix ); + } } else if ( xmlStrEqual( child->name, BAD_CAST( "faultstring" ) ) ) { xmlChar* content = xmlNodeGetContent( child ); - m_faultstring = string( ( char* )content ); - xmlFree( content ); + if ( content ) + { + m_faultstring = string( ( char* )content ); + xmlFree( content ); + } } else if ( xmlStrEqual( child->name, BAD_CAST( "detail" ) ) ) { diff --git a/src/libcmis/xml-utils.cxx b/src/libcmis/xml-utils.cxx index 6f9d2b58..597ad79e 100644 --- a/src/libcmis/xml-utils.cxx +++ b/src/libcmis/xml-utils.cxx @@ -350,8 +350,11 @@ namespace libcmis if ( xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0 ) { xmlChar* pContent = xmlNodeGetContent( xpathObj->nodesetval->nodeTab[0] ); - value = string( ( char* )pContent ); - xmlFree( pContent ); + if ( pContent ) + { + value = string( ( char* )pContent ); + xmlFree( pContent ); + } } xmlXPathFreeObject( xpathObj ); } From 5d208a273dfc99988b28c71517dff52c44586306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Mon, 18 May 2026 18:20:10 +0000 Subject: [PATCH 11/16] Cap JSON nesting depth before passing to boost read_json --- qa/libcmis/test-jsonutils.cxx | 14 +++++++++++ src/libcmis/json-utils.cxx | 46 ++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/qa/libcmis/test-jsonutils.cxx b/qa/libcmis/test-jsonutils.cxx index 4a9fd498..22cfeabe 100644 --- a/qa/libcmis/test-jsonutils.cxx +++ b/qa/libcmis/test-jsonutils.cxx @@ -56,6 +56,7 @@ class JsonTest : public CppUnit::TestFixture { public: void parseTest( ); + void parseDeepNestingTest( ); void parseTypeTest( ); void createFromPropertyTest( ); void createFromPropertiesTest( ); @@ -64,6 +65,7 @@ class JsonTest : public CppUnit::TestFixture CPPUNIT_TEST_SUITE( JsonTest ); CPPUNIT_TEST( parseTest ); + CPPUNIT_TEST( parseDeepNestingTest ); CPPUNIT_TEST( parseTypeTest ); CPPUNIT_TEST( createFromPropertyTest ); CPPUNIT_TEST( createFromPropertiesTest ); @@ -114,6 +116,18 @@ void JsonTest::parseTest( ) CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong editable", string( "true"), editable ); } +void JsonTest::parseDeepNestingTest( ) +{ + // A response with thousands of [[[[...]]]] would blow the stack inside + // boost::property_tree::json_parser::read_json. Json::parse must + // refuse to feed it to read_json, falling back to the malformed-input + // path. + string deep( 5000, '[' ); + deep.append( 5000, ']' ); + Json json = Json::parse( deep ); + CPPUNIT_ASSERT_EQUAL( deep, json.toString( ) ); +} + void JsonTest::parseTypeTest( ) { Json json = parseFile( DATA_DIR "/gdrive/jsontest-good.json" ); diff --git a/src/libcmis/json-utils.cxx b/src/libcmis/json-utils.cxx index ff0be7cd..7de3b83a 100644 --- a/src/libcmis/json-utils.cxx +++ b/src/libcmis/json-utils.cxx @@ -178,19 +178,59 @@ void Json::add( const Json& json ) } } +namespace +{ + // boost::property_tree::json_parser::read_json has with no nesting cap. + // Refuse to feed read_json input whose bracket/brace nesting goes past + // some arbitrary large value. + constexpr size_t MAX_JSON_DEPTH = 100; + + bool exceedsMaxJsonDepth( const std::string& s ) + { + size_t depth = 0; + bool inString = false; + bool escape = false; + for ( char c : s ) + { + if ( escape ) { escape = false; continue; } + if ( inString ) + { + if ( c == '\\' ) escape = true; + else if ( c == '"' ) inString = false; + continue; + } + if ( c == '"' ) inString = true; + else if ( c == '[' || c == '{' ) + { + if ( ++depth > MAX_JSON_DEPTH ) return true; + } + else if ( ( c == ']' || c == '}' ) && depth > 0 ) --depth; + } + return false; + } + + Json malformedJsonFallback( const std::string& str ) + { + return Json( str.c_str( ) ); + } +} + Json Json::parse( const string& str ) { + if ( exceedsMaxJsonDepth( str ) ) + return malformedJsonFallback( str ); + ptree pTree; - std::stringstream ss( str ); + std::stringstream ss( str ); if ( ss.good( ) ) { - try + try { property_tree::json_parser::read_json( ss, pTree ); } catch ( boost::exception const& ) { - return Json( str.c_str( ) ); + return malformedJsonFallback( str ); } } return Json( pTree ); From 7ebb9082300644ef24bda165eacc8a6dc9fd19b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Tue, 19 May 2026 07:36:12 +0000 Subject: [PATCH 12/16] Bound the response size and stall time of every HTTP request Set three libcurl options at the top of each httpRunRequest, via a shared libcmis::applyTransferLimits: - CURLOPT_MAXFILESIZE_LARGE = 1 GiB. Well above any realistic editable document; the largest legitimate LibreOffice case we know of is a few hundreds of MiB. - CURLOPT_LOW_SPEED_LIMIT = 1 byte/s - CURLOPT_LOW_SPEED_TIME = 30s. Matches Collabora Online's net.connection_timeout_secs default which is the same no-progress-for-30s of Poco. --- src/libcmis/http-session.cxx | 12 ++++++++++++ src/libcmis/http-session.hxx | 7 +++++++ src/libcmis/sharepoint-session.cxx | 2 ++ 3 files changed, 21 insertions(+) diff --git a/src/libcmis/http-session.cxx b/src/libcmis/http-session.cxx index 07c966ad..d2187d00 100644 --- a/src/libcmis/http-session.cxx +++ b/src/libcmis/http-session.cxx @@ -165,6 +165,16 @@ void rejectControlChars( const string& s, const char* what ) throw Exception( string( "Invalid character in " ) + what ); } +void applyTransferLimits( CURL* curlHandle ) +{ + constexpr curl_off_t MAX_RESPONSE_SIZE = 1024L * 1024L * 1024L; // 1 GiB + constexpr long LOW_SPEED_TIME_SECS = 30L; + + curl_easy_setopt( curlHandle, CURLOPT_MAXFILESIZE_LARGE, MAX_RESPONSE_SIZE ); + curl_easy_setopt( curlHandle, CURLOPT_LOW_SPEED_LIMIT, 1L ); + curl_easy_setopt( curlHandle, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME_SECS ); +} + } HttpSession::HttpSession( string username, string password, bool noSslCheck, @@ -680,6 +690,8 @@ void HttpSession::httpRunRequest( string url, vector< string > headers, bool red for ( vector< string >::const_iterator it = headers.begin( ); it != headers.end( ); ++it ) libcmis::rejectControlChars( *it, "header" ); + libcmis::applyTransferLimits( m_curlHandle ); + // Redirect curl_easy_setopt( m_curlHandle, CURLOPT_FOLLOWLOCATION, redirect); diff --git a/src/libcmis/http-session.hxx b/src/libcmis/http-session.hxx index 92227a4f..2551d8e8 100644 --- a/src/libcmis/http-session.hxx +++ b/src/libcmis/http-session.hxx @@ -52,6 +52,13 @@ namespace libcmis { curl_slist_append at 7.86. */ void rejectControlChars(const std::string& s, const char* what); + + /** Bound a single HTTP request so a remote that wanted to grow the + client's buffer or pin its connection cannot do so indefinitely. + Cap response size at 1 GB and abort the transfer if there is no + data transfer for 30s. + */ + void applyTransferLimits(CURL* curlHandle); } class CurlException : public std::exception diff --git a/src/libcmis/sharepoint-session.cxx b/src/libcmis/sharepoint-session.cxx index 5ef52213..224388e3 100644 --- a/src/libcmis/sharepoint-session.cxx +++ b/src/libcmis/sharepoint-session.cxx @@ -195,6 +195,8 @@ void SharePointSession::httpRunRequest( string url, vector< string > headers, bo for ( vector< string >::const_iterator it = headers.begin( ); it != headers.end( ); ++it ) libcmis::rejectControlChars( *it, "header" ); + libcmis::applyTransferLimits( m_curlHandle ); + // Redirect curl_easy_setopt( m_curlHandle, CURLOPT_FOLLOWLOCATION, redirect); From 2ba6c0aedc54fffa5cc70f21b3579b7f0f66d988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Tue, 19 May 2026 07:57:29 +0000 Subject: [PATCH 13/16] consistently URL-encode every value going into an OAuth2 form body or query --- src/libcmis/oauth2-handler.cxx | 18 +++++++++--------- src/libcmis/oauth2-providers.cxx | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libcmis/oauth2-handler.cxx b/src/libcmis/oauth2-handler.cxx index f896db39..ba2ef279 100644 --- a/src/libcmis/oauth2-handler.cxx +++ b/src/libcmis/oauth2-handler.cxx @@ -77,12 +77,12 @@ OAuth2Handler::~OAuth2Handler( ) void OAuth2Handler::fetchTokens( string authCode ) { string post = - "code=" + authCode + - "&client_id=" + m_data->getClientId() + - "&redirect_uri=" + m_data->getRedirectUri() + + "code=" + libcmis::escape( authCode ) + + "&client_id=" + libcmis::escape( m_data->getClientId() ) + + "&redirect_uri=" + libcmis::escape( m_data->getRedirectUri() ) + "&grant_type=authorization_code" ; if(boost::starts_with(m_data->getTokenUrl(), "https://oauth2.googleapis.com/")) - post += "&client_secret=" + m_data->getClientSecret(); + post += "&client_secret=" + libcmis::escape( m_data->getClientSecret() ); else post += "&scope=" + libcmis::escape( m_data->getScope() ); @@ -110,11 +110,11 @@ void OAuth2Handler::refresh( ) { m_access = string( ); string post = - "refresh_token=" + m_refresh + - "&client_id=" + m_data->getClientId() + + "refresh_token=" + libcmis::escape( m_refresh ) + + "&client_id=" + libcmis::escape( m_data->getClientId() ) + "&grant_type=refresh_token" ; if(boost::starts_with(m_data->getTokenUrl(), "https://oauth2.googleapis.com/")) - post += "&client_secret=" + m_data->getClientSecret(); + post += "&client_secret=" + libcmis::escape( m_data->getClientSecret() ); istringstream is( post ); libcmis::HttpResponsePtr resp; @@ -136,9 +136,9 @@ string OAuth2Handler::getAuthURL( ) { return m_data->getAuthUrl() + "?scope=" + libcmis::escape( m_data->getScope( ) ) + - "&redirect_uri="+ m_data->getRedirectUri( ) + + "&redirect_uri=" + libcmis::escape( m_data->getRedirectUri( ) ) + "&response_type=code" + - "&client_id=" + m_data->getClientId( ); + "&client_id=" + libcmis::escape( m_data->getClientId( ) ); } string OAuth2Handler::getAccessToken( ) diff --git a/src/libcmis/oauth2-providers.cxx b/src/libcmis/oauth2-providers.cxx index b9a366b2..b81fefc4 100644 --- a/src/libcmis/oauth2-providers.cxx +++ b/src/libcmis/oauth2-providers.cxx @@ -74,10 +74,10 @@ string OAuth2Providers::OAuth2Alfresco( HttpSession* session, const string& auth if ( !parseResponse( res.c_str( ), loginPost, loginLink ) ) return string( ); - loginPost += "username="; - loginPost += string( username ); + loginPost += "username="; + loginPost += libcmis::escape( string( username ) ); loginPost += "&password="; - loginPost += string( password ); + loginPost += libcmis::escape( string( password ) ); loginPost += "&action=Grant"; istringstream loginIs( loginPost ); From 20a33cf19e6cb713294825656f03701ffc391d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Tue, 19 May 2026 08:55:26 +0000 Subject: [PATCH 14/16] Use xmlBuffer length when constucting string The xmlBuffer holds an explicit length via xmlBufferLength so use it. --- src/libcmis/atom-document.cxx | 4 ++-- src/libcmis/atom-folder.cxx | 2 +- src/libcmis/atom-object.cxx | 4 ++-- src/libcmis/ws-soap.cxx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libcmis/atom-document.cxx b/src/libcmis/atom-document.cxx index a78b67d3..9b8bd5f1 100644 --- a/src/libcmis/atom-document.cxx +++ b/src/libcmis/atom-document.cxx @@ -250,7 +250,7 @@ libcmis::DocumentPtr AtomDocument::checkOut( ) AtomObject::writeAtomEntry( writer, props, stream, string( ) ); xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); istringstream is( str ); xmlFreeTextWriter( writer ); @@ -352,7 +352,7 @@ libcmis::DocumentPtr AtomDocument::checkIn( bool isMajor, string comment, AtomObject::writeAtomEntry( writer, properties, stream, contentType ); xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); istringstream is( str ); xmlFreeTextWriter( writer ); diff --git a/src/libcmis/atom-folder.cxx b/src/libcmis/atom-folder.cxx index 7c479ff2..28fc1b75 100644 --- a/src/libcmis/atom-folder.cxx +++ b/src/libcmis/atom-folder.cxx @@ -152,7 +152,7 @@ libcmis::FolderPtr AtomFolder::createFolder( const PropertyPtrMap& properties ) AtomObject::writeAtomEntry( writer, properties, stream, string( ) ); xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); istringstream is( str ); xmlFreeTextWriter( writer ); diff --git a/src/libcmis/atom-object.cxx b/src/libcmis/atom-object.cxx index 7eff372a..b4d6a23b 100644 --- a/src/libcmis/atom-object.cxx +++ b/src/libcmis/atom-object.cxx @@ -131,7 +131,7 @@ libcmis::ObjectPtr AtomObject::updateProperties( const PropertyPtrMap& propertie AtomObject::writeAtomEntry( writer, properties, stream, string( ) ); xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); istringstream is( str ); xmlFreeTextWriter( writer ); @@ -268,7 +268,7 @@ void AtomObject::move( boost::shared_ptr< libcmis::Folder > source, boost::share AtomObject::writeAtomEntry( writer, getProperties( ), stream, string( ) ); xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); istringstream is( str ); xmlFreeTextWriter( writer ); xmlBufferFree( buf ); diff --git a/src/libcmis/ws-soap.cxx b/src/libcmis/ws-soap.cxx index 39590650..8b2e457d 100644 --- a/src/libcmis/ws-soap.cxx +++ b/src/libcmis/ws-soap.cxx @@ -338,7 +338,7 @@ string SoapRequest::createEnvelope( const string& username, const string& passwo xmlTextWriterEndElement( writer ); // End of S:Envelope xmlTextWriterEndDocument( writer ); - string str( ( const char * )xmlBufferContent( buf ) ); + string str( ( const char * )xmlBufferContent( buf ), xmlBufferLength( buf ) ); xmlFreeTextWriter( writer ); xmlBufferFree( buf ); From fd7ee07b2eff82371530bb54e5efa875f5eb88d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Tue, 19 May 2026 10:02:47 +0000 Subject: [PATCH 15/16] Re-assert the protocol restriction inside SharePointSession::httpRunRequest --- src/libcmis/http-session.hxx | 2 +- src/libcmis/sharepoint-session.cxx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libcmis/http-session.hxx b/src/libcmis/http-session.hxx index 2551d8e8..24f2327c 100644 --- a/src/libcmis/http-session.hxx +++ b/src/libcmis/http-session.hxx @@ -182,12 +182,12 @@ class HttpSession virtual void httpRunRequest( std::string url, std::vector< std::string > headers = std::vector< std::string > ( ), bool redirect = true ); + void initProtocols( ); private: void checkCredentials( ); void checkOAuth2( std::string url ); void oauth2Refresh( ); - void initProtocols( ); }; #endif diff --git a/src/libcmis/sharepoint-session.cxx b/src/libcmis/sharepoint-session.cxx index 224388e3..03debf09 100644 --- a/src/libcmis/sharepoint-session.cxx +++ b/src/libcmis/sharepoint-session.cxx @@ -195,6 +195,11 @@ void SharePointSession::httpRunRequest( string url, vector< string > headers, bo for ( vector< string >::const_iterator it = headers.begin( ); it != headers.end( ); ++it ) libcmis::rejectControlChars( *it, "header" ); + // The base class entry points already call initProtocols() right after + // their curl_easy_reset, but re-assert it here so this override stays + // safe if a future caller skips the reset+initProtocols pattern. + initProtocols(); + libcmis::applyTransferLimits( m_curlHandle ); // Redirect From 937acbd2166146bbe49d46ff08d93f303f6b935f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Tue, 19 May 2026 14:20:55 +0000 Subject: [PATCH 16/16] Throw a useful error when the OAuth token endpoint returns no access_token OAuth2Handler::fetchTokens and ::refresh end with m_access = jresp[ "access_token" ].toString( ); Per RFC 6749 section 5.2, every token-endpoint failure response is a JSON object with an "error" field and an optional "error_description". After assigning m_access, check whether it is empty and, if so, throw libcmis::Exception with those two fields included in the message. Behaviour change is limited to error reporting: every caller that previously got an empty m_access already failed on the next request, just with a less useful exception. --- src/libcmis/oauth2-handler.cxx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/libcmis/oauth2-handler.cxx b/src/libcmis/oauth2-handler.cxx index ba2ef279..6c7374e5 100644 --- a/src/libcmis/oauth2-handler.cxx +++ b/src/libcmis/oauth2-handler.cxx @@ -30,6 +30,7 @@ #include "oauth2-handler.hxx" +#include #include #include @@ -38,6 +39,29 @@ using namespace std; +namespace +{ + // Turn an RFC 6749 token-endpoint error response into a libcmis::Exception + // whose message names the failure mode the server reported (the standard + // "error" and "error_description" fields), so callers can distinguish + // "invalid_grant: refresh token expired" from "your network is down." + libcmis::Exception tokenError( const string& what, const Json& jresp ) + { + string err = jresp[ "error" ].toString( ); + string desc = jresp[ "error_description" ].toString( ); + string msg = what; + if ( !err.empty( ) ) + { + msg += " ("; + msg += err; + if ( !desc.empty( ) ) + msg += ": " + desc; + msg += ")"; + } + return libcmis::Exception( msg ); + } +} + OAuth2Handler::OAuth2Handler(HttpSession* session, libcmis::OAuth2DataPtr data) : m_session( session ), m_data( data ), @@ -104,6 +128,9 @@ void OAuth2Handler::fetchTokens( string authCode ) Json jresp = Json::parse( resp->getStream( )->str( ) ); m_access = jresp[ "access_token" ].toString( ); m_refresh = jresp[ "refresh_token" ].toString( ); + + if ( m_access.empty( ) ) + throw tokenError( "Token request returned no access_token", jresp ); } void OAuth2Handler::refresh( ) @@ -130,6 +157,9 @@ void OAuth2Handler::refresh( ) Json jresp = Json::parse( resp->getStream( )->str( ) ); m_access = jresp[ "access_token" ].toString(); + + if ( m_access.empty( ) ) + throw tokenError( "Token refresh returned no access_token", jresp ); } string OAuth2Handler::getAuthURL( )