CVE-2022-48579: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: UnRAR before 6.2.3 allows extraction of files outside of the destination folder via symlink chains.
CWE 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.
CVSS v3 base score: 7.5
Published: 2023-08-07T04:15Z
Repository: https://github.com/pmachapman/unrar
Language: C++
Files changed: arcread.cpp, crypt.hpp, dll.rc, extinfo.cpp, extinfo.hpp, extract.cpp, extract.hpp, hardlinks.cpp, model.cpp, pathfn.cpp, timefn.hpp, ulinks.cpp, version.hpp, win32stm.cpp
Commit date: 2022-12-20 20:03:01 +1300
Commit message: Updated to 6.2.3

Diff: diff --git a/arcread.cpp b/arcread.cpp
index 3d12684..c533c4a 100644
--- a/arcread.cpp
+++ b/arcread.cpp
@@ -1474,11 +1474,13 @@ bool Archive::ReadSubData(Array<byte> *UnpData,File *DestFile,bool TestMode)
 
   if (DestFile==NULL)
   {
     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;
     }
     if (UnpData==NULL)
       SubDataIO.SetTestMode(true);
diff --git a/crypt.hpp b/crypt.hpp
index 22d18e7..9dc0215 100644
--- a/crypt.hpp
+++ b/crypt.hpp
@@ -13,14 +13,11 @@ enum CRYPT_METHOD {
 #define SIZE_PSWCHECK_CSUM        4
 
 #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.
 
 
 class CryptData
diff --git a/dll.rc b/dll.rc
index 4105de9..eedfefe 100644
--- a/dll.rc
+++ b/dll.rc
@@ -1,23 +1,23 @@
 #include <windows.h>
 #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
 {
   BLOCK "StringFileInfo"
   {
     BLOCK "040904E4"
     {
       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"
     }
   }
   BLOCK "VarFileInfo"
diff --git a/extinfo.cpp b/extinfo.cpp
index 5cb90a4..0f25f31 100644
--- a/extinfo.cpp
+++ b/extinfo.cpp
@@ -110,10 +110,72 @@ static bool LinkInPath(const wchar *Name)
     }
   return false;
 }
 
 
+// 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 \\?\.
   // Do not check PrepSrcName here, it can be root based if destination path
   // is a root based.
@@ -129,14 +191,18 @@ bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *Pr
               (Pos==0 || IsPathDiv(*(TargetName-1)));
     if (Dot2)
       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;
     
   // We could check just prepared src name, but for extra safety
   // we check both original (as from archive header) and prepared
@@ -158,19 +224,30 @@ bool IsRelativeSymlinkSafe(CommandData *Cmd,const wchar *SrcName,const wchar *Pr
 
   return AllowedDepth>=UpLevels && PrepAllowedDepth>=UpLevels;
 }
 
 
-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)
     return CreateReparsePoint(Cmd,LinkName,&Arc.FileHead);
 #endif
diff --git a/extinfo.hpp b/extinfo.hpp
index f3c7511..d8551d4 100644
--- a/extinfo.hpp
+++ b/extinfo.hpp
@@ -1,10 +1,11 @@
 #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
 
 bool ExtractHardlink(CommandData *Cmd,wchar *NameNew,wchar *NameExisting,size_t NameExistingSize);
diff --git a/extract.cpp b/extract.cpp
index 406b1a0..b5de6cd 100644
--- a/extract.cpp
+++ b/extract.cpp
@@ -10,10 +10,16 @@ CmdExtract::CmdExtract(CommandData *Cmd)
   ArcAnalyzed=false;
   Analyze=new AnalyzeData;
   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);
 #endif
 }
@@ -123,10 +129,12 @@ void CmdExtract::ExtractArchiveInit(Archive &Arc)
   AnySolidDataUnpackedWell=false;
 
   ArcAnalyzed=false;
 
   StartTime.SetCurrentTime();
+
+  LastCheckedSymlink.clear();
 }
 
 
 EXTRACT_ARC_CODE CmdExtract::ExtractArchive()
 {
@@ -616,10 +624,14 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
 #ifdef RARDLL
     if (*Cmd->DllDestName!=0)
       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;
     if (LinkEntry && (Arc.FileHead.RedirType!=FSREDIR_FILECOPY))
     {
@@ -745,11 +757,21 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
         FILE_SYSTEM_REDIRECT Type=Arc.FileHead.RedirType;
 
         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));
           if (FileCreateMode && *NameExisting!=0) // *NameExisting can be 0 in case of excessive -ap switch.
             if (Type==FSREDIR_HARDLINK)
