Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-rarfile for openSUSE:Factory 
checked in at 2026-10-26 10:03:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-rarfile (Old)
 and      /work/SRC/openSUSE:Factory/.python-rarfile.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-rarfile"

Mon Oct 26 10:03:38 2026 rev:11 rq:1392580 version:4.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-rarfile/python-rarfile.changes    
2025-06-24 20:50:49.884443377 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-rarfile.new.2252680/python-rarfile.changes   
    2026-10-26 10:05:08.593648677 +0100
@@ -1,0 +2,18 @@
+Sun Oct 25 21:39:44 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.5:
+  * Skip comments that are larger than 256k.
+  * Truncate filenames at NUL byte.
+  * Skip CRC check for some old subblocks.  Previously rarfile
+    tried to calculate header CRC by reading data payload for
+    those, but that could cause excessive allocations.
+  * RAR5: Reattempt passwords after trying wrong password when
+    header encryption is used.
+  * Restore fd position in file type detection functions.
+  * Disallow extraction outside extraction path, in case of
+    existing symlink.
+  * Disallow creating symlinks to outside of extraction path.
+  * Apply length limit to passwords, so too long password give
+    same result as for unrar.
+
+-------------------------------------------------------------------

Old:
----
  rarfile-4.2.tar.gz

New:
----
  rarfile-4.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-rarfile.spec ++++++
--- /var/tmp/diff_new_pack.I69BFt/_old  2026-10-26 10:05:09.924704476 +0100
+++ /var/tmp/diff_new_pack.I69BFt/_new  2026-10-26 10:05:09.926704559 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-rarfile
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -16,9 +16,8 @@
 #
 
 
-%define skip_python2 1
 Name:           python-rarfile
-Version:        4.2
+Version:        4.5
 Release:        0
 Summary:        RAR Archive Reader for Python
 License:        ISC

++++++ rarfile-4.2.tar.gz -> rarfile-4.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/.github/workflows/ci.yml 
new/rarfile-4.5/.github/workflows/ci.yml
--- old/rarfile-4.2/.github/workflows/ci.yml    2024-04-03 19:10:11.000000000 
+0200
+++ new/rarfile-4.5/.github/workflows/ci.yml    2026-08-02 22:45:05.000000000 
+0200
@@ -20,13 +20,13 @@
     strategy:
       matrix:
         pick:
-          - {OS: "ubuntu-latest", PY: "3.10", TOXENV: "lint,docs"}
+          - {OS: "ubuntu-latest", PY: "3.14", TOXENV: "lint,docs"}
     steps:
       - name: "Checkout"
-        uses: actions/checkout@v4
+        uses: actions/checkout@v7
 
       - name: "Setup Python ${{matrix.pick.PY}}"
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
           python-version: ${{matrix.pick.PY}}
 
@@ -45,29 +45,28 @@
     strategy:
       matrix:
         pick:
-          - {OS: "ubuntu-latest", PY: "3.8", PYNAME: "Python 3.8", TOXENV: 
"py38"}
-          - {OS: "ubuntu-latest", PY: "3.9", PYNAME: "Python 3.9", TOXENV: 
"py39-pycryptodome"}
           - {OS: "ubuntu-latest", PY: "3.10", PYNAME: "Python 3.10", TOXENV: 
"py310-cryptography"}
           - {OS: "ubuntu-latest", PY: "3.11", PYNAME: "Python 3.11", TOXENV: 
"py311-cryptography"}
           - {OS: "ubuntu-latest", PY: "3.12", PYNAME: "Python 3.12", TOXENV: 
"py312-cryptography"}
-          - {OS: "ubuntu-latest", PY: "pypy3.9", PYNAME: "PyPy3.9", TOXENV: 
"pypy39-cryptography"}
-          - {OS: "ubuntu-latest", PY: "pypy3.10", PYNAME: "PyPy3.10", TOXENV: 
"pypy310-cryptography"}
-          - {OS: "macos-latest", PY: "3.9", PYNAME: "Python 3.9", TOXENV: 
"py39-pycryptodome"}
-          - {OS: "macos-latest", PY: "3.10", PYNAME: "Python 3.10", TOXENV: 
"py310-cryptography"}
-          - {OS: "windows-latest", PY: "3.9", PYNAME: "Python 3.9", TOXENV: 
"py39-cryptography" }
-          - {OS: "windows-latest", PY: "3.10", PYNAME: "Python 3.10", TOXENV: 
"py310-cryptography" }
+          - {OS: "ubuntu-latest", PY: "3.13", PYNAME: "Python 3.13", TOXENV: 
"py313-cryptography"}
+          - {OS: "ubuntu-latest", PY: "3.14", PYNAME: "Python 3.14", TOXENV: 
"py314-cryptography"}
+          - {OS: "macos-latest", PY: "3.13", PYNAME: "Python 3.13", TOXENV: 
"py313-cryptography"}
+          - {OS: "macos-latest", PY: "3.14", PYNAME: "Python 3.14", TOXENV: 
"py314-cryptography"}
+          - {OS: "windows-latest", PY: "3.12", PYNAME: "Python 3.12", TOXENV: 
"py312-cryptography" }
+          - {OS: "windows-latest", PY: "3.14", PYNAME: "Python 3.14", TOXENV: 
"py310-cryptography" }
     steps:
       - name: "Checkout"
