GitHub Advisoryadvisory · RubyGems

GHSA-pqpw-cvm4-8mv9 Avo: Direct attachment upload endpoint lacks upload authorization and bypasses field-level upload policy

MEDIUM Published 2026-07-09 Modified 2026-07-09 Official page ↗
Aliases
CVE-2026-53769
Ecosystem
RubyGems
Packages
avo
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
Weaknesses
CWE-862, CWE-863
Reviewed
2026-07-09

Affected packages

• RubyGems avo: >= 2.28.0, < 3.32.0 → fixed in 3.32.0

Description

Summary Avo's direct attachment upload endpoint lacks server-side upload authorization and bypasses the documented field-level upload policy methods such as `upload_{FIELD_ID}?`. An authenticated Avo user who can reach the Avo attachment upload endpoint can replace or add attachment content, including binary content, filename, and content-type metadata, on a resolved record even when both `update?` and `upload_?` policies deny the operation. This primarily affects multi-role Avo Pro/Advanced-style deployments where non-administrator or restricted operator users can reach Avo and per-record or per-field operations are expected to be enforced by policies. Details Avo exposes a direct attachment upload endpoint: ```text POST //avo_api/resources/:resource_name/:id/attachments/ ``` The vulnerable code path is `Avo::AttachmentsController#create`: • `app/controllers/avo/attachments_controller.rb:9-24` Current behavior: ```ruby def create blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename] association_name = BaseResource.valid_attachment_name(@record, params[:attachment_key]) if association_name @record.send(association_name).attach blob elsif params[:key].blank? raise ActionController::BadRequest.new(...) end render json: { url: main_app.url_for(blob), href: main_app.url_for(blob) } end ``` This endpoint creates an `ActiveStorage::Blob` before validating the requested attachment association and before any upload authorization could be enforced. If `attachment_key` resolves to an association, the blob is attached to the target record. If the request uses the key-based/Trix path, the endpoint can still persist the blob and return `url`/`href` even when no attachment association is resolved on the target record. The controller never calls: • `@resource.authorization.authorize_action("upload_?", record: @record, ...)` • `@resource.authorization.authorize_action("update?", record: @record, ...)` This is inconsistent with the rest of Avo's file authorization implementation. The field-level file authorization concern defines upload authorization as: • `lib/avo/fields/concerns/file_authorization.rb:11-12` • `lib/avo/fields/concerns/file_authorization.rb:25-27` ```ruby def can_upload_file? authorize_file_action(:upload) end def authorize_file_action(action) authorize_action("#{action}_#{id}?", record: record, raise_exception: false) end ``` That upload policy is used by UI components to decide whether to render upload controls, but the server-side upload endpoint does not enforce the same policy. A user can therefore bypass the policy by directly POSTing to the endpoint. By contrast, attachment deletion does call attachment-specific authorization: • `app/controllers/avo/attachments_controller.rb:27-65` ```ruby def destroy if authorized_to :delete ... end end def authorized_to(action) @resource.authorization.authorize_action("#{action}_#{params[:attachment_name]}?", record: @record, raise_exception: false) end ``` The asymmetry is: • `destroy`: calls `delete_?` • `create`: does not call `upload_?` or `update?` The field-level upload authorization intent is also documented by Avo: • Issue #1624 requested the "Ability to police each file upload/download/delete". https://github.com/avo-hq/avo/issues/1624 • PR #1625 introduced `upload_cover_photo?` as an example upload policy method. https://github.com/avo-hq/avo/pull/1625 • PR #1667 clarified the policy semantics: "From now on, only field level authorization will be considered. If it is defined and returns true, the action will be granted; otherwise, it will not." https://github.com/avo-hq/avo/pull/1667 • The current Avo authorization documentation lists `upload_{FIELD_ID}?` as the policy method controlling whether a user can upload an attachment, stating: "Controls whether the user can upload the attachment." https://docs.avohq.io/4.0/authorization.html • The same documentation says the same `upload_file?` policy method will be used to "authorize the file upload" in action file fields. https://docs.avohq.io/4.0/authorization.html Affected versions observed: • PoC-confirmed: Avo `3.31.2`, commit `46aa6b3bc9e3283110c39e58cfec8bb95adc1897` • Same vulnerable code path by source inspection: `origin/main` HEAD as of 2026-05-29, `9e23ddc88f2b1e762e4a5ec35a6f86370ac16c73` • Same vulnerable code path by source inspection: Avo `v4.0.0.beta.26`, `6d339595a27f8779cb99b4aa38ddc97cb702b30f` • Same vulnerable code path by source inspection: `origin/4-dev` at version `4.0.0.beta.40`, `7ab9794f8b044b11b9677cdc57547d99cf96c3f3` Suggested affected range for the GitHub Security Advisory form: ```text >= 2.28.0 ``` Rationale: • The direct attachment upload endpoint exists without upload authorization from commit `ab5f5970e2aa76e6ca0a95bf04f510ba7ed5e858` (`feature: trix attachments`, 2021-04-24), first included in tag `v1.4.0`. • The documented field-level upload policy bypass is confirmed from commit `667049ceeda838394214489693d088708d9da77d` (`feature: field level file authorization`, 2023-03-12), first included in tag `v2.28.0`. • PR #1667 later clarified the field-level-only grant/deny semantics in commit `31b6ef94f8cc4c340a2a75eec36c838cda933ce7`, first included in tag `v2.30.1`. From a narrow missing-authorization perspective, the issue exists from `v1.4.0`; the `>= 2.28.0` range is a conservative submission range anchored to the documented field-level upload policy boundary. Fixed version: to be determined by maintainers. PoC A local request spec was used to reproduce the issue. The PoC adds `pundit` to the test group and replaces `Avo::Services::AuthorizationService` with a test double. The test double records every `authorize_action` invocation and, when invoked, delegates the decision to a `PostPolicy` resolved via `Pundit.policy!`. The policy setup is: • `PostPolicy#update?` returns `false` • `PostPolicy#upload_attachments?` returns `false` • `PostPolicy#upload_cover_photo?` returns `false` • `PostPolicy#method_missing` returns `true` for all other policy methods ending in `?`, simulating a user with general read access but explicit update/upload denial • the replacement authorization service records all `authorize_action` calls The open-source repository does not include Avo Pro's authorization client. The critical observation is independent of which client is plugged in: the vulnerable endpoint never invokes `authorize_action` at all, so the call list remains empty. The PoC user has `roles: {admin: true}`, which is required only to pass the dummy app's coarse route-level guard: ```ruby authenticate :user, ->(user) { user.is_admin? } ``` The vulnerability under test is one layer below that guard: the fine-grained record/field authorization that Avo Pro/Pundit deployments would normally enforce. The dummy route gate is not part of Avo's upload policy decision. In a deployment where non-admin operators reach Avo through a different authentication rule, `AttachmentsController#create` would still skip `authorize_action` for the upload. Policy scoping is orthogonal to this finding. Even if `apply_policy` filtered the record set during lookup, `AttachmentsController#create` would still not authorize the upload action itself for records that survive the scope. The spec demonstrates: 1. Normal record update is denied by policy and does not persist changes. 2. Direct upload with `attachment_key=cover_photo` succeeds even though `PostPolicy#upload_cover_photo?` returns `false`, replacing the existing `has_one_attached` `cover_photo` blob. 3. Direct upload with `attachment_key=attachments` succeeds even though `PostPolicy#upload_attachments?` returns `false`, adding to a `has_many_attached` association. 4. Direct upload with `params[:key]` and no attachment association succeeds, crea…

Related

Alias · CVE (1)

  • CVE-2026-53769 Avo is a framework to create admin panels for Ruby on Rails apps. vulnerability

Weakness · CWE (2)

  • CWE-862 Missing Authorization weakness
  • CWE-863 Incorrect Authorization weakness

References