@@ -759,11 +781,26 @@ bool CmdExtract::ExtractCurrentFile(Archive &Arc,size_t HeaderSize,bool &Repeat)
         }
         else
           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
           {
             uiMsg(UIERROR_UNKNOWNEXTRA,Arc.FileName,ArcFileName);
             LinkSuccess=false;
@@ -946,12 +983,10 @@ 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))
   {
     bool OpenFailed=true;
     // If we couldn't find the existing file, check if match is present
@@ -1267,10 +1302,12 @@ void CmdExtract::ExtrCreateDir(Archive &Arc,const wchar *ArcFileName)
         uiMsg(UIERROR_RENAMING,Arc.FileName,OrigName,DestFileName);
 #endif
         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);
         }
       }
     }
@@ -1348,10 +1385,12 @@ bool CmdExtract::ExtrCreateFile(Archive &Arc,File &CurFile)
           wchar OrigName[ASIZE(DestFileName)];
           wcsncpyz(OrigName,DestFileName,ASIZE(OrigName));
 
           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))
           {
 #ifndef SFX_MODULE
             uiMsg(UIERROR_RENAMING,Arc.FileName,OrigName,DestFileName);
diff --git a/extract.hpp b/extract.hpp
index aac463c..0fb5a96 100644
--- a/extract.hpp
+++ b/extract.hpp
@@ -69,10 +69,16 @@ class CmdExtract
 
     bool GlobalPassword;
     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
   public:
     CmdExtract(CommandData *Cmd);
diff --git a/hardlinks.cpp b/hardlinks.cpp
index 40cc0aa..171b5fa 100644
--- a/hardlinks.cpp
+++ b/hardlinks.cpp
@@ -1,9 +1,7 @@
 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);
     uiMsg(UIERROR_NOLINKTARGET);
     ErrHandler.SetErrorCode(RARX_CREATE);
diff --git a/model.cpp b/model.cpp
index 83391c5..e4f9e3c 100644
--- a/model.cpp
+++ b/model.cpp
@@ -530,17 +530,19 @@ inline bool RARPPM_CONTEXT::decodeSymbol2(ModelPPM *Model)
   else
   {
     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;
   }
   return true;
diff --git a/pathfn.cpp b/pathfn.cpp
index 983bd74..162eda2 100644
--- a/pathfn.cpp
+++ b/pathfn.cpp
@@ -29,15 +29,21 @@ wchar* ConvertPath(const wchar *SrcPath,wchar *DestPath,size_t DestSize)
   while (*DestPtr!=0)
   {
     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))
         s=t+1;
       else
diff --git a/timefn.hpp b/timefn.hpp
index 5271361..49b61e8 100644
--- a/timefn.hpp
+++ b/timefn.hpp
@@ -20,10 +20,21 @@ class RarTime
   private:
     static const uint TICKS_PER_SECOND = 1000000000; // Internal precision.
 
     // 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.
     // Unlike TICKS_PER_SECOND, it is a public field.
     static const uint REMINDER_PRECISION = TICKS_PER_SECOND;
diff --git a/ulinks.cpp b/ulinks.cpp
index 5f53e3a..141a97f 100644
--- a/ulinks.cpp
+++ b/ulinks.cpp
@@ -68,11 +68,12 @@ static bool SafeCharToWide(const char *Src,wchar *Dest,size_t DestSize)
       DestChars++;
   return SrcChars==DestChars;
 }
 
 