-        uses: actions/checkout@v4
+        uses: actions/checkout@v7
 
       - name: "Setup ${{matrix.pick.PYNAME}}"
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
           python-version: ${{matrix.pick.PY}}
 
       - name: "Install archivers (linux)"
         if: ${{runner.os == 'Linux'}}
         run: |
+          sudo -nH apt-get -qqy update
           sudo -nH apt-get -qqy install unrar unar libarchive-tools p7zip-rar
           unrar
           unar -h
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/.github/workflows/release.yml 
new/rarfile-4.5/.github/workflows/release.yml
--- old/rarfile-4.2/.github/workflows/release.yml       2024-04-03 
19:10:11.000000000 +0200
+++ new/rarfile-4.5/.github/workflows/release.yml       2026-08-02 
22:45:05.000000000 +0200
@@ -13,13 +13,13 @@
     name: "Build source package"
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v5
+      - uses: actions/checkout@v7
+      - uses: actions/setup-python@v6
         with: {python-version: "3.11"}
       - run: python3 -m pip install -r etc/requirements.build.txt 
--disable-pip-version-check
       - run: python3 setup.py sdist
       - run: python3 setup.py bdist_wheel
-      - uses: actions/upload-artifact@v4
+      - uses: actions/upload-artifact@v7
         with: {name: "dist", path: "dist"}
 
   publish:
@@ -27,18 +27,19 @@
     runs-on: ubuntu-latest
     needs: [sdist]
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v5
+      - uses: actions/checkout@v7
+      - uses: actions/setup-python@v6
         with: {python-version: "3.11"}
 
       - run: python3 -m pip install -r etc/requirements.build.txt 
--disable-pip-version-check
 
       - name: "Get files"
-        uses: actions/download-artifact@v4
+        uses: actions/download-artifact@v8
         with: {name: "dist", path: "dist"}
 
       - name: "Install pandoc"
         run: |
+          sudo -nH apt-get -qqy update
           sudo -nH apt-get -u -y install pandoc
           pandoc --version
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/Makefile new/rarfile-4.5/Makefile
--- old/rarfile-4.2/Makefile    2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/Makefile    2026-08-02 22:45:05.000000000 +0200
@@ -15,7 +15,7 @@
 all:
        pyflakes rarfile.py
        tox -e lint
-       tox -e py38-cryptography -- -n auto
+       tox -e py310-cryptography -- -n auto
 
 install:
        python setup.py install --prefix=$(prefix)
@@ -32,7 +32,7 @@
        rm -rf .tox
 
 ack:
-       for fn in test/files/*.py38-cryptography; do \
+       for fn in test/files/*.py310-cryptography; do \
                cp $$fn `echo $$fn | sed 's/[.]py.*/.exp/'` || exit 1; \
        done
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/PKG-INFO new/rarfile-4.5/PKG-INFO
--- old/rarfile-4.2/PKG-INFO    2024-04-03 19:10:18.862236700 +0200
+++ new/rarfile-4.5/PKG-INFO    2026-08-02 22:45:15.232896300 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: rarfile
-Version: 4.2
+Version: 4.5
 Summary: RAR archive reader for Python
 Home-page: https://github.com/markokr/rarfile
 Author: Marko Kreen
@@ -14,8 +14,18 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Topic :: System :: Archiving :: Compression
-Requires-Python: >=3.6
+Requires-Python: >=3.10
 License-File: LICENSE
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: keywords
+Dynamic: license
+Dynamic: license-file
+Dynamic: requires-python
+Dynamic: summary
 
 rarfile - RAR archive reader for Python
 =======================================
@@ -57,7 +67,7 @@
 |             |                      | * Does not support RAR2 locked files.   
            |
 |             |                      | * Does not support RAR5 Blake2 hash 
checking.       |
 
+-------------+----------------------+-----------------------------------------------------+
-| 7zip_       | Supported            | * RAR support not available on 
Debian/Ubuntu repos. |
+| 7zip_       | Supported            | * Requires ``7zip-rar`` package on 
Debian/Ubuntu.   |
 
+-------------+----------------------+-----------------------------------------------------+
 | p7zip_      | Supported            | * Unmaintained?                         
            |
 |             |                      | * Requires ``p7zip-rar`` package on 
Debian/Ubuntu.  |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/README.rst new/rarfile-4.5/README.rst
--- old/rarfile-4.2/README.rst  2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/README.rst  2026-08-02 22:45:05.000000000 +0200
@@ -39,7 +39,7 @@
 |             |                      | * Does not support RAR2 locked files.   
            |
 |             |                      | * Does not support RAR5 Blake2 hash 
checking.       |
 
+-------------+----------------------+-----------------------------------------------------+
-| 7zip_       | Supported            | * RAR support not available on 
Debian/Ubuntu repos. |
+| 7zip_       | Supported            | * Requires ``7zip-rar`` package on 
Debian/Ubuntu.   |
 
