From 768d49094c248a460fd5f740343aa10b64e5cd5a Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 22 Jun 2026 18:21:42 -0500 Subject: [PATCH] Fix path traversal in Topaz extraction (arbitrary file write) The Topaz header record "tag" is read verbatim from the untrusted book file by bookReadString() and then used unsanitized to build the output filename in extractFiles(). A crafted tag such as "../../foo" let a malicious .azw/Topaz file write attacker-controlled bytes outside the extraction directory. The payload content requires no book key, since an unencrypted record with compressedLength == 0 is returned raw. Strip the record name to its basename before joining it to destdir so traversal sequences (../, /, \) can no longer escape, and add an abspath-based containment check as defense in depth. Co-Authored-By: Claude Opus 4.8 --- DeDRM_plugin/topazextract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/DeDRM_plugin/topazextract.py b/DeDRM_plugin/topazextract.py index 97d75a7..4f2c057 100644 --- a/DeDRM_plugin/topazextract.py +++ b/DeDRM_plugin/topazextract.py @@ -358,7 +358,16 @@ class TopazBook: destdir = os.path.join(outdir,"page") if name == b'glyphs': destdir = os.path.join(outdir,"glyphs") + # The record name is read verbatim from the (untrusted) book + # file, so strip any directory components to stop a crafted + # name (e.g. "../../foo") from writing outside of destdir. + fname = os.path.basename(fname) outputFile = os.path.join(destdir,fname) + # Defense in depth: never let the resolved path escape destdir. + real_out = os.path.abspath(outputFile) + real_dir = os.path.abspath(destdir) + if not (real_out == real_dir or real_out.startswith(real_dir + os.sep)): + raise DrmException("Invalid book record name: {0}".format(repr(name))) print(".", end=' ') record = self.getBookPayloadRecord(name,index) if isinstance(record, str):