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"]