+-------------+----------------------+-----------------------------------------------------+
 | p7zip_      | Supported            | * Unmaintained?                         
            |
 |             |                      | * Requires ``p7zip-rar`` package on 
Debian/Ubuntu.  |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/doc/news.rst new/rarfile-4.5/doc/news.rst
--- old/rarfile-4.2/doc/news.rst        2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/doc/news.rst        2026-08-02 22:45:05.000000000 +0200
@@ -4,6 +4,53 @@
 
 .. py:currentmodule:: rarfile
 
+Version 4.5 (2026-08-02)
+------------------------
+
+Security fixes:
+
+* Skip comments that are larger than 256k.
+
+Fixes:
+
+* Truncate filenames at NUL byte.
+
+Version 4.4 (2026-07-22)
+------------------------
+
+Security fixes:
+
+* Skip CRC check for some old subblocks.  Previously
+  `rarfile` tried to calculate header CRC by reading
+  data payload for those, but that could cause excessive
+  allocations.
+
+Fixes:
+
+* RAR5: Reattempt passwords after trying wrong password
+  when header encryption is used.
+  [`#131 <https://github.com/markokr/rarfile/pull/131>`_]
+
+* Restore fd position in file type detection functions.
+  [`#121 <https://github.com/markokr/rarfile/pull/121>`_]
+
+Version 4.3 (2026-07-08)
+------------------------
+
+Security fixes:
+
+* Disallow extraction outside extraction path,
+  in case of existing symlink.
+  [`#114 <https://github.com/markokr/rarfile/pull/114>`_]
+
+* Disallow creating symlinks to outside of extraction path.
+  [`#118 <https://github.com/markokr/rarfile/pull/118>`_]
+
+Fixes:
+
+* Apply length limit to passwords, so too long password
+  give same result as for unrar.
+
 Version 4.2 (2024-04-03)
 ------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/dumprar.py new/rarfile-4.5/dumprar.py
--- old/rarfile-4.2/dumprar.py  2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/dumprar.py  2026-08-02 22:45:05.000000000 +0200
@@ -148,6 +148,14 @@
     (rf.RAR5_XREDIR_ISDIR, "DIR"),
 )
 
+r2_subblock_types = {
+    rf.RAR_OLD_SUB_OS2: "OS2",
+    rf.RAR_OLD_SUB_UNIX: "UNIX",
+    rf.RAR_OLD_SUB_MAC: "MAC",
+    rf.RAR_OLD_SUB_BEOS: "BEOS",
+    rf.RAR_OLD_SUB_NT: "NT",
+    rf.RAR_OLD_SUB_STREAM: "STREAM",
+}
 
 dos_mode_bits = (
     (0x01, "READONLY"),
@@ -331,6 +339,10 @@
             xprint("  volnr=%d", h.endarc_volnr)
     elif h.type == rf.RAR_BLOCK_MARK:
         xprint("  flags=0x%04x:", h.flags)
+    elif h.type == rf.RAR_BLOCK_OLD_SUB:
+        xprint("  flags=0x%04x:%s", h.flags, render_flags(h.flags, 
generic_bits))
+        xprint("  sub_type=0x%04x:%s", h.old_sub_type,
+               r2_subblock_types.get(h.old_sub_type, '*UNKNOWN*'))
     else:
         xprint("  flags=0x%04x:%s", h.flags, render_flags(h.flags, 
generic_bits))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/etc/requirements.build.txt 
new/rarfile-4.5/etc/requirements.build.txt
--- old/rarfile-4.2/etc/requirements.build.txt  2024-04-03 19:10:11.000000000 
+0200
+++ new/rarfile-4.5/etc/requirements.build.txt  2026-08-02 22:45:05.000000000 
+0200
@@ -1,4 +1,3 @@
-setuptools>=67
-wheel>=0.41
-twine==4.0.2
-tox==4.8.0
+setuptools==82.0.1
+twine==6.2.0
+tox==4.56.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/rarfile.egg-info/PKG-INFO 
new/rarfile-4.5/rarfile.egg-info/PKG-INFO
--- old/rarfile-4.2/rarfile.egg-info/PKG-INFO   2024-04-03 19:10:18.000000000 
+0200
+++ new/rarfile-4.5/rarfile.egg-info/PKG-INFO   2026-08-02 22:45:15.000000000 
+0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: rarfile
-Version: 4.2
+Version: 4.5
 Summary: RAR archive reader for Python
 Home-page: https://github.com/markokr/rarfile
 Author: Marko Kreen
@@ -14,8 +14,18 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Topic :: System :: Archiving :: Compression
-Requires-Python: >=3.6
+Requires-Python: >=3.10
 License-File: LICENSE
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: keywords
+Dynamic: license
+Dynamic: license-file
+Dynamic: requires-python
+Dynamic: summary
 
 rarfile - RAR archive reader for Python
 =======================================
@@ -57,7 +67,7 @@
 |             |                      | * Does not support RAR2 locked files.   
            |
 |             |                      | * Does not support RAR5 Blake2 hash 
checking.       |
 
+-------------+----------------------+-----------------------------------------------------+
-| 7zip_       | Supported            | * RAR support not available on 
Debian/Ubuntu repos. |
+| 7zip_       | Supported            | * Requires ``7zip-rar`` package on 
Debian/Ubuntu.   |
 
+-------------+----------------------+-----------------------------------------------------+
 | p7zip_      | Supported            | * Unmaintained?                         
            |
 |             |                      | * Requires ``p7zip-rar`` package on 
Debian/Ubuntu.  |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/rarfile.egg-info/SOURCES.txt 
new/rarfile-4.5/rarfile.egg-info/SOURCES.txt
--- old/rarfile-4.2/rarfile.egg-info/SOURCES.txt        2024-04-03 
19:10:18.000000000 +0200
+++ new/rarfile-4.5/rarfile.egg-info/SOURCES.txt        2026-08-02 
22:45:15.000000000 +0200
@@ -54,6 +54,8 @@
 test/files/rar15-comment-lock.rar.exp
 test/files/rar15-comment.rar
 test/files/rar15-comment.rar.exp
+test/files/rar2-unix-owner.rar
+test/files/rar2-unix-owner.rar.exp
 test/files/rar202-comment-nopsw.rar
 test/files/rar202-comment-nopsw.rar.exp
 test/files/rar202-comment-psw.rar
@@ -96,6 +98,8 @@
 test/files/rar5-crc.sfx
 test/files/rar5-dups.rar
 test/files/rar5-dups.rar.exp
+test/files/rar5-evil-symlink-traversal.rar
+test/files/rar5-evil-symlink-traversal.rar.exp
 test/files/rar5-hlink.rar
 test/files/rar5-hlink.rar.exp
 test/files/rar5-hpsw.rar
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/rarfile.py new/rarfile-4.5/rarfile.py
--- old/rarfile-4.2/rarfile.py  2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/rarfile.py  2026-08-02 22:45:05.000000000 +0200
@@ -1,6 +1,6 @@
 # rarfile.py
 #
-# Copyright (c) 2005-2024  Marko Kreen <[email protected]>
+# Copyright (c) 2005-2026  Marko Kreen <[email protected]>
 #
 # Permission to use, copy, modify, and/or distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -92,7 +92,7 @@
             self.decrypt = ciph.decryptor().update
 
 
-__version__ = "4.2"
+__version__ = "4.5"
 
 # export only interesting items
 __all__ = ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", 
"RarFile", "RarExtFile"]
@@ -194,6 +194,14 @@
 RAR_SKIP_IF_UNKNOWN = 0x4000
 RAR_LONG_BLOCK = 0x8000
 
