CVE-2022-48579 (C++, published 2023-08-07T04:15Z)
Description: value: UnRAR before 6.2.3 allows extraction of files outside of the destination folder via symlink chains.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-59 \u2014 Improper Link Resolution Before File Access ('Link Following')
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.
Repository: https://github.com/pmachapman/unrar
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 message: Updated to 6.2.3


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();
  }