GHSA-8FQ9-273G-6MRG

Vulnerability from github – Published: 2026-06-17 18:49 – Updated: 2026-06-17 18:49
VLAI
Summary
Avo: Missing Authorization in Avo Association Attach Endpoint Allows Unauthorized Relationship Manipulation and Privilege Escalation
Details

Summary

A critical missing authorization flaw exists in Avo's association attach workflow. The UI and GET /resources/:resource/:id/:related/new path can check attach_<association>?, but the actual write endpoint, POST /resources/:resource/:id/:related, does not run the same authorization check before mutating the association.

As a result, an authenticated low-privileged Avo user can bypass hidden/disabled attach controls and directly attach related records to a parent record by sending a crafted POST request. In applications where associations represent teams, tenants, roles, projects, users, memberships, ownership, or other authorization-bearing relationships, this can lead to privilege escalation and cross-tenant data exposure.

Details

The association attach route writes relationships through Avo::AssociationsController#create:

# config/routes.rb
post "/:resource_name/:id/:related_name", to: "associations#create", as: "associations_create"

The controller registers an attach authorization callback only for new, not for create:

# app/controllers/avo/associations_controller.rb
before_action :set_attachment_record, only: [:create, :destroy]
before_action :authorize_index_action, only: :index
before_action :authorize_attach_action, only: :new
before_action :authorize_detach_action, only: :destroy

The new action is only the form-rendering step. The actual mutation happens in create:

def create
  if create_association
    create_success_action
  else
    create_fail_action
  end
end

create_association then attaches the attacker-supplied related record to the parent:

def create_association
  association_name = BaseResource.valid_association_name(@record, association_from_params)

  perform_action_and_record_errors do
    if through_reflection? && additional_params.present?
      new_join_record.save
    elsif has_many_reflection? || through_reflection?
      @record.send(association_name) << @attachment_record
    else
      @record.send(:"#{association_name}=", @attachment_record)
      @record.save!
    end
  end
end

The only attach-specific authorization helper is:

def authorize_attach_action
  authorize_if_defined "attach_#{@field.id}?"
end

Because this helper is bound only to new, a policy that denies attach_users?, attach_teams?, attach_roles?, or similar methods blocks the UI/form path but does not protect the write path.

This is inconsistent with the detach path, which does authorize the mutating destroy action:

before_action :authorize_detach_action, only: :destroy

The bug is especially dangerous because Avo already treats association authorization as an access-control boundary in UI components:

# lib/avo/concerns/checks_assoc_authorization.rb
method_name = :"#{policy_method}_#{association_name}?".to_sym

if service.has_method?(method_name, raise_exception: false)
  service.authorize_action(method_name, record:, raise_exception: false)
else
  !Avo.configuration.explicit_authorization
end

However, server-side enforcement is missing on the actual attach POST endpoint.

Proof of Concept

Prerequisites:

  1. A Rails application mounts Avo, for example at /admin.
  2. Avo authorization is enabled.
  3. A low-privileged user can authenticate to Avo.
  4. A parent record and a related record are both reachable by ID.
  5. The relevant policy denies attaching the relationship, for example:
def attach_users?
  false
end

Example target scenario:

  • Parent resource: projects
  • Parent ID: 1
  • Related association: users
  • Related user ID to attach: 42
  • Expected policy: low-privileged users must not be able to attach users to projects.

The UI/form request may be blocked:

GET /admin/resources/projects/1/users/new

But the direct write endpoint can still be invoked:

POST /admin/resources/projects/1/users
Content-Type: application/x-www-form-urlencoded

authenticity_token=<CSRF>&fields[related_id]=42

Run the attached PoC:

python poc_avo_association_attach_bypass.py \
  --base-url http://localhost:3000 \
  --avo-root /admin \
  --cookie "_app_session=<LOW_PRIVILEGED_SESSION_COOKIE>" \
  --parent-resource projects \
  --parent-id 1 \
  --related-name users \
  --related-id 42 \
  --check-new

If GET /new is forbidden or redirected but the direct POST succeeds, the authorization bypass is confirmed.

To perform the actual attach:

python poc_avo_association_attach_bypass.py \
  --base-url http://localhost:3000 \
  --avo-root /admin \
  --cookie "_app_session=<LOW_PRIVILEGED_SESSION_COOKIE>" \
  --parent-resource projects \
  --parent-id 1 \
  --related-name users \
  --related-id 42 \
  --confirm-attach

Expected vulnerable result:

  • The low-privileged user can attach the related record despite attach_<association>? being denied.
  • The parent record now includes the related record.

Impact

This vulnerability allows unauthorized relationship manipulation through Avo.

Depending on the affected association, the impact can include:

  • Privilege escalation by attaching a user to an admin group, privileged project, tenant, organization, role, or membership record.
  • Cross-tenant data exposure when tenant/user/project membership determines record visibility.
  • Integrity loss by changing ownership, assignment, access-control relationships, or business workflow state.
  • Policy bypass even when Avo UI controls correctly hide the attach button or deny the attach form.

Recommended Fix

Enforce attach authorization on the mutating endpoint.

At minimum:

before_action :authorize_attach_action, only: [:new, :create]

Additionally:

  1. Authorize against the parent record and the selected related record before writing the relationship.
  2. Ensure create fails closed when attach_<association>? is missing and explicit_authorization is enabled.
  3. Add regression tests that directly POST to /resources/:resource_name/:id/:related_name while attach_<association>? returns false.
  4. Verify has_many, has_one, has_many :through, and has_and_belongs_to_many association paths all enforce the same server-side authorization.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.32.0"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "avo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.32.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "avo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0.beta.1"
            },
            {
              "fixed": "4.0.0.beta.51"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55518"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639",
      "CWE-862",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-17T18:49:11Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "## Summary\n\nA critical missing authorization flaw exists in Avo\u0027s association attach workflow. The UI and `GET /resources/:resource/:id/:related/new` path can check `attach_\u003cassociation\u003e?`, but the actual write endpoint, `POST /resources/:resource/:id/:related`, does not run the same authorization check before mutating the association.\n\nAs a result, an authenticated low-privileged Avo user can bypass hidden/disabled attach controls and directly attach related records to a parent record by sending a crafted POST request. In applications where associations represent teams, tenants, roles, projects, users, memberships, ownership, or other authorization-bearing relationships, this can lead to privilege escalation and cross-tenant data exposure.\n\n## Details\n\nThe association attach route writes relationships through `Avo::AssociationsController#create`:\n\n```ruby\n# config/routes.rb\npost \"/:resource_name/:id/:related_name\", to: \"associations#create\", as: \"associations_create\"\n```\n\nThe controller registers an attach authorization callback only for `new`, not for `create`:\n\n```ruby\n# app/controllers/avo/associations_controller.rb\nbefore_action :set_attachment_record, only: [:create, :destroy]\nbefore_action :authorize_index_action, only: :index\nbefore_action :authorize_attach_action, only: :new\nbefore_action :authorize_detach_action, only: :destroy\n```\n\nThe `new` action is only the form-rendering step. The actual mutation happens in `create`:\n\n```ruby\ndef create\n  if create_association\n    create_success_action\n  else\n    create_fail_action\n  end\nend\n```\n\n`create_association` then attaches the attacker-supplied related record to the parent:\n\n```ruby\ndef create_association\n  association_name = BaseResource.valid_association_name(@record, association_from_params)\n\n  perform_action_and_record_errors do\n    if through_reflection? \u0026\u0026 additional_params.present?\n      new_join_record.save\n    elsif has_many_reflection? || through_reflection?\n      @record.send(association_name) \u003c\u003c @attachment_record\n    else\n      @record.send(:\"#{association_name}=\", @attachment_record)\n      @record.save!\n    end\n  end\nend\n```\n\nThe only attach-specific authorization helper is:\n\n```ruby\ndef authorize_attach_action\n  authorize_if_defined \"attach_#{@field.id}?\"\nend\n```\n\nBecause this helper is bound only to `new`, a policy that denies `attach_users?`, `attach_teams?`, `attach_roles?`, or similar methods blocks the UI/form path but does not protect the write path.\n\nThis is inconsistent with the detach path, which does authorize the mutating `destroy` action:\n\n```ruby\nbefore_action :authorize_detach_action, only: :destroy\n```\n\nThe bug is especially dangerous because Avo already treats association authorization as an access-control boundary in UI components:\n\n```ruby\n# lib/avo/concerns/checks_assoc_authorization.rb\nmethod_name = :\"#{policy_method}_#{association_name}?\".to_sym\n\nif service.has_method?(method_name, raise_exception: false)\n  service.authorize_action(method_name, record:, raise_exception: false)\nelse\n  !Avo.configuration.explicit_authorization\nend\n```\n\nHowever, server-side enforcement is missing on the actual attach POST endpoint.\n\n## Proof of Concept\n\nPrerequisites:\n\n1. A Rails application mounts Avo, for example at `/admin`.\n2. Avo authorization is enabled.\n3. A low-privileged user can authenticate to Avo.\n4. A parent record and a related record are both reachable by ID.\n5. The relevant policy denies attaching the relationship, for example:\n\n```ruby\ndef attach_users?\n  false\nend\n```\n\nExample target scenario:\n\n- Parent resource: `projects`\n- Parent ID: `1`\n- Related association: `users`\n- Related user ID to attach: `42`\n- Expected policy: low-privileged users must not be able to attach users to projects.\n\nThe UI/form request may be blocked:\n\n```http\nGET /admin/resources/projects/1/users/new\n```\n\nBut the direct write endpoint can still be invoked:\n\n```http\nPOST /admin/resources/projects/1/users\nContent-Type: application/x-www-form-urlencoded\n\nauthenticity_token=\u003cCSRF\u003e\u0026fields[related_id]=42\n```\n\nRun the attached PoC:\n\n```bash\npython poc_avo_association_attach_bypass.py \\\n  --base-url http://localhost:3000 \\\n  --avo-root /admin \\\n  --cookie \"_app_session=\u003cLOW_PRIVILEGED_SESSION_COOKIE\u003e\" \\\n  --parent-resource projects \\\n  --parent-id 1 \\\n  --related-name users \\\n  --related-id 42 \\\n  --check-new\n```\n\nIf `GET /new` is forbidden or redirected but the direct POST succeeds, the authorization bypass is confirmed.\n\nTo perform the actual attach:\n\n```bash\npython poc_avo_association_attach_bypass.py \\\n  --base-url http://localhost:3000 \\\n  --avo-root /admin \\\n  --cookie \"_app_session=\u003cLOW_PRIVILEGED_SESSION_COOKIE\u003e\" \\\n  --parent-resource projects \\\n  --parent-id 1 \\\n  --related-name users \\\n  --related-id 42 \\\n  --confirm-attach\n```\n\nExpected vulnerable result:\n\n- The low-privileged user can attach the related record despite `attach_\u003cassociation\u003e?` being denied.\n- The parent record now includes the related record.\n\n## Impact\n\nThis vulnerability allows unauthorized relationship manipulation through Avo.\n\nDepending on the affected association, the impact can include:\n\n- Privilege escalation by attaching a user to an admin group, privileged project, tenant, organization, role, or membership record.\n- Cross-tenant data exposure when tenant/user/project membership determines record visibility.\n- Integrity loss by changing ownership, assignment, access-control relationships, or business workflow state.\n- Policy bypass even when Avo UI controls correctly hide the attach button or deny the attach form.\n\n## Recommended Fix\n\nEnforce attach authorization on the mutating endpoint.\n\nAt minimum:\n\n```ruby\nbefore_action :authorize_attach_action, only: [:new, :create]\n```\n\nAdditionally:\n\n1. Authorize against the parent record and the selected related record before writing the relationship.\n2. Ensure `create` fails closed when `attach_\u003cassociation\u003e?` is missing and `explicit_authorization` is enabled.\n3. Add regression tests that directly POST to `/resources/:resource_name/:id/:related_name` while `attach_\u003cassociation\u003e?` returns `false`.\n4. Verify `has_many`, `has_one`, `has_many :through`, and `has_and_belongs_to_many` association paths all enforce the same server-side authorization.",
  "id": "GHSA-8fq9-273g-6mrg",
  "modified": "2026-06-17T18:49:11Z",
  "published": "2026-06-17T18:49:11Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/avo-hq/avo/security/advisories/GHSA-8fq9-273g-6mrg"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/avo-hq/avo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Avo: Missing Authorization in Avo Association Attach Endpoint Allows Unauthorized Relationship Manipulation and Privilege Escalation"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…