+# Subtypes for RAR_BLOCK_OLD_SUB
+RAR_OLD_SUB_OS2 = 0x100
+RAR_OLD_SUB_UNIX = 0x101
+RAR_OLD_SUB_MAC = 0x102
+RAR_OLD_SUB_BEOS = 0x103
+RAR_OLD_SUB_NT = 0x104
+RAR_OLD_SUB_STREAM = 0x105
+
 # Host OS types
 RAR_OS_MSDOS = 0    #: MSDOS (only in RAR3)
 RAR_OS_OS2 = 1      #: OS2 (only in RAR3)
@@ -210,6 +218,10 @@
 RAR_M4 = 0x34   #: Compression level `-m4`.
 RAR_M5 = 0x35   #: Compression level `-m5` - Maximum compression.
 
+RAR_MAX_PASSWORD = 127  #: Max number of utf-16 chars in passwords.
+RAR_MAX_KDF_SHIFT = 24  #: Max power-of-2 for KDF count
+RAR_MAX_COMMENT = 256 * 1024  #: Max supported comment size
+
 #
 # RAR5 constants
 #
@@ -334,6 +346,7 @@
                 if curdata[pos:pos + len(RAR5_ID)] == RAR5_ID:
                     return RAR_V5, pos
                 findpos = pos + len(sig)
+        fd.restore_pos()
     return 0, 0
 
 
@@ -347,6 +360,7 @@
     """
     with XFile(xfile) as fd:
         buf = fd.read(len(RAR5_ID))
+        fd.restore_pos()
     if buf.startswith(RAR_ID):
         return RAR_V3
     elif buf.startswith(RAR5_ID):
@@ -396,6 +410,10 @@
     """File requires password"""
 
 
+class BadSymLinkError(Error):
+    """Invalid symbolic link"""
+
+
 class NeedFirstVolume(Error):
     """Need to start from first volume.
 
@@ -943,6 +961,17 @@
             path = os.fspath(path)
         dstfn = os.path.join(path, fname)
 
+        # Reject members whose destination escapes `path` once symlinks
+        # already created on disk are resolved.  Without this, a symlink
+        # member can point outside `path` and a later file/dir member
+        # named through it will be written outside the extraction root.
+        real_path = os.path.realpath(path)
+        real_dst = os.path.realpath(dstfn)
+        if real_dst != real_path and not real_dst.startswith(real_path + 
os.sep):
+            raise BadRarFile(
+                "Refusing to extract entry that escapes destination: %r" % 
info.filename
+            )
+
         dirname = os.path.dirname(dstfn)
         if dirname and dirname != ".":
             os.makedirs(dirname, exist_ok=True)
