T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/convert_pdf_to_images.py:98- Finding
Symbolic Link Following Allows Unauthorized Local File Inclusion in ZIP Archives
- Content
View full analysis
Vulnerability Details
File Location:
scripts/convert_pdf_to_images.py, lines 98–116
Vulnerability Type: Symbolic-link-based local file disclosure
Risk Level: MediumVulnerable Code
python # Get all image files image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp') image_files = sorted( [f for f in os.listdir(images_dir) if f.lower().endswith(image_extensions)] ) if not image_files: print(f"Error: no image files found in directory {images_dir}") sys.exit(1) # Create ZIP file try: with zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) as zipf: for image_file in image_files: image_path = os.path.join(images_dir, image_file) # Add to ZIP, preserving the filename zipf.write(image_path, image_file) print(f" Added: {image_file}")Technical Analysis
The ZIP-generation function enumerates every directory entry whose name ends with a supported image extension and passes its path directly to
zipfile.ZipFile.write(). It does not verify that an entry is a regular file, reject symbolic links, or ensure that the resolved path remains within the intended image directory.When an entry such as
secret.jpgis a symbolic link to another readable local file,zipf.write()follows the link and archives the target file's contents under the harmless-looking namesecret.jpg. The function also archives pre-existing image-named entries rather than restricting the archive to files generated during the current conversion.Exploitation requires the attacker to be able to create or influence entries in the selected output directory before ZIP creation.
Attack Path
- An attacker gains write access to, or otherwise controls, the output directory selected through
--output-dir. - The attacker creates an image-named symbolic link, for example:
text images/secret.jpg -> /path/to/sensitive/f
...[truncated 992 chars]
- An attacker gains write access to, or otherwise controls, the output directory selected through
- Remediation
View remediation
Remediation Suggestions
- Archive only the exact paths returned by
pdf_to_images()during the current invocation, rather than enumerating every image-named entry in the directory. - Reject symbolic links explicitly with
os.path.islink()orPath.is_symlink(). - Require each candidate to be a regular file.
- Resolve the output directory and candidate paths and verify that every candidate remains under the resolved output directory.
- Where supported, use descriptor-based operations and no-follow semantics to reduce time-of-check/time-of-use race exposure.
- Consider creating a new private output directory with restrictive permissions for each conversion.
Example validation logic:
python from pathlib import Path base_dir = Path(images_dir).resolve() for image_file in generated_image_paths: candidate = base_dir / image_file if candidate.is_symlink() or not candidate.is_file(): raise ValueError(f"Unsafe archive entry: {image_file}") resolved = candidate.resolve() if base_dir not in resolved.parents: raise ValueError(f"Archive entry escapes output directory: {image_file}") zipf.write(resolved, image_file)The containment check and file opening should occur as closely together as possible. For stronger protection against concurrent replacement, use platform-appropriate no-follow file operations.
- Archive only the exact paths returned by