-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))
   {
     size_t DataSize=(size_t)Arc.FileHead.PackSize;
@@ -98,17 +99,18 @@ bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const w
     // can include the destination path as a prefix, which can
     // confuse IsRelativeSymlinkSafe algorithm.
     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));
   if (hd->RedirType==FSREDIR_WINSYMLINK || hd->RedirType==FSREDIR_JUNCTION)
   {
diff --git a/version.hpp b/version.hpp
index 2749f7f..97e5b8f 100644
--- a/version.hpp
+++ b/version.hpp
@@ -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
diff --git a/win32stm.cpp b/win32stm.cpp
index 9907de7..048fd86 100644
--- a/win32stm.cpp
+++ b/win32stm.cpp
@@ -116,12 +116,16 @@ void ExtractStreams(Archive &Arc,const wchar *FileName,bool TestMode)
   bool HostFound=FindFile::FastFind(FileName,&fd);
 
   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;
   if (HostFound && HostFile.Open(FileName,FMF_OPENSHARED|FMF_UPDATE))
     SetFileTime(HostFile.GetHandle(),&fd.ftCreationTime,&fd.ftLastAccessTime,

Vulnerable code:       // So huge allocation must never happen in valid archives.
// 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.

FILEVERSION 6, 20, 2, 681
PRODUCTVERSION 6, 20, 2, 681
      VALUE "FileVersion", "6.20.2\0"
      VALUE "ProductVersion", "6.20.2\0"
  // If link target includes "..", it must not have another links
  // in the path, because they can bypass our safety check. For example,
  // or "dir/lnk1" -> ".." first and "dir/lnk1/lnk2" -> ".." next.
bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName)
    return ExtractUnixLink30(Cmd,DataIO,Arc,LinkName);
#elif defined _WIN_ALL
bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName);
          ConvertPath(Arc.FileHead.RedirName,RedirName,ASIZE(RedirName));
              LinkSuccess=ExtractSymlink(Cmd,DataIO,Arc,DestFileName);
  SlashToNative(NameExisting,NameExisting,NameExistingSize); // Not needed for RAR 5.1+ archives.

  SlashToNative(NameExisting,NameExisting,NameExistingSize); // Not needed for RAR 5.1+ archives.

    pps--;
      pps++;
    if (s[0]=='\\' && s[1]=='\\')
      const wchar *Slash=wcschr(s+2,'\\');
      if (Slash!=NULL && (Slash=wcschr(Slash+1,'\\'))!=NULL)
        s=Slash+1;
bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName)
bool ExtractUnixLink50(CommandData *Cmd,const wchar *Name,FileHeader *hd)
#define RARVER_BETA      2
#define RARVER_DAY      12
#define RARVER_MONTH    11
  if (CurFile.WCreate(FullName) && Arc.ReadSubData(NULL,&CurFile,false))
    CurFile.Close();
Fixed code:       // 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.
#define CRYPT5_KDF_LG2_COUNT     15 // LOG2 of PDKDF2 iteration count.
FILEVERSION 6, 20, 3, 714
PRODUCTVERSION 6, 20, 3, 714
      VALUE "FileVersion", "6.20.3\0"
      VALUE "ProductVersion", "6.20.3\0"
// 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;
}


  // If link target includes "..", it must not have another links in its
  // source path, because they can bypass our safety check. For example,
  // 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.
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

    return ExtractUnixLink30(Cmd,DataIO,Arc,LinkName,UpLink);
#elif defined(_WIN_ALL)
bool LinksToDirs(const wchar *SrcName,const wchar *SkipPart,std::wstring &LastChecked);
bool ExtractSymlink(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,const wchar *LinkName,bool &UpLink);

  // 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;


  LastCheckedSymlink.clear();
    if (ExtrFile && Command!='P' && !Cmd->Test && !Cmd->AbsoluteLinks &&
        UpLinkExtracted)
      ExtrFile=LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);

        
          // 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));
            {
              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.
            }
          if (!Cmd->AbsoluteLinks && UpLinkExtracted)
            LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);
          if (!Cmd->AbsoluteLinks && UpLinkExtracted)
            LinksToDirs(DestFileName,Cmd->ExtrPath,LastCheckedSymlink);
    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;


    // 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.
      pps++;

    // Skip UNC Windows \\server\share\ or Unix //server/share/
    if (IsPathDiv(s[0]) && IsPathDiv(s[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;
        }
    // 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.
static bool ExtractUnixLink30(CommandData *Cmd,ComprDataIO &DataIO,Archive &Arc,
                              const wchar *LinkName,bool &UpLink)
    UpLink=strstr(Target,"..")!=NULL;
static bool ExtractUnixLink50(CommandData *Cmd,const wchar *Name,FileHeader *hd)
#define RARVER_BETA      3
#define RARVER_DAY      15
#define RARVER_MONTH    12

  if (CurFile.WCreate(FullName))
  {
    if (Arc.ReadSubData(NULL,&CurFile,false))
      CurFile.Close();
  }