@@ -952,7 +981,7 @@
         if info.is_dir():
             return self._make_dir(info, dstfn, pwd, set_attrs)
         if info.is_symlink():
-            return self._make_symlink(info, dstfn, pwd, set_attrs)
+            return self._make_symlink(info, dstfn, pwd, set_attrs, path)
         return None
 
     def _create_helper(self, name, flags, info):
@@ -974,10 +1003,10 @@
             self._set_attrs(info, dstfn)
         return dstfn
 
-    def _make_symlink(self, info, dstfn, pwd, set_attrs):
+    def _make_symlink(self, info, dstfn, pwd, set_attrs, top):
         target_is_directory = False
         if info.host_os == RAR_OS_UNIX:
-            link_name = self.read(info, pwd)
+            link_name = self.read(info, pwd).decode("utf8", "replace")
             target_is_directory = (info.flags & RAR_FILE_DIRECTORY) == 
RAR_FILE_DIRECTORY
         elif info.file_redir:
             redir_type, redir_flags, link_name = info.file_redir
@@ -989,6 +1018,18 @@
             warnings.warn(f"Unsupported link type - {info.filename}", 
UnsupportedWarning)
             return None
 
+        # disallow abs paths
+        target = os.path.normpath(link_name)
+        if os.path.isabs(target) or os.path.splitdrive(target)[0]:
+            raise BadSymLinkError('Absolute links not allowed')
+
+        # disallow ../ traversal
+        dest_abs = os.path.realpath(top)
+        target_base = os.path.dirname(dstfn)
+        target_abs = os.path.realpath(os.path.join(target_base, target))
+        if os.path.commonpath([target_abs, dest_abs]) != dest_abs:
+            raise BadSymLinkError('Link to outside not allowed')
+
         os.symlink(link_name, dstfn, target_is_directory=target_is_directory)
         return dstfn
 
@@ -1383,6 +1424,8 @@
     endarc_datacrc = None
     endarc_volnr = None
 
+    old_sub_type = None
+
     def _must_disable_hack(self):
         if self.type == RAR_BLOCK_FILE:
             if self.flags & RAR_FILE_PASSWORD:
@@ -1493,6 +1536,16 @@
         elif h.type == RAR_BLOCK_OLD_EXTRA:
             pos += 7
             crc_pos = pos
+        elif h.type == RAR_BLOCK_OLD_SUB:
+            pos = self._parse_old_subblock(h, hdata, pos)
+
+            # these types do not have their own data CRC,
+            # so data was included in header CRC.
+            if h.old_sub_type in (RAR_OLD_SUB_UNIX, RAR_OLD_SUB_MAC):
+                # skip CRC check, it requires to read data part
+                return h
+
+            crc_pos = h.header_size
         elif h.type == RAR_BLOCK_ENDARC:
             if h.flags & RAR_ENDARC_DATACRC:
                 h.endarc_datacrc, pos = load_le32(hdata, pos)
@@ -1503,12 +1556,8 @@
         else:
             crc_pos = h.header_size
 
-        # check crc
-        if h.type == RAR_BLOCK_OLD_SUB:
-            crcdat = hdata[2:] + fd.read(h.add_size)
-        else:
-            crcdat = hdata[2:crc_pos]
-
+        # calculate crc
+        crcdat = hdata[2:crc_pos]
         calc_crc = crc32(crcdat) & 0xFFFF
 
         # return good header
@@ -1565,6 +1614,9 @@
             h.orig_filename = name
             h.filename = name.decode("utf8", "replace")
         else:
+            nul = name.find(b"\0")
+            if nul >= 0:
+                name = name[:nul]
             # stored in random encoding
             h.orig_filename = name
             h.filename = self._decode(name)
@@ -1587,6 +1639,12 @@
 
         return pos
 
+    def _parse_old_subblock(self, h, hdata, pos):
+        """Parse RAR2 subblock
+        """
+        h.old_sub_type, _reserved = S_OLD_SUBBLOCK_HDR.unpack_from(hdata, pos)
+        return pos
+
     def _parse_subblocks(self, h, hdata, pos):
         """Find old-style comment subblock
         """
@@ -1604,6 +1662,9 @@
             # followed by block-specific header
             if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= 
pos_next:
                 declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
