For GitHub pullrequest webhooks, geteventstatusapi() in awx/api/views/webhooks.py returns pullrequest.statusesurl from the request body verbatim without any host validation:
def geteventstatusapi(self): if self.geteventtype() != 'pullrequest': return return self.request.data.get('pullrequest', {}).get('statusesurl')
The value is persisted into job extravars and later used by updatewebhookstatus() in awx/main/models/mixins.py:
statusapi = self.extravarsdict.get('awxwebhookstatusapi') headers = {k: v.format(self.webhookcredential.getinput('token')), 'Content-Type': 'application/json'} response = requests.post(statusapi, data=json.dumps(data), headers=headers, timeout=30)
There is no host allowlist for the expected Git provider endpoint and no private-network egress filtering before the request is sent. The webhook receiver relies on an HMAC secret (webhookkey), which is readable by users with the admin role on the job template via /api/v2/jobtemplates/{id}/webhookkey/. This allows a template admin to forge a signed webhook payload with statusesurl pointing to an attacker-controlled endpoint, and AWX will POST status updates including the Git PAT in the Authorization header to that endpoint.
Controller registers LOGAGGREGATORHOST as a bare CharField with no character validation, and the logging settings validator only verifies that host/type are present. When any LOGAGGREGATOR setting is changed via PATCH /api/controller/v2/settings/logging/ (superuser-only), Controller regenerates /var/lib/awx/rsyslog/rsyslog.conf and restarts awx-rsyslogd. In the tcp/udp (omfwd) code path the host value is written raw as target="{host}", allowing an attacker to close the action() statement and append module(load="omprog") plus an omprog action() whose binary= runs an arbitrary shell command in the rsyslog control-plane component (uid=awx). That process can read SECRETKEY and the Postgres credentials, decrypt every stored Credential, and forge inter-service JWTs. Upstream: https://github.com/ansible/awx (devel) — UNFIXED (no PR) Affected file: awx/main/utils/externallogging.py (constructrsyslogconftemplate: :26 spoolDirectory, :100 errorfile, :128 target); awx/main/conf.py (:565-574, :958-979); awx/conf/views.py (:131-133)
A flaw was found in automation-controller (AWX). The AWXTASKENV setting is protected by a validator that rejects environment-variable keys which manipulate dynamic linkers or interpreters (LD, DYLD, PYTHON, PERL5OPT). The sibling GALAXYTASKENV setting (awx/main/conf.py) is registered as a bare KeyValueField with no such validator. Its value is passed verbatim into project-update extravars (awx/main/tasks/jobs.py RunProjectUpdate.buildextravarsfile -> 'galaxytaskenv') and injected into the environment of the ansible-galaxy command tasks in awx/playbooks/projectupdate.yml (environment: "{{ galaxytaskenv | combine(additionalgalaxyenv) }}"). A system administrator can set GALAXYTASKENV to include LDPRELOAD or PYTHONPATH referencing a file in a project checkout under the shared /var/lib/awx/projects volume, achieving arbitrary code execution inside the project-sync execution environment on the control-plane task pod on the next sync of any project with a requirements file. The container has read/write access to all organizations' checked-out project source and to injected ANSIBLEGALAXYSERVERTOKEN values, enabling cross- tenant tampering and token theft, and constitutes a bypass of the linker/ interpreter blocklist AWX enforces on AWXTASKENV. Note: in ansible/awx upstream (devel) neither AWXTASKENV nor GALAXYTASKENV currently carries the blocklist -- the AWXTASKENV control is a downstream patch; both must be present upstream to prevent regression. Discovered internally; verified live on AAP 2.7 / automation-controller 4.8.1. Upstream: github.com/ansible/awx (awx/main/conf.py GALAXYTASKENV; awx/main/tasks/jobs.py RunProjectUpdate.buildextravarsfile; awx/playbooks/projectupdate.yml)
A flaw was found in automation-controller (AWX). The external credential test endpoints execute the credential plugin backend in-process in the controller web service (awx/api/views/init.py CredentialExternalTest / CredentialTypeExternalTest -> callbackendwitherrorhandling -> plugin.backend()). CredentialExternalTest uses objpermissiontype='use' and merges request-body 'inputs' over the stored credential inputs, so a user with only the use role can override serverurl. The Thycotic Secret Server plugin (awx-plugins src/awxplugins/credentials/tss.py tssbackend) passes serverurl verbatim into the delinea/thycotic SDK, which issues GET {serverurl}/api/v1/ healthcheck and GET {serverurl}/health and, on a healthy response, POST {serverurl}/oauth2/token, with no allow-list, scheme, IP-range, or DNS-rebind check. Errors are wrapped as a bare SecretServerError (blind), but latency differs by target state (refused ~0.5s vs filtered ~12s until the gateway 503s), yielding a reliable timing oracle for scanning in-cluster services, the Kubernetes API, link-local metadata (169.254.169.254), and localhost daemons from the control-plane pod. This is the same root-cause class as the previously reported HashiCorp Vault / Centrify / CyberArk CCP credential-plugin SSRFs; Thycotic Secret Server is an additional sink. Discovered internally; verified live on AAP 2.6/2.7 / automation-controller 4.8.1; still present on devel (both awx and awx-plugins). Upstream: github.com/ansible/awx (awx/api/views/init.py CredentialExternalTest, CredentialTypeExternalTest, callbackendwitherrorhandling); github.com/ansible/awx-plugins (src/awxplugins/credentials/tss.py tssbackend)
A flaw was found in the AWX GitHub webhook integration. When a GitHub pullrequest webhook is received, GithubWebhookReceiver.geteventstatusapi() in awx/api/views/webhooks.py returns pullrequest.statusesurl from the request body without validation. The value is stored in job extra variables as awxwebhookstatusapi and later used by WebhookMixin.updatewebhookstatus() in awx/main/models/mixins.py as the callback URL when posting job status updates. If a job template is configured with webhookservice=github and a GitHub Personal Access Token credential as webhookcredential, the controller sends that token in the Authorization header when POSTing to the stored callback URL on job completion. Although the controller is designed to post commit status updates back to GitHub, it does not restrict the callback URL to trusted GitHub API endpoints. An attacker who can submit a forged webhook request with a valid HMAC-SHA1 signature for the target job template's webhookkey can supply an attacker-controlled statusesurl and cause exfiltration of the configured GitHub PAT when the triggered job completes. Normal GitHub webhook deliveries do not allow arbitrary contributors to control statusesurl; exploitation requires knowledge of the per-template webhook shared secret or privileged access to retrieve it from the controller. Upstream: https://github.com/ansible/awx Affected files: - awx/api/views/webhooks.py (GithubWebhookReceiver.geteventstatusapi) - awx/main/models/mixins.py (WebhookMixin.updatewebhookstatus)
A flaw was found in the AWX GitHub webhook integration. When a GitHub pullrequest webhook is received, GithubWebhookReceiver.geteventstatusapi() in awx/api/views/webhooks.py returns pullrequest.statusesurl from the request body without validation. The value is stored in job extra variables as awxwebhookstatusapi and later used by WebhookMixin.updatewebhookstatus() in awx/main/models/mixins.py as the callback URL when posting job status updates. If a job template is configured with webhookservice=github and a GitHub Personal Access Token credential as webhookcredential, the controller sends that token in the Authorization header when POSTing to the stored callback URL on job completion. Although the controller is designed to post commit status updates back to GitHub, it does not restrict the callback URL to trusted GitHub API endpoints. An attacker who can submit a forged webhook request with a valid HMAC-SHA1 signature for the target job template's webhookkey can supply an attacker-controlled statusesurl and cause exfiltration of the configured GitHub PAT when the triggered job completes. Normal GitHub webhook deliveries do not allow arbitrary contributors to control statusesurl; exploitation requires knowledge of the per-template webhook shared secret or privileged access to retrieve it from the controller. Upstream: https://github.com/ansible/awx Affected files: - awx/api/views/webhooks.py (GithubWebhookReceiver.geteventstatusapi) - awx/main/models/mixins.py (WebhookMixin.updatewebhookstatus)
A flaw was found in automation-controller (AWX). GET /api/controller/v2/hosts/ accepts a hostfilter query parameter that HostList.getqueryset (awx/api/views/ init.py:1938-1944) parses with SmartFilter.queryfromstring and intersects with the RBAC-filtered host queryset via qs &= filterqs; 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(). getfieldfromlookup(Host, k) and then executes Host.objects.filter({k: v}) directly, including regex and icontains lookups. FieldLookupBackend (django- ansible-base ansiblebase/restfilters/utils.py:9-69) rejects only fields in PASSWORDFIELDS/encryptedfields, fields marked preventsearch, and traversal loops -- it performs no authorization check on the rows the relation traversal reaches. JobEvent.eventdata/stdout and AdHocCommandEvent.eventdata/stdout (awx/main/models/events.py:215,253,695,705) are not wrapped in preventsearch(), unlike extravars, jobenv, jobargs and surveypasswords which are. Consequently a user with only the Read role on an inventory can submit hostfilter values such as lastjobjobeventseventdataregex=<pattern>, jobeventsasprimaryhoststdouticontains=<substring>, or adhoccommandeventseventdataregex=<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 eventdata/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 playbookuuid was extracted exactly); still present on devel. Upstream: github.com/ansible/awx (awx/api/views/init.py HostList.getqueryset; awx/main/utils/filters.py SmartFilter.BoolOperand; sensitive fields in awx/main/models/events.py) and github.com/ansible/django-ansible-base (ansiblebase/restfilters/utils.py getfieldsfrompath).
A flaw was found in automation-controller (AWX). OrganizationGalaxyCredentialsList (awx/api/views/organization.py) validates only that the attached credential's kind is galaxyapitoken, and OrganizationAccess.canattach (awx/main/access.py) has no branch for the galaxycredentials relationship, so it falls through to BaseAccess.canattach, which requires only canchange on the organization (org admin) and read access on the credential -- not userole. Consequently a user who administers any one organization and holds only read on a Galaxy/Hub credential in another organization (for example a system/platform auditor) can POST to /api/controller/v2/organizations/{id}/galaxycredentials/ to bind that foreign credential. RunProjectUpdate.buildenv (awx/main/tasks/jobs.py) then iterates organization.galaxycredentials, decrypts each token, and exports it as ANSIBLEGALAXYSERVERSERVERnTOKEN so ansible-galaxy authenticates to the credential owner's Hub URL. The token is masked in the API jobenv, but it is used server-side on the attacker's behalf against the victim's Hub -- a read-only role gains cross-tenant use of another org's secret. Discovered internally; verified live on AAP 2.7 / automation-controller 4.8.1 (a platform auditor attached a foreign org's credential (204) and a subsequent project sync exported the victim Hub URL + masked token); still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py OrganizationAccess. canattach / BaseAccess.canattach; awx/api/views/organization.py OrganizationGalaxyCredentialsList; awx/main/tasks/jobs.py RunProjectUpdate.buildenv)
A flaw was found in automation-controller (AWX). POST/PATCH /api/controller/v2/credentialtypes/ validates the injectors JSON by RENDERING every string under injectors.{env,file,extravars} through a Jinja2 sandbox synchronously in the uWSGI web request worker (awx/main/fields.py:748-785, CredentialTypeInjectorField.validate -> validatetemplatestring: sandbox.ImmutableSandboxedEnvironment(undefined=StrictUndefined).fromstring(tmpl).render(...) at line 750). The sandbox blocks server-side-template-injection code-execution gadgets (class, globals, |attr('...'), str.format dunder walk — all verified rejected live and consistent with ImmutableSandboxedEnvironment), so this is not remote code execution and has no confidentiality or integrity impact. However, nothing bounds total iteration count, output size, or wall-clock time: nested {% for %} loops multiply freely and string multiplication is unrestricted, so a single request can render billions of iterations in-process. Live timing on the target scaled linearly (90,000 iterations 0.62s; 18,000,000 iterations 1.22s; 72,000,000 iterations 3.71s), proving the render runs synchronously in automation-controller-web; a nested-loop request of roughly 8.1e9 iterations exceeds the uWSGI harakiri timeout and kills the worker, and a number of concurrent requests at or above the worker count drives every web worker into a kill/respawn loop, taking the Controller API and UI (job launch, callbacks, inventory sync, RBAC, UI) offline for as long as the attack is sustained. Separately, the validation try/except catches only UndefinedError, SecurityError, and TemplateSyntaxError (fields.py:751/757/759); a loader-less {% extends %} or {% include %} raises TypeError('no loader for this environment specified'), and large string operations raise OverflowError — neither is caught, so the exception propagates as a raw Django 500 error page (verified live), a robustness defect and a second confirmation that attacker templates execute in the web process. The endpoint is restricted to superusers (awx/main/access.py:1078-1098 CredentialTypeAccess with no canadd override -> BaseAccess.canadd returns user.issuperuser), so this is a defense-in-depth / availability hardening issue rather than a privilege boundary break. There is no wall-clock, iteration, or output bound anywhere around the render on the devel branch (no MAXRANGE or timeout present). This is the automation-controller sibling of the already-reported EDA credential-types injector-validation denial of service and the Controller notificationtemplates message-validation denial of service, in a distinct code path (awx.main.fields.CredentialTypeInjectorField) reached from a distinct endpoint. Discovered internally (pentest-aap); verified live on AAP 2.7 / automation-controller 4.8.1; still present on devel (fix prepared, not merged). Upstream: github.com/ansible/awx (awx/main/fields.py CredentialTypeInjectorField.validate / validatetemplatestring).
A flaw was found in automation-controller (AWX). Project.scmbranch (awx/main/models/credential path awx/main/models/projects.py:98-111, a CharField with no validators) and scmrefspec are accepted as free text with no rejection of a leading dash (no validatescmbranch/validatescmrefspec and no leading-dash guard anywhere in awx/api/serializers.py, awx/main, or awx/playbooks). During project sync, RunProjectUpdate.buildextravarsfile (awx/main/tasks/jobs.py:1481-1518) copies scmbranch (:1486/:1508) and scmrefspec (:1517-1518) verbatim into the project-update extravars, and awx/playbooks/projectupdate.yml (- hosts: localhost, connection: local, :28-30) passes them to the ansible.builtin.git module as version: "{{ scmbranch | quote }}" (:50) and refspec: "{{ scmrefspec | default(omit) }}" (:51). The git module's switchversion() builds [/usr/bin/git, checkout, --force, <version>] with no -- end-of-options sentinel (upstream lib/ansible/modules/git.py), so a value beginning with -- is parsed by git as an option flag. The | quote filter is ansible's shlex.quote and does NOT defend against this: an all-shell-safe string such as --pathspec-from-file=/etc/passwd is returned unchanged (matching the live proof, whose command line shows no quotes). Setting scmbranch to --pathspec-from-file=/var/run/secrets/kubernetes.io/serviceaccount/token makes git read the file line-by-line as pathspecs and echo each line on stderr as error: pathspec '<line>' did not match any file(s) known to git; ansible-runner captures stderr into the job event stream, returned via GET /api/controller/v2/projectupdates/{id}/stdout/?format=txt — an arbitrary-file-read oracle on the sync host. Because projectupdate.yml runs on localhost and, on the tested OpenShift Operator deployment, that host is the automation-controller-task control-plane pod (confirmed: the leaked token's pod name matches a control node in /api/controller/v2/ping/), the read reaches control-plane secrets: the system:serviceaccount:ansible-automation-platform:automation-controller JWT (pod/secret CRUD in the AAP namespace -> full platform takeover), /etc/tower/SECRETKEY (offline decryption of every stored Credential), and /etc/tower/conf.d/.py (PostgreSQL credentials). Any user able to create or edit a project (Organization projectadmin, or Project Admin on a single project) can trigger this on project create/edit auto-sync; system administrator is not required, and any org's admin reads secrets governing all tenants on the control plane. scmrefspec (fed to git fetch, no quote at all) is a second option-injection sink to fix together. On deployments where project updates run in an isolated execution environment without control-plane secrets, the primitive is limited to that context. Discovered internally (pentest-aap); verified live on AAP 2.7 / automation-controller 4.8.1; still present on devel (fix prepared, not merged) Upstream: github.com/ansible/awx (awx/main/models/projects.py scmbranch/scmrefspec; awx/api/serializers.py Project serializers; awx/main/tasks/jobs.py RunProjectUpdate.buildextravarsfile; awx/playbooks/projectupdate.yml) and github.com/ansible/ansible (ansible.builtin.git switchversion -- add --).