T05 · Unauthorized Access and Privilege Escalation
- Location
build_pdfs.py:50- Finding
Unrestricted Local File Read Through Manifest-Controlled Paths
- Content
View full analysis
Vulnerability Details
File Location:
build_pdfs.py, lines 50–64
Vulnerability Type: Improper path authorization and unrestricted file access
Risk Level: HighVulnerable Code
python def make_pdf(out_path: str, files: list[str]) -> int: """Merge images and PDFs into a single output PDF. Returns page count.""" writer = PdfWriter() for fpath in files: if not os.path.exists(fpath): print(f" [WARN] Missing file, skipping: {fpath}") continue ext = fpath.lower().rsplit(".", 1)[-1] if ext in ("jpg", "jpeg", "png", "webp", "bmp", "tiff", "gif"): pdf_bytes = img_to_pdf_bytes(fpath) reader = PdfReader(io.BytesIO(pdf_bytes)) elif ext == "pdf": reader = PdfReader(fpath) else: print(f" [WARN] Unknown file type, skipping: {fpath}") continue for page in reader.pages: writer.add_page(page)Technical Analysis
File paths supplied through the JSON manifest are passed directly to
Image.open()orPdfReader()after only an existence check. The implementation does not canonicalize paths, enforce containment within/mnt/user-data/uploads/, reject symbolic links, or verify that the path refers to an authorized regular file.Consequently, an attacker who can control the manifest can request any readable local image or PDF. The contents are incorporated into a generated output PDF, creating a channel through which files outside the legitimate receipt-upload directory can be accessed and disclosed.
Attack Path
- The attacker prepares a manifest containing an absolute path or traversal path to a sensitive local image or PDF.
- The attacker invokes
build_pdfs.pywith that manifest. make_pdf()confirms only that the target exists.Image.open()orPdfReader()reads the unauthorized local file.- The pages are c ...[truncated 498 chars]
- Remediation
View remediation
Remediation Suggestions
- Resolve every supplied path with
os.path.realpath()orpathlib.Path.resolve(). - Verify that each resolved path is contained beneath the canonical
/mnt/user-data/uploads/directory. - Reject absolute paths, traversal components, symbolic links, device files, and non-regular files.
- Use an allowlist of server-generated upload identifiers rather than accepting arbitrary filesystem paths.
- Open files using race-resistant mechanisms where supported, including
O_NOFOLLOW. - Apply file-size, extension, and content-signature validation before parsing.
Example containment policy:
python from pathlib import Path UPLOAD_ROOT = Path("/mnt/user-data/uploads").resolve() def authorized_upload(value: str) -> Path: candidate = Path(value).resolve(strict=True) if UPLOAD_ROOT not in candidate.parents: raise ValueError("File is outside the authorized upload directory") if not candidate.is_file() or candidate.is_symlink(): raise ValueError("Only regular, non-symlink upload files are allowed") return candidate- Resolve every supplied path with
