CVE-2022-48579 (published 2023-08-07T04:15Z)

UnRAR before 6.2.3 allows extraction of files outside of the destination folder via symlink chains.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-59
cwe_name: Improper Link Resolution Before File Access ('Link Following')
description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/pmachapman/unrar
commit_details: repo_url: https://github.com/pmachapman/unrar
author: Peter Chapman
author_date: 2022-12-20 20:03:01+13:00
msg: Updated to 6.2.3
repository: repo_url: https://github.com/pmachapman/unrar
repo_name: pmachapman/unrar
description: Unrar source code from rarlab.com
repo_language: C++
owner: pmachapman
file_changes: filename: arcread.cpp
change_type: ModificationType.MODIFY
diff: @@ -1476,7 +1476,9 @@ bool Archive::ReadSubData(Array<byte> *UnpData,File *DestFile,bool TestMode)
   {
     if (SubHead.UnpSize>0x1000000)
     {
-      // So huge allocation must never happen in valid archives.
+      // Prevent the excessive allocation. When reading to memory, normally
+      // this function operates with reasonably small blocks, such as
+      // the archive comment, NTFS ACL or "Zone.Identifier" NTFS stream.
       uiMsg(UIERROR_SUBHEADERUNKNOWN,FileName);
       return false;
     }

programming_language: C++
filename: crypt.hpp
change_type: ModificationType.MODIFY
diff: @@ -15,10 +15,7 @@ enum CRYPT_METHOD {
 #define CRYPT_BLOCK_SIZE         16
 #define CRYPT_BLOCK_MASK         (CRYPT_BLOCK_SIZE-1) // 0xf
 
-// 2013.04.29: set to 15 for RAR 5.00 beta 1.
-// 2022.09.07: changed to 16 for upcoming RAR 6.20.
-#define CRYPT5_KDF_LG2_COUNT     16 // LOG2 of PDKDF2 iteration count.
-
+#define CRYPT5_KDF_LG2_COUNT     15 // LOG2 of PDKDF2 iteration count.
 #define CRYPT5_KDF_LG2_COUNT_MAX 24 // LOG2 of maximum accepted iteration count.
 #define CRYPT_VERSION             0 // Supported encryption version.
 

programming_language: C++
filename: dll.rc
change_type: ModificationType.MODIFY
diff: @@ -2,8 +2,8 @@
 #include <commctrl.h>
 
 VS_VERSION_INFO VERSIONINFO
-FILEVERSION 6, 20, 2, 681
-PRODUCTVERSION 6, 20, 2, 681
+FILEVERSION 6, 20, 3, 714
+PRODUCTVERSION 6, 20, 3, 714
 FILEOS VOS__WINDOWS32
 FILETYPE VFT_APP
 {
@@ -14,8 +14,8 @@ FILETYPE VFT_APP
       VALUE "CompanyName", "Alexander Roshal\0"
       VALUE "ProductName", "RAR decompression library\0"
       VALUE "FileDescription", "RAR decompression library\0"
-      VALUE "FileVersion", "6.20.2\0"
-      VALUE "ProductVersion", "6.20.2\0"
+      VALUE "FileVersion", "6.20.3\0"
+      VALUE "ProductVersion", "6.20.3\0"
       VALUE "LegalCopyright", "Copyright  Alexander Roshal 1993-2022\0"
       VALUE "OriginalFilename", "Unrar.dll\0"
     }

programming_language: C++
filename: extinfo.cpp
change_type: ModificationType.MODIFY
diff: @@ -112,6 +112,68 @@ static bool LinkInPath(const wchar *Name)
 }
 
 
+// Delete symbolic links in file path, if any, and replace them by directories.
+// Prevents extracting files outside of destination folder with symlink chains.
+bool LinksToDirs(const wchar *SrcName,const wchar *SkipPart,std::wstring &LastChecked)
+{
+  // Unlike Unix, Windows doesn't expand lnk1 in symlink targets like
+  // "lnk1/../dir", but converts the path to "dir". In Unix we need to call
+  // this function to prevent placing unpacked files outside of destination
+  // folder if previously we unpacked "dir/lnk1" -> "..",
+  // "dir/lnk2" -> "lnk1/.." and "dir/lnk2/anypath/poc.txt".
+  // We may still need this function to prevent abusing symlink chains
+  // in link source path if we remove detection of such chains
+  // in IsRelativeSymlinkSafe. This function seems to make other symlink
+  // related safety checks redundant, but for now we prefer to keep them too.
+  //
+  // 2022.12.01: the performance impact is minimized after adding the check
+  // against the previous path and enabling this verification only after
+  // extracting a symlink with ".." in target. So we enabled it for Windows
+  // as well for extra safety.
+//#ifdef _UNIX
+  wchar Path[NM];
+  if (wcslen(SrcName)>=ASIZE(Path))
+    return false;  // It should not be that long, skip.
+  wcsncpyz(Path,SrcName,ASIZE(Path));
+
+  size_t SkipLength=wcslen(SkipPart);
+
+  if (SkipLength>0 && wcsncmp(Path,SkipPart,SkipLength)!=0)
+    SkipLength=0; // Parameter validation, not really needed now.
+
+  // Do not check parts already checked in previous path to improve performance.
+  for (uint I=0;Path[I]!=0 && I<LastChecked.size() && Path[I]==LastChecked[I];I++)
+    if (IsPathDiv(Path[I]) && I>SkipLength)
+      SkipLength=I;
+
+  wchar *Name=Path;
+  if (SkipLength>0)
+  {
+    // Avoid converting symlinks in destination path part specified by user.
+    Name+=SkipLength;
+    while (IsPathDiv(*Name))
+      Name++;
+  }
+
+  for (wchar *s=Path+wcslen(Path)-1;s>Name;s--)
+    if (IsPathDiv(*s))
+    {
+      *s=0;
+      FindData FD;
+      if (FindFile::FastFind(Path,&FD,true) && FD.IsLink)
+#ifdef _WIN_ALL
+        if (!DelDir(Path))
+#else
+        if (!DelFile(Path))
+#endif
+          return false; // Couldn't delete the symlink to replace it with directory.
+    }
+  LastChecked=SrcName;
+//#endif
+  return true;
+}
+
+
 bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *PrepSrcName,const wchar *TargetName)
 {
   // Catch root dir based /path/file paths also as stuff like \\?\.
@@ -131,10 +193,14 @@ bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *Pr
       UpLevels++;
     TargetName++;
   }