+                if declen > RAR_MAX_COMMENT:
+                    pos = pos_next
+                    continue
                 pos += S_COMMENT_HDR.size
                 data = hdata[pos: pos_next]
                 cmt = rar3_decompress(ver, meth, data, declen, sflags,
@@ -1616,6 +1677,11 @@
 
     def _read_comment_v3(self, inf, pwd=None):
 
+        if inf.compress_size > RAR_MAX_COMMENT:
+            return None
+        if inf.file_size > RAR_MAX_COMMENT:
+            return None
+
         # read data
         with XFile(inf.volume_file) as rf:
             rf.seek(inf.data_offset)
@@ -1827,10 +1893,10 @@
     def _gen_key(self, kdf_count, salt):
         if self._last_aes256_key[:2] == (kdf_count, salt):
             return self._last_aes256_key[2]
-        if kdf_count > 24:
+        if kdf_count > RAR_MAX_KDF_SHIFT:
             raise BadRarFile("Too large kdf_count")
         pwd = self._get_utf8_password()
-        key = pbkdf2_hmac("sha256", pwd, salt, 1 << kdf_count)
+        key = rar5_s2k(pwd, salt, 1 << kdf_count)
         self._last_aes256_key = (kdf_count, salt, key)
         return key
 
@@ -1956,7 +2022,12 @@
 
         h.file_compress_flags, pos = load_vint(hdata, pos)
         h.file_host_os, pos = load_vint(hdata, pos)
-        h.orig_filename, pos = load_vstr(hdata, pos)
+
+        name, pos = load_vstr(hdata, pos)
+        nul = name.find(b"\0")
+        if nul >= 0:
+            name = name[:nul]
+        h.orig_filename = name
         h.filename = h.orig_filename.decode("utf8", "replace").rstrip("/")
 
         # use compatible values
@@ -1995,6 +2066,8 @@
     def _check_password(self, check_value, kdf_count_shift, salt):
         if len(check_value) != RAR5_PW_CHECK_SIZE + RAR5_PW_SUM_SIZE:
             return
+        if kdf_count_shift > RAR_MAX_KDF_SHIFT:
+            raise BadRarFile("Too large kdf_count")
 
         hdr_check = check_value[:RAR5_PW_CHECK_SIZE]
         hdr_sum = check_value[RAR5_PW_CHECK_SIZE:]
@@ -2004,7 +2077,7 @@
 
         kdf_count = (1 << kdf_count_shift) + 32
         pwd = self._get_utf8_password()
-        pwd_hash = pbkdf2_hmac("sha256", pwd, salt, kdf_count)
+        pwd_hash = rar5_s2k(pwd, salt, kdf_count)
 
         pwd_check = bytearray(RAR5_PW_CHECK_SIZE)
         len_mask = RAR5_PW_CHECK_SIZE - 1
@@ -2015,6 +2088,8 @@
             raise RarWrongPassword()
 
     def _parse_encryption_block(self, h, hdata, pos):
+        self._hdrenc_main = h
+        self._needs_password = True
         h.encryption_algo, pos = load_vint(hdata, pos)
         h.encryption_flags, pos = load_vint(hdata, pos)
         h.encryption_kdf_count, pos = load_byte(hdata, pos)
@@ -2025,7 +2100,6 @@
             raise BadRarFile("Unsupported header encryption cipher")
         if h.encryption_check_value and self._password:
             self._check_password(h.encryption_check_value, 
h.encryption_kdf_count, h.encryption_salt)
-        self._hdrenc_main = h
         return h
 
     def _process_file_extra(self, h, xdata):
@@ -2150,6 +2224,10 @@
             return None
         if item.compress_type != RAR_M0:
             return None
+        if item.compress_size > RAR_MAX_COMMENT:
+            return None
+        if item.file_size > RAR_MAX_COMMENT:
+            return None
 
         if item.flags & RAR_FILE_PASSWORD:
             algo, ___flags, kdf_count, salt, iv, ___checkval = 
item.file_encryption
@@ -2302,6 +2380,8 @@
             n -= len(data)
         data = b"".join(buf)
         if n > 0:
+            if self._returncode:
+                check_returncode(self._returncode, "", 
tool_setup().get_errmap())
             raise BadRarFile("Failed the read enough data: req=%d got=%d" % 
(orig, len(data)))
 
         # done?
@@ -2682,17 +2762,27 @@
 class XFile:
     """Input may be filename or file object.
     """
-    __slots__ = ("_fd", "_need_close")
+    __slots__ = ("_fd", "_need_close", "_initial_pos")
 
     def __init__(self, xfile, bufsize=1024):
         if is_filelike(xfile):
+            self._initial_pos = xfile.tell()
             self._need_close = False
             self._fd = xfile
             self._fd.seek(0)
         else:
+            self._initial_pos = None
             self._need_close = True
             self._fd = open(xfile, "rb", bufsize)
 
+    def restore_pos(self):
+        if self._initial_pos is None:
+            return
+        try:
+            self._fd.seek(self._initial_pos)
+        except:
+            pass
+
     def read(self, n=None):
         """Read from file."""
         return self._fd.read(n)
@@ -2876,7 +2966,7 @@
 S_BLK_HDR = Struct("<HBHH")
 S_FILE_HDR = Struct("<LLBLLBBHL")
 S_COMMENT_HDR = Struct("<HBBH")
-
+S_OLD_SUBBLOCK_HDR = Struct("<HB")
 
 def load_vint(buf, pos):
     """Load RAR5 variable-size int."""
@@ -3061,12 +3151,23 @@
     return True
 
 
+def rar5_s2k(pwd, salt, kdf_count):
+    """String-to-key hash for RAR5.
+    """
+    if not isinstance(pwd, str):
+        pwd = pwd.decode("utf8")
+    wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2]
+    ustr = wstr.decode("utf-16le").encode("utf8")
+    return pbkdf2_hmac("sha256", ustr, salt, kdf_count)
+
+
 def rar3_s2k(pwd, salt):
     """String-to-key hash for RAR3.
     """
     if not isinstance(pwd, str):
         pwd = pwd.decode("utf8")
