fix(utils): prevent path traversal in encode_images image filenames - #118
Open
andesyteoss wants to merge 1 commit into
Open
fix(utils): prevent path traversal in encode_images image filenames#118andesyteoss wants to merge 1 commit into
andesyteoss wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthrough
ChangesImage storage hardening
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
omniparse/utils.py (1)
16-26: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPass the image directly here.
responseDocument.add_imagealready acceptsPIL.Image, so the temp-file/base64 round-trip is redundant; removing it also lets you dropimport tempfile.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@omniparse/utils.py` around lines 16 - 26, Update the image-handling flow around inputDocument.add_image to pass the PIL.Image object directly instead of saving it to a temporary PNG, reading bytes, and base64-encoding them. Remove the now-unused tempfile import and any related temporary-file conversion logic, while preserving the existing image_name value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@omniparse/utils.py`:
- Around line 16-26: Update the image-handling flow around
inputDocument.add_image to pass the PIL.Image object directly instead of saving
it to a temporary PNG, reading bytes, and base64-encoding them. Remove the
now-unused tempfile import and any related temporary-file conversion logic,
while preserving the existing image_name value.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
omniparse.utils.encode_imageswrites each parsed image to disk using the dict key from the parser as the full file path, then deletes it:filenameis not a sanitized basename — it is whatever key the underlying document parser (marker) emits for an embedded image. If any code path allows a document to influence that key (image resource names, XObject names in PDFs, embedded media names in PPTX/DOCX), a value like../../etc/omniparse_pwnor an absolute path becomes:image.save(filename, "PNG")).os.remove(filename)) — the same path is unlinked at the end.encode_imagesis reached from unauthenticated FastAPI routes (omniparse/documents/router.py:/pdf,/ppt,/docs, and the image router), so the trigger is an unauthenticated document upload.omniparse.utils.encode_images(omniparse/utils.py)omniparse/documents/__init__.pylines 57/114/169,omniparse/image/__init__.pyline 96document_router.post("/pdf" | "/ppt" | "/docs")inomniparse/documents/router.py; image routerimagesdict →filenamekey →image.save(filename)/os.remove(filename)Fix
Two changes in
encode_images, both minimal and behavior-preserving for legitimate inputs:safe_name = os.path.basename(filename) or f"image_{i}.png". Any../segments or absolute-path prefixes are discarded before the value is ever handed to the filesystem.tempfile.TemporaryDirectory()gives a fresh, private directory per call. The image is saved inside it, read back, and the directory (with all contents) is removed on context exit. This eliminates the separateos.remove(filename)call, which was itself a second traversal sink, and also removes a pre-existing bug where the file was written into the process CWD.The
image_namerecorded on the response document is now the sanitized basename, which is what downstream consumers already treat as a display name.Tests
python -c "from omniparse.utils import encode_images"— imports cleanly.imagesdict with keys../../evil.png,/tmp/absolute.png, and a normalimg_0.png, plus a stub PIL-like object with a.save(path, fmt)method. Before the patch,savewas invoked with the raw traversal path; after the patch,saveis only ever invoked with a path inside theTemporaryDirectory, and the recordedimage_nameisevil.png/absolute.png/img_0.pngrespectively. No file is left on disk after the call.git diff main..HEAD --stat→ 1 file changed, 17 insertions, 12 deletions. No API surface change.Adversarial review
Before submitting we tried to disprove this. The main "is it really exploitable?" question is whether the marker parser actually propagates document-controlled bytes into the image dict keys. We could not fully confirm that against marker's source in this environment, so the exploit precondition (crafted document yields a traversal-shaped key) is not proven end-to-end here. However: (a) the sink is unambiguously unsafe —
image.save(filename)andos.remove(filename)on an unvalidated string is a path-traversal primitive regardless of the current parser's behavior; (b) the routes are unauthenticated (noDepends(...)auth guard ondocument_routeror the image router), so any exploitability is remote and pre-auth; (c) the fix is defense-in-depth that costs nothing —os.path.basename+tempfile.TemporaryDirectoryis standard practice and does not change legitimate output. Even in the worst case where today's marker never emits traversal keys, this patch prevents a future parser change or a new caller from silently reintroducing the sink.Proof of concept
Reproduce the sink behavior directly against the pre-patch function (no marker needed — this demonstrates that the function trusts its input):
End-to-end trigger shape against a running server (unauthenticated):
curl -X POST -F "file=@crafted.pdf" http://localhost:8000/parse_document/pdfwhere
crafted.pdfis a document whose parser output contains an image resource name shaped like../../tmp/x.png. The server-side effect isimage.save("../../tmp/x.png", "PNG")followed byos.remove("../../tmp/x.png"), both executed with the omniparse process's privileges.Impact
Happy to iterate on the patch shape if you'd prefer a different sanitization strategy (e.g.
secure_filenamefrom werkzeug, or enforcing a UUID name). Thanks for maintaining omniparse.Summary by CodeRabbit