A crafted DICOMDIR can set ReferencedFileID to a path outside the File-set root. pydicom resolves the path only to confirm that it exists, but does not verify that the resolved path remains under the File-set root. Subsequent public FileSet operations such as copy(), write(), and remove()+write(use_existing=True) use that unchecked path in file I/O operations. This allows arbitrary file read/copy and, in some flows, move/delete outside the File-set root.
Verified on pydicom 3.1.0.dev0.
Relevant logic is in src/pydicom/fileset.py:
RecordNode._file_id converts ReferencedFileID directly to Path(...)FileSet.load() checks only (root / file_id).resolve(strict=True) to confirm existenceFileSet.load() does not verify that the final resolved path is contained within the File-set rootFileInstance.path returns self.file_set.path / self.node._file_idFileSet.copy() uses shutil.copyfile(instance.path, dst)FileSet.write() uses Path(instance.path).unlink() and shutil.move(...)Because there is no containment check such as resolved.relative_to(root.resolve(strict=True)), a malicious DICOMDIR can reference:
/etc/passwd../...This is not limited to obviously invalid VR input. Even when pydicom emits warnings for invalid ReferencedFileID values, the operation is not blocked. I also confirmed a symlink-based variant using a conformant file ID.
A realistic server-side scenario is:
DICOMDIR using FileSetFileSet.copy() or FileSet.write()DICOMDIR is included in the exported resultMinimal reproduction:
DICOMDIR2.4.53.0.2Exploitability
AV:LAC:LPR:NUI:RScope
S:UImpact
C:HI:HA:H7.8/CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H