From 39d576e416d83eabd1c9c4209838103715e8931d Mon Sep 17 00:00:00 2001 From: freyers Date: Fri, 15 May 2026 22:26:23 +0000 Subject: [PATCH] fix(resourceinfo): reject manifest paths escaping the destination m_relativePath comes straight from the manifest and was joined onto basePath and written with no containment check, so an entry like '../../x' (zip-slip) wrote files anywhere on disk. Add a containment check (weakly_canonical + lexically_relative, lexical fallback) and refuse any Put* whose target escapes basePath. --- src/ResourceInfo/ResourceInfo.cpp | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/ResourceInfo/ResourceInfo.cpp b/src/ResourceInfo/ResourceInfo.cpp index 1a2394b..ed05368 100644 --- a/src/ResourceInfo/ResourceInfo.cpp +++ b/src/ResourceInfo/ResourceInfo.cpp @@ -21,6 +21,47 @@ namespace CarbonResources { +namespace +{ + +bool IsPathWithinBase( const std::filesystem::path& base, const std::filesystem::path& candidate ) +{ + std::error_code ec; + + std::filesystem::path normalizedBase = std::filesystem::weakly_canonical( base, ec ); + if( ec ) + { + normalizedBase = base.lexically_normal(); + } + + ec.clear(); + + std::filesystem::path normalizedCandidate = std::filesystem::weakly_canonical( candidate, ec ); + if( ec ) + { + normalizedCandidate = candidate.lexically_normal(); + } + + std::filesystem::path relative = normalizedCandidate.lexically_relative( normalizedBase ); + + if( relative.empty() ) + { + return false; + } + + for( const auto& part : relative ) + { + if( part == ".." ) + { + return false; + } + } + + return true; +} + +} + std::string Location::CalculateLocationFromChecksums( const std::string& relativePathChecksum, const std::string& dataChecksum ) const { std::stringstream ss; @@ -330,6 +371,11 @@ Result ResourceInfo::PutDataLocalRelative( ResourcePutDataParams& params ) const std::filesystem::path path = params.resourceDestinationSettings.basePath / m_relativePath.GetValue(); + if( !IsPathWithinBase( params.resourceDestinationSettings.basePath, path ) ) + { + return Result{ ResultType::MALFORMED_RESOURCE_INPUT }; + } + bool res = ResourceTools::SaveFile( path, data ); if( res ) @@ -349,6 +395,11 @@ Result ResourceInfo::PutDataRemoteCdn( ResourcePutDataParams& params ) const // Construct path std::filesystem::path dataPath = params.resourceDestinationSettings.basePath / m_location.GetValue().ToString(); + if( !IsPathWithinBase( params.resourceDestinationSettings.basePath, dataPath ) ) + { + return Result{ ResultType::MALFORMED_RESOURCE_INPUT }; + } + std::string compressedData; if( !ResourceTools::GZipCompressData( data, compressedData ) ) @@ -375,6 +426,11 @@ Result ResourceInfo::PutDataLocalCdn( ResourcePutDataParams& params ) const // Construct path std::filesystem::path dataPath = params.resourceDestinationSettings.basePath / m_location.GetValue().ToString(); + if( !IsPathWithinBase( params.resourceDestinationSettings.basePath, dataPath ) ) + { + return Result{ ResultType::MALFORMED_RESOURCE_INPUT }; + } + bool res = ResourceTools::SaveFile( dataPath, data ); if( res ) @@ -391,6 +447,11 @@ Result ResourceInfo::PutDataStreamLocalRelative( ResourcePutDataStreamParams& pa { std::filesystem::path path = params.resourceDestinationSettings.basePath / m_relativePath.GetValue(); + if( !IsPathWithinBase( params.resourceDestinationSettings.basePath, path ) ) + { + return Result{ ResultType::MALFORMED_RESOURCE_INPUT }; + } + bool res = params.dataStream->StartWrite( path ); if( res ) @@ -408,6 +469,11 @@ Result ResourceInfo::PutDataStreamLocalCdn( ResourcePutDataStreamParams& params // Construct path std::filesystem::path dataPath = params.resourceDestinationSettings.basePath / m_location.GetValue().ToString(); + if( !IsPathWithinBase( params.resourceDestinationSettings.basePath, dataPath ) ) + { + return Result{ ResultType::MALFORMED_RESOURCE_INPUT }; + } + bool res = params.dataStream->StartWrite( dataPath ); if( res )