CVE-2026-84707 automation-controller: automation-controller-container: automation-controller: host_filter SmartFilter ORM traversal exposes JobEvent/AdHocCommandEvent event_data and stdout to users without permission on the job, enabling blind charact ...
A flaw was found in Red Hat Ansible Automation Platform's automation-controller.
The host_filter query parameter on the hosts list API is parsed into a raw Django
ORM filter whose lookup path is validated only against a sensitive-field blocklist,
with no authorization check on the database relations it traverses. Because job
event and ad-hoc command output fields are not on that blocklist, an authenticated
user holding only the Read role on an inventory can construct filters that traverse
into the output of jobs they have no permission to view and use the returned host
count as a boolean oracle. Using regular-expression lookups, the attacker can
extract, character by character, the output (which routinely contains plaintext
credentials, tokens, and command results) of jobs and ad-hoc commands belonging to
other organizations, resulting in cross-tenant disclosure of job output
A flaw was found in automation-controller (AWX). GET /api/controller/v2/hosts/
accepts a host_filter query parameter that HostList.get_queryset (awx/api/views/
__init__.py:1938-1944) parses with SmartFilter.query_from_string and intersects
with the RBAC-filtered host queryset via `qs &= filter_qs`; that intersection
restricts only which Host rows are returned, not which related rows the lookup's
JOINs traverse. SmartFilter.BoolOperand (awx/main/utils/filters.py:151-170)
validates the user-supplied lookup PATH only by calling FieldLookupBackend().
get_field_from_lookup(Host, k) and then executes Host.objects.filter(**{k: v})
directly, including __regex and __icontains lookups. FieldLookupBackend (django-
ansible-base ansible_base/rest_filters/utils.py:9-69) rejects only fields in
PASSWORD_FIELDS/encrypted_fields, fields marked __prevent_search__, and traversal
loops -- it performs no authorization check on the rows the relation traversal
reaches. JobEvent.event_data/stdout and AdHocCommandEvent.event_data/stdout
(awx/main/models/events.py:215,253,695,705) are not wrapped in prevent_search(),
unlike extra_vars, job_env, job_args and survey_passwords which are. Consequently a
user with only the Read role on an inventory can submit host_filter values such as
last_job__job_events__event_data__regex=<pattern>,
job_events_as_primary_host__stdout__icontains=<substring>, or
ad_hoc_command_events__event_data__regex=<pattern> and read the returned host count
as a one-bit oracle. Since __regex is accepted, this allows efficient character-by-
character extraction of the event_data/stdout of jobs and ad-hoc commands the
caller is explicitly denied (HTTP 403) via the direct API, for any job that ever
ran against a host in a readable inventory -- including jobs in other organizations
when a host spans inventories across organizations. Job output routinely contains
plaintext credentials, API tokens, command output and host facts. This is a
horizontal privilege escalation from inventory-read to read-arbitrary-job-output.
Discovered internally (pentest-aap); verified live on AAP 2.7 / automation-
controller 4.8.1 (the forbidden job's playbook_uuid was extracted exactly); still
present on devel.
Upstream: github.com/ansible/awx (awx/api/views/__init__.py HostList.get_queryset;
awx/main/utils/filters.py SmartFilter.BoolOperand; sensitive fields in
awx/main/models/events.py) and github.com/ansible/django-ansible-base
(ansible_base/rest_filters/utils.py get_fields_from_path).
This issue has been addressed in the following products:
Red Hat Ansible Automation Platform 2.6 for RHEL 10
Red Hat Ansible Automation Platform 2.6 for RHEL 9
Via RHSA-2026:71113 https://access.redhat.com/errata/RHSA-2026:71113
This issue has been addressed in the following products:
Red Hat Ansible Automation Platform 2.5 for RHEL 9
Red Hat Ansible Automation Platform 2.5 for RHEL 8
Via RHSA-2026:71114 https://access.redhat.com/errata/RHSA-2026:71114
A flaw was found in automation-controller (AWX). GET /api/controller/v2/hosts/ accepts a host_filter query parameter that HostList.get_queryset (awx/api/views/ __init__.py:1938-1944) parses with SmartFilter.query_from_string and intersects with the RBAC-filtered host queryset via `qs &= filter_qs`; that intersection restricts only which Host rows are returned, not which related rows the lookup's JOINs traverse. SmartFilter.BoolOperand (awx/main/utils/filters.py:151-170) validates the user-supplied lookup PATH only by calling FieldLookupBackend(). get_field_from_lookup(Host, k) and then executes Host.objects.filter(**{k: v}) directly, including __regex and __icontains lookups. FieldLookupBackend (django- ansible-base ansible_base/rest_filters/utils.py:9-69) rejects only fields in PASSWORD_FIELDS/encrypted_fields, fields marked __prevent_search__, and traversal loops -- it performs no authorization check on the rows the relation traversal reaches. JobEvent.event_data/stdout and AdHocCommandEvent.event_data/stdout (awx/main/models/events.py:215,253,695,705) are not wrapped in prevent_search(), unlike extra_vars, job_env, job_args and survey_passwords which are. Consequently a user with only the Read role on an inventory can submit host_filter values such as last_job__job_events__event_data__regex=<pattern>, job_events_as_primary_host__stdout__icontains=<substring>, or ad_hoc_command_events__event_data__regex=<pattern> and read the returned host count as a one-bit oracle. Since __regex is accepted, this allows efficient character-by- character extraction of the event_data/stdout of jobs and ad-hoc commands the caller is explicitly denied (HTTP 403) via the direct API, for any job that ever ran against a host in a readable inventory -- including jobs in other organizations when a host spans inventories across organizations. Job output routinely contains plaintext credentials, API tokens, command output and host facts. This is a horizontal privilege escalation from inventory-read to read-arbitrary-job-output. Discovered internally (pentest-aap); verified live on AAP 2.7 / automation- controller 4.8.1 (the forbidden job's playbook_uuid was extracted exactly); still present on devel. Upstream: github.com/ansible/awx (awx/api/views/__init__.py HostList.get_queryset; awx/main/utils/filters.py SmartFilter.BoolOperand; sensitive fields in awx/main/models/events.py) and github.com/ansible/django-ansible-base (ansible_base/rest_filters/utils.py get_fields_from_path).