{"id":"GHSA-8fq9-273g-6mrg","summary":"Avo: Missing Authorization in Avo Association Attach Endpoint Allows Unauthorized Relationship Manipulation and Privilege Escalation","details":"## Summary\n\nA critical missing authorization flaw exists in Avo's 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? && 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&fields[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.","aliases":["CVE-2026-55518"],"modified":"2026-07-18T17:30:29.248210428Z","published":"2026-06-17T18:49:11Z","database_specific":{"nvd_published_at":"2026-07-17T21:17:09Z","cwe_ids":["CWE-639","CWE-862","CWE-863"],"severity":"CRITICAL","github_reviewed":true,"github_reviewed_at":"2026-06-17T18:49:11Z"},"references":[{"type":"WEB","url":"https://github.com/avo-hq/avo/security/advisories/GHSA-8fq9-273g-6mrg"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55518"},{"type":"WEB","url":"https://github.com/avo-hq/avo/pull/4568"},{"type":"WEB","url":"https://github.com/avo-hq/avo/commit/995928e586fd1788dd496bd51c4dbe4a79cb2b9c"},{"type":"PACKAGE","url":"https://github.com/avo-hq/avo"},{"type":"WEB","url":"https://github.com/avo-hq/avo/releases/tag/v3.32.1"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/avo/CVE-2026-55518.yml"},{"type":"WEB","url":"https://www.cve.org/CVERecord?id=CVE-2026-55518"}],"affected":[{"package":{"name":"avo","ecosystem":"RubyGems","purl":"pkg:gem/avo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.32.1"}]}],"versions":["0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.3.1","0.3.2","0.4.1","0.4.10","0.4.2","0.4.3","0.4.4","0.4.5","0.4.6","0.4.7","0.4.8","0.4.9","0.5.0.beta1","0.5.0.beta10","0.5.0.beta11","0.5.0.beta12","0.5.0.beta13","0.5.0.beta14","0.5.0.beta15","0.5.0.beta2","0.5.0.beta3","0.5.0.beta4","0.5.0.beta5","0.5.0.beta6","0.5.0.beta7","0.5.0.beta8","0.5.0.beta9","1.0.0","1.0.1","1.0.2","1.0.4","1.0.5","1.1.0","1.1.0.pre.1","1.10.0","1.10.1","1.10.2","1.10.3","1.11.0","1.11.1","1.11.2","1.11.3","1.11.4","1.11.5","1.11.6","1.12.0","1.12.1","1.12.2","1.12.3","1.12.4","1.13.0","1.13.1","1.13.2","1.13.3","1.14.0","1.15.0","1.15.0.pre.1","1.16.0","1.16.1","1.16.2","1.16.3","1.16.4","1.17.0","1.17.1","1.18.0","1.18.0.pre.1","1.18.0.pre.2","1.18.0.pre.3","1.18.1","1.18.2","1.18.2.pre.0","1.19.0","1.19.1.pre.1","1.19.1.pre.10","1.19.1.pre.11","1.19.1.pre.2","1.19.1.pre.3","1.19.1.pre.4","1.19.1.pre.5","1.19.1.pre.6","1.19.1.pre.7","1.19.1.pre.8","1.19.1.pre.9","1.2.10","1.2.11.pre.1","1.2.11.pre.2","1.2.11.pre.3","1.2.11.pre.4","1.2.2","1.2.3","1.2.4","1.2.5","1.2.6","1.2.6.pre.1","1.2.7","1.2.8","1.2.9","1.20.1","1.20.2.pre.1","1.20.2.pre.2","1.21.0","1.21.0.pre.1","1.21.1.pre.1","1.22.0","1.22.0.pre.1","1.22.1","1.22.1.pre.1","1.22.1.pre.2","1.22.2","1.22.3","1.22.4","1.23.0","1.24.0","1.24.1","1.24.2","1.25.0","1.25.1","1.25.2","1.3.0","1.3.0.pre.1","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.3.5.pre.1","1.4.0","1.4.0.pre.1","1.4.1","1.4.2","1.4.3","1.4.4","1.4.4.pre.1","1.4.5.pre.1","1.5.0","1.5.1","1.5.2","1.5.3","1.5.4","1.5.5","1.6.0","1.6.1","1.6.2.pre.1","1.6.3.pre.1","1.6.3.pre.2","1.6.3.pre.3","1.6.4.pre.1","1.7.0","1.7.1","1.7.2","1.7.3","1.7.3.pre.1","1.8.0","1.8.1","1.8.2","1.8.3","1.8.4","1.9.0","1.9.1","2.0.0","2.1.0","2.1.1","2.1.2.pre1","2.1.2.pre2","2.10.0","2.10.2","2.10.3.pre.1","2.11.0","2.11.1","2.11.1.pre.1","2.11.1.pre.2","2.11.1.pre.3","2.11.2.pre.1","2.11.2.pre.2","2.11.2.pre.3","2.11.3.pre.1","2.11.3.pre.2","2.11.3.pre.3","2.12.0","2.12.1.pre.1","2.13.0","2.13.1","2.13.2.pre.1","2.13.2.pre.2","2.13.3.pre.1","2.13.3.pre.2","2.13.3.pre.3","2.13.3.pre.4","2.13.4.pre.1","2.13.5.pre.1","2.13.5.pre.2","2.13.6.pre.1","2.13.6.pre.2","2.14.0","2.14.1","2.14.1.pre.1","2.14.2","2.14.2.pre.1","2.14.3.pre.1.branding","2.14.3.pre.2.tailwindcss","2.14.3.pre.3.jsbundling","2.14.3.pre.4.tosqlfix","2.14.3.pre.5.nosprockets","2.14.3.pre.6.nosprockets","2.14.3.pre.7.polytranslations1","2.15.0","2.15.1","2.15.2","2.15.2.pre.1","2.15.3","2.15.3.pre.1.data.attrs.to.sidebar.items","2.16.0","2.16.1.pre.1.nativefields","2.17.0","2.17.1.pre.1.zeitwerk.eager.load.dir","2.17.1.pre.2.customauthorizationclients","2.17.1.pre.3","2.17.1.pre.4.issue.1342","2.17.1.pre.5.stackedlayout","2.18.0","2.18.1","2.18.1.pre.1.eagerloaddirs","2.19.0","2.2.0","2.2.1","2.2.2","2.20.0","2.21.0","2.21.1.pre.issue1444","2.21.1.pre.issue1450","2.21.1.pre.pr1476","2.21.1.pre.pr1484","2.21.2.pre.pr1486","2.21.3.pre.pr1489","2.22.0","2.23.0","2.23.1","2.23.2","2.23.3.pre.1.pr1529","2.24.0","2.24.1","2.25.0","2.25.1.pre.1.pr1579","2.26.0","2.26.1.pr1584.pre.1","2.26.2.pre.pr1579","2.26.3.pre.pr1601","2.27.0","2.27.1","2.27.2.pre.pr1606","2.28.0","2.28.1.pre.pr1642","2.28.2.pre.pr1642","2.28.3.pre.pr1646","2.29.0","2.29.1","2.29.1.pre.pr1652","2.3.0","2.3.1.pre.1","2.3.1.pre.2","2.3.1.pre.3","2.3.1.pre.4","2.3.1.pre.5","2.3.1.pre.6","2.30.0","2.30.1","2.30.1.pre1.pr1683","2.30.1.pre2.pr1683","2.30.1.pre3.pr1683","2.30.1.pre4.pr1683","2.30.2","2.31.0","2.32.0","2.32.1","2.32.2","2.32.3","2.32.4","2.32.5","2.32.6","2.33.0","2.33.1","2.33.2","2.33.3","2.33.3.pre.1","2.33.3.pre.2","2.34.0","2.34.1","2.34.2","2.34.3","2.34.4","2.34.4.pre.1","2.34.5","2.34.6","2.34.7.pre.1","2.35.0","2.36.0","2.36.1","2.36.2","2.36.3","2.37.0","2.37.1","2.37.2","2.38.0","2.39.0","2.4.0","2.4.1","2.40.0","2.41.0","2.42.0","2.42.1","2.42.2","2.43.0","2.44.0","2.45.0","2.46.0","2.47.0","2.48.0","2.49.0","2.5.0","2.5.1","2.5.2.pre.1","2.5.2.pre.2","2.5.2.pre.3","2.5.2.pre.4","2.5.2.pre.5","2.5.2.pre.6","2.5.2.pre.7","2.50.0","2.51.0","2.52.0","2.53.0","2.6.0","2.6.1.pre.1","2.6.1.pre.2","2.7.0","2.7.1.pre.1","2.8.0","2.9.0","2.9.1.pre1","2.9.1.pre2","2.9.1.pre3","2.9.1.pre4","2.9.1.pre5","2.9.1.pre6","2.9.1.pre7","2.9.2.pre1","3.0.0.beta1","3.0.0.pre1","3.0.0.pre10","3.0.0.pre11","3.0.0.pre12","3.0.0.pre13","3.0.0.pre14","3.0.0.pre15","3.0.0.pre16","3.0.0.pre17","3.0.0.pre18","3.0.0.pre19","3.0.0.pre2","3.0.0.pre3","3.0.0.pre4","3.0.0.pre5","3.0.0.pre6","3.0.0.pre7","3.0.0.pre8","3.0.0.pre9","3.0.1.beta1","3.0.1.beta10","3.0.1.beta11","3.0.1.beta12","3.0.1.beta13","3.0.1.beta14","3.0.1.beta15","3.0.1.beta16","3.0.1.beta17","3.0.1.beta18","3.0.1.beta19","3.0.1.beta2","3.0.1.beta20","3.0.1.beta21","3.0.1.beta22","3.0.1.beta23","3.0.1.beta24","3.0.1.beta3","3.0.1.beta4","3.0.1.beta5","3.0.1.beta6","3.0.1.beta7","3.0.1.beta8","3.0.1.beta9","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.10.0","3.10.1","3.10.10","3.10.2","3.10.3","3.10.4","3.10.5","3.10.6","3.10.7","3.10.8","3.10.9","3.11.0","3.11.1","3.11.10","3.11.2","3.11.3","3.11.4","3.11.5","3.11.6","3.11.7","3.11.8","3.11.9","3.12.0","3.13.0","3.13.1","3.13.2","3.13.3","3.13.4","3.13.5","3.13.6","3.13.7","3.14.0","3.14.1","3.14.2","3.14.3","3.14.4","3.14.5","3.15.0","3.15.1","3.15.2","3.15.3","3.15.4","3.15.5","3.15.6","3.15.7","3.16.0","3.16.1","3.16.2","3.16.3","3.16.4","3.16.5","3.16.6","3.17.0","3.17.1","3.17.1.tw4","3.17.2","3.17.2.tw4","3.17.3","3.17.3.tw4","3.17.4","3.17.4.tw4","3.17.5","3.17.5.tw4","3.17.6","3.17.6.tw4","3.17.7","3.17.8","3.17.8.tw4","3.17.9","3.17.9.beta1","3.17.9.beta2","3.17.9.tw4","3.18.0","3.18.0.tw4","3.18.1","3.18.1.tw4","3.18.2","3.18.2.tw4","3.19.0","3.19.1","3.19.2","3.19.3","3.2.0","3.2.1","3.2.2","3.2.3","3.20.0","3.20.1","3.20.2","3.20.3","3.21.0","3.21.1","3.22.0","3.22.2","3.22.3","3.22.4","3.22.5","3.23.0","3.23.1","3.24.0","3.24.1","3.25.0","3.25.1","3.25.2","3.25.3","3.26.0","3.26.2","3.26.3","3.26.4","3.27.0","3.28.0","3.29.0","3.29.1","3.3.0","3.3.1","3.3.2","3.3.3","3.3.4","3.3.5","3.3.6","3.30.0","3.30.1","3.30.2","3.30.3","3.30.4","3.31.0","3.31.1","3.31.2","3.32.0","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.5.0","3.5.1","3.5.2","3.5.3","3.5.4","3.5.5","3.5.6","3.5.6.beta1","3.5.7","3.5.8","3.6.0","3.6.1","3.6.2","3.6.3","3.6.4","3.7.0","3.7.1","3.7.2","3.7.3","3.7.4","3.8.0","3.8.1","3.8.2","3.9.0","3.9.1","3.9.2"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.32.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-8fq9-273g-6mrg/GHSA-8fq9-273g-6mrg.json"}},{"package":{"name":"avo","ecosystem":"RubyGems","purl":"pkg:gem/avo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0.beta.1"},{"fixed":"4.0.0.beta.51"}]}],"versions":["4.0.0.beta.1","4.0.0.beta.10","4.0.0.beta.11","4.0.0.beta.12","4.0.0.beta.13","4.0.0.beta.14","4.0.0.beta.15","4.0.0.beta.16","4.0.0.beta.17","4.0.0.beta.18","4.0.0.beta.19","4.0.0.beta.2","4.0.0.beta.20","4.0.0.beta.21","4.0.0.beta.22","4.0.0.beta.23","4.0.0.beta.24","4.0.0.beta.25","4.0.0.beta.26","4.0.0.beta.27","4.0.0.beta.28","4.0.0.beta.29","4.0.0.beta.3","4.0.0.beta.30","4.0.0.beta.31","4.0.0.beta.32","4.0.0.beta.33","4.0.0.beta.34","4.0.0.beta.35","4.0.0.beta.36","4.0.0.beta.37","4.0.0.beta.38","4.0.0.beta.39","4.0.0.beta.4","4.0.0.beta.40","4.0.0.beta.41","4.0.0.beta.42","4.0.0.beta.43","4.0.0.beta.44","4.0.0.beta.45","4.0.0.beta.46","4.0.0.beta.47","4.0.0.beta.48","4.0.0.beta.49","4.0.0.beta.5","4.0.0.beta.50","4.0.0.beta.6","4.0.0.beta.7","4.0.0.beta.8","4.0.0.beta.9"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-8fq9-273g-6mrg/GHSA-8fq9-273g-6mrg.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N"}]}