-    seed = bytearray(pwd.encode("utf-16le") + salt)
+    wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2]
+    seed = bytearray(wstr + salt)
     h = Rar3Sha1(rarbug=True)
     iv = b""
     for i in range(16):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/setup.py new/rarfile-4.5/setup.py
--- old/rarfile-4.2/setup.py    2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/setup.py    2026-08-02 22:45:05.000000000 +0200
@@ -23,7 +23,7 @@
     url="https://github.com/markokr/rarfile";,
     py_modules=['rarfile'],
     keywords=['rar', 'unrar', 'archive'],
-    python_requires=">=3.6",
+    python_requires=">=3.10",
     classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Intended Audience :: Developers",
Binary files old/rarfile-4.2/test/files/rar2-unix-owner.rar and 
new/rarfile-4.5/test/files/rar2-unix-owner.rar differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/test/files/rar2-unix-owner.rar.exp 
new/rarfile-4.5/test/files/rar2-unix-owner.rar.exp
--- old/rarfile-4.2/test/files/rar2-unix-owner.rar.exp  1970-01-01 
01:00:00.000000000 +0100
+++ new/rarfile-4.5/test/files/rar2-unix-owner.rar.exp  2026-08-02 
22:45:05.000000000 +0200
@@ -0,0 +1,6 @@
+Archive: test/files/rar2-unix-owner.rar
+FILE: hdrlen=40 datlen=17 is=F--
+  flags=0x8080:LONG,D1024
+  os=3:UNIX ver=20 mode=-rw-rw-r-- meth=3 cmp=17 dec=4 vol=0
+  crc=0x7e3265a8 (2117232040) date_time=2026-07-15 22:00:22
+  name=file.txt
Binary files old/rarfile-4.2/test/files/rar5-evil-symlink-traversal.rar and 
new/rarfile-4.5/test/files/rar5-evil-symlink-traversal.rar differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rarfile-4.2/test/files/rar5-evil-symlink-traversal.rar.exp 
new/rarfile-4.5/test/files/rar5-evil-symlink-traversal.rar.exp
--- old/rarfile-4.2/test/files/rar5-evil-symlink-traversal.rar.exp      
1970-01-01 01:00:00.000000000 +0100
+++ new/rarfile-4.5/test/files/rar5-evil-symlink-traversal.rar.exp      
2026-08-02 22:45:05.000000000 +0200
@@ -0,0 +1,20 @@
+Archive: test/files/rar5-evil-symlink-traversal.rar
+R5_FILE: hdrlen=42 datlen=0 hdr_extra=18 is=--L
+  block_flags=0x0003:EXTRA,DATA
+  name=up
+  file_flags=0x0004:CRC32
+  cmp_algo=0 cmp_meth=0 dict=0 solid=False
+  os=1:UNIX mode=lrwxr-xr-x cmp=0 dec=0 vol=0
+  crc=0x00000000 (0)
+  date_time=2026-05-11 21:59:26
+  mtime=2026-05-11T21:59:26.388379155+00:00
+  redir: type=UNIX_SYMLINK flags=1:DIR destination=..
+R5_FILE: hdrlen=47 datlen=23 hdr_extra=11 is=F--
+  block_flags=0x0003:EXTRA,DATA
+  name=up/pwned.txt
+  file_flags=0x0004:CRC32
+  cmp_algo=0 cmp_meth=0 dict=0 solid=False
+  os=1:UNIX mode=-rw-r--r-- cmp=23 dec=23 vol=0
+  crc=0xc0327d47 (3224534343)
+  date_time=2026-05-11 21:59:26
+  mtime=2026-05-11T21:59:26.388813228+00:00
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/test/test_api.py 
new/rarfile-4.5/test/test_api.py
--- old/rarfile-4.2/test/test_api.py    2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/test/test_api.py    2026-08-02 22:45:05.000000000 +0200
@@ -115,6 +115,13 @@
     assert rarfile.is_rarfile("test/files/_missing_.rar") is False
 
 
+def test_restore_pos():
+    with open('test/files/rar5-crc.rar', 'rb') as fd:
+        fd.seek(3)
+        assert rarfile.is_rarfile(fd) is True
+        assert fd.tell() == 3
+
+
 def test_getinfo():
     with rarfile.RarFile("test/files/rar5-crc.rar") as rf:
         inf = rf.getinfo("stest1.txt")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/test/test_extract.py 