-  // If link target includes "..", it must not have another links
-  // in the path, because they can bypass our safety check. For example,
+  // If link target includes "..", it must not have another links in its
+  // source path, because they can bypass our safety check. For example,
   // suppose we extracted "lnk1" -> "." first and "lnk1/lnk2" -> ".." next
-  // or "dir/lnk1" -> ".." first and "dir/lnk1/lnk2" -> ".." next.
+  // or "dir/lnk1" -> ".." first, "dir/lnk1/lnk2" -> ".." next and
+  // file "dir/lnk1/lnk2/poc.txt" last.
+  // Do not confuse with link chains in target, this is in link source path.
+  // It is important for Windows too, though this check can be omitted
+  // if LinksToDirs is invoked in Windows as well.
   if (UpLevels>0 && LinkInPath(PrepSrcName))
     return false;
     
@@ -160,15 +226,26 @@ bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *Pr
 }
 
 
-bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName)
+bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName,bool &UpLink)
 {
+  // Returning true in Uplink indicates that link target might include ".."
+  // and enables additional checks. It is ok to falsely return true here,
+  // as it implies only the minor performance penalty. But we shall always
+  // return true for links with ".." in target for security reason.
+
+  UpLink=true; // Assume the target might include potentially unsafe "..".
+#if defined(SAVE_LINKS) && defined(_UNIX) || defined(_WIN_ALL)
+  if (Arc.Format==RARFMT50) // For RAR5 archives we can check RedirName for both Unix and Windows.
+    UpLink=wcsstr(Arc.FileHead.RedirName,L"..")!=NULL;
+#endif
+
 #if defined(SAVE_LINKS) && defined(_UNIX)
   // For RAR 3.x archives we process links even in test mode to skip link data.
   if (Arc.Format==RARFMT15)
-    return ExtractUnixLink30(Cmd,DataIO,Arc,LinkName);
+    return ExtractUnixLink30(Cmd,DataIO,Arc,LinkName,UpLink);
   if (Arc.Format==RARFMT50)
     return ExtractUnixLink50(Cmd,LinkName,&Arc.FileHead);
-#elif defined _WIN_ALL
+#elif defined(_WIN_ALL)
   // RAR 5.0 archives store link information in file header, so there is
   // no need to additionally test it if we do not create a file.
   if (Arc.Format==RARFMT50)

programming_language: C++
filename: extinfo.hpp
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,9 @@
 #ifndef _RAR_EXTINFO_
 #define _RAR_EXTINFO_
 
+bool LinksToDirs(const wchar *SrcName,const wchar *SkipPart,std::wstring &LastChecked);
 bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *PrepSrcName,const wchar *TargetName);
-bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName);
+bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName,bool &UpLink);
 #ifdef _UNIX
 void SetUnixOwner(Archive &Arc,const wchar *FileName);
 #endif

programming_language: C++
filename: extract.cpp
change_type: ModificationType.MODIFY
diff: @@ -12,6 +12,12 @@ CmdExtract::CmdExtract(CommandData *Cmd)
   memset(Analyze,0,sizeof(*Analyze));
 
   TotalFileCount=0;
+
+  // Common for all archives involved. Set here instead of DoExtract()
+  // to use in unrar.dll too. Allows to avoid LinksToDirs() calls
+  // and save CPU time in no symlinks including ".." in target were extracted.
+  UpLinkExtracted=false;
+
   Unp=new Unpack(&DataIO);
 #ifdef RAR_SMP
   Unp->SetThreads(Cmd->Threads);
@@ -125,6 +131,8 @@ void CmdExtract::ExtractArchiveInit(Archive &Arc)
   ArcAnalyzed=false;
 
   StartTime.SetCurrentTime();
+
+  LastCheckedSymlink.clear();
 }
 
 
@@ -618,6 +626,10 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
       wcsncpyz(DestFileName,Cmd->DllDestName,ASIZE(DestFileName));
 #endif
 
+    if (ExtrFile && Command!='P' && !Cmd->Test && !Cmd->AbsoluteLinks &&
+        UpLinkExtracted)
+      ExtrFile=LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);
+
     File CurFile;
 
     bool LinkEntry=Arc.FileHead.RedirType!=FSREDIR_NONE;
