fix(drive): require upload access to probe a folder for filenames - #603
fix(drive): require upload access to probe a folder for filenames#603ebrahimgamdiwala wants to merge 1 commit into
Conversation
`does_entity_exist` and `get_new_title` were whitelisted with no permission
check. Both take a folder id straight from the caller, so any logged-in user
could ask whether a given filename exists in any folder on the site:
/api/method/suite.drive.api.files.does_entity_exist?name=<name>&folder=<id>
`get_new_title` leaks strictly more - on an exact match it returns the name
suffixed with a count of the matching siblings, so the reply also reveals how
many similarly named files the folder holds.
Filenames are content. The realistic caller is not someone guessing ids, it
is someone whose access was revoked: folder ids are learned while shared and
outlive the grant, so an unshared user can keep polling a folder they can no
longer open and keep getting answers.
Both now resolve the folder against `upload`.
Why `upload` and not `read`: these serve only the uploader naming a file it
is about to write, and every principal the share dialog grants `upload` also
holds `read` (`ShareDialog.vue` sets `read: 1` on all three presets), so
`upload` is the narrower gate of the two - it refuses everyone `read` would,
plus view-only users who never call these. It also puts them in agreement
with `upload_file` and `can_create_in_folder`, which resolve the same folder
at the same level.
The guard cannot break an upload that would otherwise succeed: `upload_file`
already applies the identical check, with the same default-folder resolution
and the same message, moments later in the same flow.
`suite/writer/api/docs.py` calls `get_new_title`. It already checked the same
level on the same folder two lines below; the check is moved above the call
so Writer keeps its own error message and skips a lookup for a caller it is
about to turn away. No behavioural change - the same callers are refused.
Adds 4 regression tests to the existing integration suite; the 2 covering the
unshared and revoked callers were verified failing without this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confidence Score: 4/5The PR appears safe to merge, with only non-blocking request-validation hardening needed on the modified endpoints. The authorization behavior is consistent with existing upload flows, but malformed API argument types can still reach permission and database helpers without controlled validation. Files Needing Attention: suite/drive/api/files.py Reviews (1): Last reviewed commit: "fix(drive): require upload access to pro..." | Re-trigger Greptile |
|
|
||
| @frappe.whitelist() | ||
| def does_entity_exist(name: str | None = None, folder: str | None = None): | ||
| """Whether `folder` already holds a file called `name`. |
There was a problem hiding this comment.
Validate whitelisted string inputs
These endpoints still pass caller-controlled name, folder, title, and parent_name values into permission and database helpers without runtime type validation, producing inconsistent framework errors for malformed requests.
Context Used: Guidelines for reviewing Frappe Framework applicat... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Frappe already applies runtime type validation to whitelisted arguments. frappe.whitelist() wraps the function in validate_argument_types, which validates every argument against its annotation with pydantic and raises FrappeTypeError before the function body runs.
Verified against these two endpoints:
folder={'read': 0} → FrappeTypeError: Argument 'folder' in 'suite.drive.api.files.does_entity_exist' should be of type 'str | None' but got 'dict'
folder=['a', 'b'] → FrappeTypeError: ... but got 'list'
folder=12345 → FrappeTypeError: ... but got 'int'
All three are rejected before reaching user_has_permission or frappe.db.exists. The str | None annotations on name, folder, title and parent_name are the validation — adding manual isinstance checks would be dead code.
The one thing in this area that does differ: a well-formed id naming no record surfaces DoesNotExistError rather than PermissionError. That matches upload_file, which resolves the same folder at the same permission level a moment later in the same flow, so the two endpoints stay consistent with each other. Leaving it as is.
does_entity_existandget_new_titlewere whitelisted with no permission check. Both take a folder id straight from the caller, so any logged-in user could ask whether a given filename exists in any folder on the site:get_new_titleleaks strictly more - on an exact match it returns the name suffixed with a count of the matching siblings, so the reply also reveals how many similarly named files the folder holds.Filenames are content. The realistic caller is not someone guessing ids, it is someone whose access was revoked: folder ids are learned while shared and outlive the grant, so an unshared user can keep polling a folder they can no longer open and keep getting answers.
Both now resolve the folder against
upload.Why
uploadand notread: these serve only the uploader naming a file it is about to write, and every principal the share dialog grantsuploadalso holdsread(ShareDialog.vuesetsread: 1on all three presets), souploadis the narrower gate of the two - it refuses everyonereadwould, plus view-only users who never call these. It also puts them in agreement withupload_fileandcan_create_in_folder, which resolve the same folder at the same level.The guard cannot break an upload that would otherwise succeed:
upload_filealready applies the identical check, with the same default-folder resolution and the same message, moments later in the same flow.suite/writer/api/docs.pycallsget_new_title. It already checked the same level on the same folder two lines below; the check is moved above the call so Writer keeps its own error message and skips a lookup for a caller it is about to turn away. No behavioural change - the same callers are refused.Adds 4 regression tests to the existing integration suite; the 2 covering the unshared and revoked callers were verified failing without this change.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.