new/rarfile-4.5/test/test_extract.py
--- old/rarfile-4.2/test/test_extract.py        2024-04-03 19:10:11.000000000 
+0200
+++ new/rarfile-4.5/test/test_extract.py        2026-08-02 22:45:05.000000000 
+0200
@@ -158,29 +158,27 @@
         assert get_props(rf, "data_link") == "--L"
         assert get_props(rf, "random_link") == "--L"
 
-        rf.extractall(tmp_path)
+        rf.extract('data.txt', tmp_path)
+        rf.extract('data_link', tmp_path)
+        with pytest.raises(rarfile.BadSymLinkError):
+            rf.extract('random_link', tmp_path)
 
-        assert sorted(os.listdir(tmp_path)) == ["data.txt", "data_link", 
"random_link"]
+        assert sorted(os.listdir(tmp_path)) == ["data.txt", "data_link"]
 
         data = rf.getinfo("data.txt")
         data_link = rf.getinfo("data_link")
-        random_link = rf.getinfo("random_link")
 
         assert not data.is_symlink()
         assert data_link.is_symlink()
-        assert random_link.is_symlink()
 
         assert rf.read(data) == b"data\n"
         assert rf.read(data_link) == b"data.txt"
-        assert rf.read(random_link) == b"../random123"
 
         assert os.path.isfile(tmp_path / "data.txt")
         assert os.path.islink(tmp_path / "data_link")
-        assert os.path.islink(tmp_path / "random_link")
 
         # str - work around pypy3 bug
         assert os.readlink(str(tmp_path / "data_link")) == "data.txt"
-        assert os.readlink(str(tmp_path / "random_link")) == "../random123"
 
 
 def test_symlink_win(tmp_path):
@@ -195,13 +193,20 @@
         assert get_props(rf, "links/file_link") == "--L"
 
         with pytest.warns(rarfile.UnsupportedWarning):
-            rf.extractall(tmp_path)
+            rf.extract('content/dir1', tmp_path)
+            rf.extract('content/dir2', tmp_path)
+            rf.extract('content/file.txt', tmp_path)
+            rf.extract('links/dir_junction', tmp_path)
+            rf.extract('links/dir_link', tmp_path)
+            rf.extract('links/file_link', tmp_path)
+
+        with pytest.raises(rarfile.BadSymLinkError):
+            rf.extract('links/bad_link', tmp_path)
 
         assert sorted(os.listdir(tmp_path)) == ["content", "links"]
         assert sorted(os.listdir(tmp_path / "content")) == ["dir1", "dir2", 
"file.txt"]
-        assert sorted(os.listdir(tmp_path / "links")) == ["bad_link", 
"dir_link", "file_link"]
+        assert sorted(os.listdir(tmp_path / "links")) == ["dir_link", 
"file_link"]
 
-        assert os.path.islink(tmp_path / "links/bad_link")
         assert os.path.islink(tmp_path / "links/dir_link")
         assert os.path.islink(tmp_path / "links/file_link")
 
@@ -225,3 +230,37 @@
         assert os.path.isfile(tmp_path / "vols" / "bigfile.txt")
         assert os.path.isfile(tmp_path / "vols" / "smallfile.txt")
 
+
+
[email protected](sys.platform == "win32", reason="symlink semantics differ 
on Windows")
+def test_symlink_chained_traversal_blocked(tmp_path):
+    """Regression test for symlink-chained zip-slip (CWE-22 + CWE-59).
+
+    The fixture rar5-evil-symlink-traversal.rar contains two members:
+      1. up           -- symlink whose target is the relative path '..'
+      2. up/pwned.txt -- regular file whose extraction path traverses
+                         the symlink above and lands outside the extract dir.
+
+    Without the realpath containment check in _extract_one, extractall
+    silently writes pwned.txt outside the extraction directory (in
+    tmp_path rather than tmp_path/extract).
+    """
+    extract = tmp_path / "extract"
+    extract.mkdir()
+
+    fn = "test/files/rar5-evil-symlink-traversal.rar"
+    with rarfile.RarFile(fn) as rf:
+        with pytest.raises(rarfile.BadSymLinkError):
+            rf.extractall(str(extract))
+
+    # test if pre-existing symlink is detected
+    up = extract / "up"
+    up.symlink_to('..')
+    assert str(up.readlink()) == '..'
+
+    with pytest.raises(rarfile.BadRarFile):
+        rf.extract('up/pwned.txt', extract)
+
+    # The second member would land at tmp_path/pwned.txt without the guard.
+    assert not (tmp_path / "pwned.txt").exists()
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rarfile-4.2/test/test_format.py 
new/rarfile-4.5/test/test_format.py
--- old/rarfile-4.2/test/test_format.py 2024-04-03 19:10:11.000000000 +0200
+++ new/rarfile-4.5/test/test_format.py 2026-08-02 22:45:05.000000000 +0200
@@ -93,6 +93,10 @@
     assert r.comment is None
     assert r.namelist() == []
 
+    with pytest.raises(rarfile.RarWrongPassword):
+        r.setpassword("password222")
+    assert r.needs_password() is True
+
     r.setpassword("password")
     assert r.needs_password() is True
     assert r.namelist() == ["stest1.txt", "stest2.txt"]

Reply via email to