@@ -747,7 +759,17 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
         if (Type==FSREDIR_HARDLINK || Type==FSREDIR_FILECOPY)
         {
           wchar RedirName[NM];
-          ConvertPath(Arc.FileHead.RedirName,RedirName,ASIZE(RedirName));
+        
+          // 2022.11.15: Might be needed when unpacking WinRAR 5.0 links with
+          // Unix RAR. WinRAR 5.0 used \ path separators here, when beginning
+          // from 5.10 even Windows version uses / internally and converts
+          // them to \ when reading FHEXTRA_REDIR.
+          // We must perform this conversion before ConvertPath call,
+          // so paths mixing different slashes like \dir1/dir2\file are
+          // processed correctly.
+          SlashToNative(Arc.FileHead.RedirName,RedirName,ASIZE(RedirName));
+
+          ConvertPath(RedirName,RedirName,ASIZE(RedirName));
 
           wchar NameExisting[NM];
           ExtrPrepareName(Arc,RedirName,NameExisting,ASIZE(NameExisting));
@@ -761,7 +783,22 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
           if (Type==FSREDIR_UNIXSYMLINK || Type==FSREDIR_WINSYMLINK || Type==FSREDIR_JUNCTION)
           {
             if (FileCreateMode)
-              LinkSuccess=ExtractSymlink(Cmd,DataIO,Arc,DestFileName);
+            {
+              bool UpLink;
+              LinkSuccess=ExtractSymlink(Cmd,DataIO,Arc,DestFileName,UpLink);
+              UpLinkExtracted|=LinkSuccess && UpLink;
+
+              // We do not actually need to reset the cache here if we cache
+              // only the single last checked path, because at this point
+              // it will always contain the link own path and link can't
+              // overwrite its parent folder. But if we ever decide to cache
+              // several already checked paths, we'll need to reset them here.
+              // Otherwise if no files were created in one of such paths,
+              // let's say because of file create error, it might be possible
+              // to overwrite the path with link and avoid checks. We keep this
+              // code here as a reminder in case of possible modifications.
+              LastCheckedSymlink.clear(); // Reset cache for safety reason.
+            }
           }
           else
           {
@@ -948,8 +985,6 @@ void CmdExtract::UnstoreFile(ComprDataIO &DataIO,int64 DestUnpSize)
 
 bool CmdExtract::ExtractFileCopy(File &New,wchar *ArcName,const wchar *RedirName,wchar *NameNew,wchar *NameExisting,size_t NameExistingSize,int64 UnpSize)
 {
-  SlashToNative(NameExisting,NameExisting,NameExistingSize); // Not needed for RAR 5.1+ archives.
-
   File Existing;
   if (!Existing.Open(NameExisting))
   {
@@ -1269,6 +1304,8 @@ void CmdExtract::ExtrCreateDir(Archive &Arc,const wchar *ArcFileName)
         DirExist=FileExist(DestFileName) && IsDir(GetFileAttr(DestFileName));
         if (!DirExist)
         {
+          if (!Cmd->AbsoluteLinks && UpLinkExtracted)
+            LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);
           CreatePath(DestFileName,true,Cmd->DisableNames);
           MDCode=MakeDir(DestFileName,!Cmd->IgnoreGeneralAttr,Arc.FileHead.FileAttr);
         }
@@ -1350,6 +1387,8 @@ bool CmdExtract::ExtrCreateFile(Archive &Arc,File &CurFile)
 
           MakeNameUsable(DestFileName,true);
 
+          if (!Cmd->AbsoluteLinks && UpLinkExtracted)
+            LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);
           CreatePath(DestFileName,true,Cmd->DisableNames);
           if (FileCreate(Cmd,&CurFile,DestFileName,ASIZE(DestFileName),&UserReject,Arc.FileHead.UnpSize,&Arc.FileHead.mtime,true))
           {

programming_language: C++
filename: extract.hpp
change_type: ModificationType.MODIFY
diff: @@ -71,6 +71,12 @@ class CmdExtract
     bool PrevProcessed; // If previous file was successfully extracted or tested.
     wchar DestFileName[NM];
     bool PasswordCancelled;
+    bool UpLinkExtracted; // At least one symlink with ".." in target was extracted.
+
+    // Last path checked for symlinks. We use it to improve the performance,
+    // so we do not check recently checked folders again.
+    std::wstring LastCheckedSymlink;
+
 #if defined(_WIN_ALL) && !defined(SFX_MODULE) && !defined(SILENT)
     bool Fat32,NotFat32;
 #endif

programming_language: C++
filename: hardlinks.cpp
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,5 @@
 bool ExtractHardlink(CommandData *Cmd,wchar *NameNew,wchar *NameExisting,size_t NameExistingSize)
 {
-  SlashToNative(NameExisting,NameExisting,NameExistingSize); // Not needed for RAR 5.1+ archives.
-
   if (!FileExist(NameExisting))
   {
     uiMsg(UIERROR_HLINKCREATE,NameNew);

programming_language: C++
filename: model.cpp
change_type: ModificationType.MODIFY
diff: @@ -532,13 +532,15 @@ inline bool RARPPM_CONTEXT::decodeSymbol2(ModelPPM *Model)
     Model->Coder.SubRange.LowCount=HiCnt;
     Model->Coder.SubRange.HighCount=Model->Coder.SubRange.scale;
     i=NumStats-Model->NumMasked;
-    pps--;
+
+    // 2022.12.02: we removed pps-- here and changed the code below to avoid
+    // "array subscript -1 is outside array bounds" warning in some compilers.
     do 
     { 
-      pps++;
       if (pps>=ps+ASIZE(ps)) // Extra safety check.
         return false;
       Model->CharMask[(*pps)->Symbol]=Model->EscCount; 
+      pps++;
     } while ( --i );
     psee2c->Summ += Model->Coder.SubRange.scale;
     Model->NumMasked = NumStats;

programming_language: C++
filename: pathfn.cpp
change_type: ModificationType.MODIFY
diff: @@ -31,11 +31,17 @@ wchar* ConvertPath(const wchar *SrcPath,wchar *DestPath,size_t DestSize)
     const wchar *s=DestPtr;
     if (s[0]!=0 && IsDriveDiv(s[1]))
       s+=2;
-    if (s[0]=='\\' && s[1]=='\\')
+
+    // Skip UNC Windows \\server\share\ or Unix //server/share/
+    if (IsPathDiv(s[0]) && IsPathDiv(s[1]))
     {
-      const wchar *Slash=wcschr(s+2,'\\');
-      if (Slash!=NULL && (Slash=wcschr(Slash+1,'\\'))!=NULL)
-        s=Slash+1;
+      uint SlashCount=0;
+      for (const wchar *t=s+2;*t!=0;t++)
+        if (IsPathDiv(*t) && ++SlashCount==2)
+        {
+          s=t+1; // Found two more path separators after leading two.
+          break;
+        }
     }
     for (const wchar *t=s;*t!=0;t++)
       if (IsPathDiv(*t))

programming_language: C++
filename: timefn.hpp
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,17 @@ class RarTime
 
     // Internal time representation in 1/TICKS_PER_SECOND since 01.01.1601.
     // We use nanoseconds here to handle the high precision Unix time.
+    // It allows dates up to July 2185.
+    //
+    // If we'll ever need to extend the date range, we can define a lower
+    // precision Windows version of TICKS_PER_SECOND. But then Unix and Windows
+    // versions can differ in least significant digits of "lt" time output
+    // for Unix archives.
+    // Alternatively we can introduce 'bool HighPrecision' set to true
+    // in SetUnixNS() and TicksPerSecond() instead of constant above.
+    // It might be more reliable than defining TicksPerSecond variable,
+    // which wouldn't survive memset of any structure hosting RarTime.
+    // We would need to eliminate all such memsets in the entire code first.
     uint64 itime;
   public:
     // RarLocalTime::Reminder precision. Must be equal to TICKS_PER_SECOND.

programming_language: C++
filename: ulinks.cpp
change_type: ModificationType.MODIFY
diff: @@ -70,7 +70,8 @@ static bool SafeCharToWide(const char *Src,wchar *Dest,size_t DestSize)
 }
 
 
-bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName)
+static bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,
+                              const wchar *LinkName,bool &UpLink)
 {
   char Target[NM];
   if (IsLink(Arc.FileHead.FileAttr))
@@ -100,13 +101,14 @@ bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const w
     if (!Cmd->AbsoluteLinks && (IsFullPath(TargetW) ||
         !IsRelativeSymlinkSafe(Cmd,Arc.FileHead.FileName,LinkName,TargetW)))
       return false;
+    UpLink=strstr(Target,"..")!=NULL;
     return UnixSymlink(Cmd,Target,LinkName,&Arc.FileHead.mtime,&Arc.FileHead.atime);
   }
   return false;
 }
 
 
-bool ExtractUnixLink50(CommandData *Cmd,const wchar *Name,FileHeader *hd)
+static bool ExtractUnixLink50(CommandData *Cmd,const wchar *Name,FileHeader *hd)
 {
   char Target[NM];
   WideToChar(hd->RedirName,Target,ASIZE(Target));

programming_language: C++
filename: version.hpp
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 #define RARVER_MAJOR     6
 #define RARVER_MINOR    20
-#define RARVER_BETA      2
-#define RARVER_DAY      12
-#define RARVER_MONTH    11
+#define RARVER_BETA      3
+#define RARVER_DAY      15
+#define RARVER_MONTH    12
 #define RARVER_YEAR   2022

programming_language: Ruby
filename: win32stm.cpp
change_type: ModificationType.MODIFY
diff: @@ -118,8 +118,12 @@ void ExtractStreams(Archive &Arc,const wchar *FileName,bool TestMode)
   if ((fd.FileAttr & FILE_ATTRIBUTE_READONLY)!=0)
     SetFileAttr(FileName,fd.FileAttr & ~FILE_ATTRIBUTE_READONLY);
   File CurFile;
-  if (CurFile.WCreate(FullName) && Arc.ReadSubData(NULL,&CurFile,false))
-    CurFile.Close();
+
+  if (CurFile.WCreate(FullName))
+  {
+    if (Arc.ReadSubData(NULL,&CurFile,false))
+      CurFile.Close();
+  }
 
   // Restoring original file timestamps.
   File HostFile;

programming_language: C++