Where
-Infinity
0
Severity
4

Verbatim report from Maksym Mazura via secalert. Impacted project ansible-core, verified on 2.21.5; exception listed in PRESERVE in lib/ansible/internal/task.py on v2.21.6rc1, v2.22.0rc2 and devel; not reproducible on 2.16.3. Three leak vectors: (1) module arg-validation value echoed, (2) templating Origin snippet of the vars file (failing line + two preceding lines), (3) full fail/assert msg; also under -vvv. INLINE reproducer provided: sec.yml (secreta !unsafe "FAKEUNSAFE111", secretb "FAKEPLAIN222 {{ nosuchvar }}"); pb.yml (command echo {{secretb}}, waitfor port {{secreta}}, fail msg {{secreta}} - all nolog:true ignoreerrors:true); run: ansible-playbook -i localhost, -c local -e @sec.yml pb.yml. Output on 2.21.5 shows FAKEUNSAFE111 and the secretb line (plus preceding secreta line) next to the "censored" notice. Suggested fix: do not preserve exception (or replace its message) when the result is censored, as 2.16 did; or have CallbackBase.handleexception print fixed text for censored results. NOTE: no reproducer file/attachment was attached to the ticket - only the inline steps above; Red Hat has not yet independently reproduced. Disclosure: not public, no bounty requested.

First published (updated )
Severity
4

Rate Moderate - exploitation requires an authenticated user with the collection-namespace owner role to store the payload (roleless upload is rejected), and a separate authenticated viewer must actively activate the crafted link (no execution on page load); the build CSP does not block the script-executing scheme. Component mapping confirmed via newcli: Platform UI assets ship in automation-gateway (2.5 RPM), automation-platform-ui (2.6 RPM), and the gateway-rhel9 container (2.6 and 2.7); standalone ansible-hub-ui uses a different, non-reproduced renderer. Source flaw component = upstream ansible-ui (frontend/hub).

First published (updated )
Severity
8.8
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

A flaw was found in ansible-core. When installing a collection with ansible-galaxy collection install, the archive extractor validates member paths using lexical path normalisation (os.path.abspath) instead of resolving symbolic links (os.path.realpath), and it performs no containment check on symlink-typed directory members before creating them. A crafted collection tarball can chain symlink directory entries so that a subsequent file member is written outside the intended destination directory. This allows an attacker who can get a victim to install a malicious collection to overwrite arbitrary files with the privileges of the user running ansible-galaxy, leading to code execution on the control node. This is a bypass of the fix for CVE-2020-10691.

1 / 3
Source: MITRE
First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

A DOM-based Cross-Site Scripting (XSS) vulnerability exists in the Ansible Platform UI due to unvalidated input handling within the application's redirect route. Specifically, the application extracts a target destination from the next query parameter and directly assigns it to the browser's location.href without verifying its format or scheme. The platform includes built-in URL validation functions designed to block malicious URI schemes (such as javascript: and data:) as well as off-site or protocol-relative redirects, this specific route bypasses those controls. Consequently, an attacker can craft a malicious link that, when accessed by an authenticated user, causes arbitrary JavaScript to execute within the context of the user's session.

1 / 2
Source: MITRE
First published (updated )
Severity
4
XSS

DOM-based cross-site scripting (XSS) vulnerability in the Ansible Platform UI's /redirect route (project: ansible/ansible-ui, affected component: platform/main/Redirect.tsx). The vulnerable Redirect component and /redirect route were introduced in commit df1902cc on 2024-04-23, and the upstream devel branch still pointed to the confirmed UI commit as of 2026-10-06. The root cause is that the next query parameter in Redirect.tsx flows directly to location.href without validation, even though the UI already provides a validateUrlPath helper (in frontend/common/AnsibleLogin/validateUrlPath.ts) that rejects javascript:, data:, absolute, and protocol-relative URLs — this helper is used by the login component but is not applied to the Redirect component, which is registered in usePlatformNavigation.tsx and exposed to authenticated users behind PlatformLogin after a valid Gateway session.

First published (updated )
Severity
4
Path Traversal

Reported via the ansible-security list by Liqiang Ji (ISCAS / SQUARE Research Group).

Module: src/ansiblerunner/utils/streaming.py, function unstreamdir().

unstreamdir() performs a custom ("fancy") extraction to preserve permissions and symlinks. Two defects: 1. CWE-59 (link following): a symlink member's target is read verbatim from the archive content and re-created with os.symlink() without validation, so it can point outside targetdirectory (e.g. '../outside' or an absolute path). A subsequent member extracted through that symlink lands outside targetdirectory (arbitrary write escape). 2. CWE-22 (path traversal): outpath = os.path.join(targetdirectory, info.filename) is built from the unsanitized member name. ZipFile.extract() sanitizes the arcname, but os.utime()/os.chmod() operate on the raw outpath, so a member named '../victim' changes mtime/permissions on a file outside targetdirectory.

Exploitability: in the standard AWX/AAP topology the transmit stream is produced by the trusted controller (streamdir only encodes pre-existing symlinks) and the controller->executor path is authenticated, so there this is defense-in-depth hardening. It is directly relevant to deployments that feed untrusted input into ansible-runner's Worker() path.

Upstream: https://github.com/ansible/ansible-runner Fix PR: https://github.com/ansible/ansible-runner/pull/1550 Confirmed present in 2.4.3 and devel.

First published (updated )
Severity
7

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)

First published (updated )
Severity
6.4
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N

A flaw was found in the Ansible Automation Platform automation-controller notification subsystem. CustomEmailBackend (awx/main/notifications/emailbackend.py:24) is a thin subclass of django.core.mail.backends.smtp.EmailBackend and does not override open()/sendmessages() to validate its connect target, so the user-controlled notificationconfiguration.host and .port reach smtplib.SMTP(host, port, timeout=...) with no allow-list and no private/loopback/ link-local/reserved rejection. An organization-scoped Notification Admin (awx.notificationadminrole, checked by NotificationTemplateAccess.canadd/canchange at awx/main/access.py:2578/2582 — a delegatable, non-superuser role) can create or PATCH an email notification template with host/port pointing at any internal address and POST the template's /test/ endpoint. The dispatcher runs the backend on the controller-task pod, which opens a raw TCP connection to the attacker-chosen host:port. The smtplib exception string is persisted verbatim into Notification.error (a plain TextField, awx/main/models/notifications.py:219) and is readable over the API, producing a three-state oracle: "[Errno 111] Connection refused" (closed), "timed out" (filtered), and "Connection unexpectedly closed: timed out" or an SMTPResponseException (open). Because the transport is a raw socket, the flaw reaches non-HTTP internal services (for example Redis, PostgreSQL, receptor) and the in-cluster Kubernetes API, and can send SMTP-protocol bytes into those listeners. In addition, the SMTP AUTH exchange transmits the template's stored, otherwise write-only password to the configured host, so a user who can change the host on a shared organization template can exfiltrate its stored SMTP password. The HTTP-based notification backends (webhook, grafana, mattermost, rocketchat) were hardened against this on the 2.7 branch via awx/main/notifications/urlvalidation.py::validateurl(); the email backend (and the IRC backend) were omitted from that hardening. That guard is present only on the 2.7 branch; on the 2.5, 2.6, and development branches it is absent and no notification backend validates its connect target.

Upstream: https://github.com/ansible/tower (awx) Affected file: awx/main/notifications/emailbackend.py:24 (CustomEmailBackend, no host validation); awx/main/models/notifications.py:219 (Notification.error oracle sink); awx/main/access.py:2559-2582 (notificationadminrole). Guard (2.7 only): awx/main/notifications/urlvalidation.py::validateurl (added by tower #7875).

1 / 2
Source: Red Hat
First published (updated )
Severity
4

A flaw was found in the Ansible Automation Platform automation-controller notification subsystem. CustomEmailBackend (awx/main/notifications/emailbackend.py:24) is a thin subclass of django.core.mail.backends.smtp.EmailBackend and does not override open()/sendmessages() to validate its connect target, so the user-controlled notificationconfiguration.host and .port reach smtplib.SMTP(host, port, timeout=...) with no allow-list and no private/loopback/ link-local/reserved rejection. An organization-scoped Notification Admin (awx.notificationadminrole, checked by NotificationTemplateAccess.canadd/canchange at awx/main/access.py:2578/2582 — a delegatable, non-superuser role) can create or PATCH an email notification template with host/port pointing at any internal address and POST the template's /test/ endpoint. The dispatcher runs the backend on the controller-task pod, which opens a raw TCP connection to the attacker-chosen host:port. The smtplib exception string is persisted verbatim into Notification.error (a plain TextField, awx/main/models/notifications.py:219) and is readable over the API, producing a three-state oracle: "[Errno 111] Connection refused" (closed), "timed out" (filtered), and "Connection unexpectedly closed: timed out" or an SMTPResponseException (open). Because the transport is a raw socket, the flaw reaches non-HTTP internal services (for example Redis, PostgreSQL, receptor) and the in-cluster Kubernetes API, and can send SMTP-protocol bytes into those listeners. In addition, the SMTP AUTH exchange transmits the template's stored, otherwise write-only password to the configured host, so a user who can change the host on a shared organization template can exfiltrate its stored SMTP password. The HTTP-based notification backends (webhook, grafana, mattermost, rocketchat) were hardened against this on the 2.7 branch via awx/main/notifications/urlvalidation.py::validateurl(); the email backend (and the IRC backend) were omitted from that hardening. That guard is present only on the 2.7 branch; on the 2.5, 2.6, and development branches it is absent and no notification backend validates its connect target.

Upstream: https://github.com/ansible/tower (awx) Affected file: awx/main/notifications/emailbackend.py:24 (CustomEmailBackend, no host validation); awx/main/models/notifications.py:219 (Notification.error oracle sink); awx/main/access.py:2559-2582 (notificationadminrole). Guard (2.7 only): awx/main/notifications/urlvalidation.py::validateurl (added by tower #7875).

First published (updated )
Severity
4.3
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

A flaw was found in the Ansible Automation Platform automation-controller request handling. The shipped production settings define REMOTEHOSTHEADERS=['HTTPXFORWARDEDFOR'] and PROXYIPALLOWEDLIST=[] (empty). In APIView.initializerequest (awx/api/generics.py:199-201), client-supplied REMOTEHOSTHEADERS are stripped only when PROXYIPALLOWEDLIST is non-empty and the request did not arrive through a listed proxy; with the empty shipped allow-list this guard is skipped and the X-Forwarded-For header is never stripped. When the client IP is resolved via getremotehost()/getremotehosts() (django-ansible-base ansiblebase/lib/utils/requests.py), the function reads REMOTEHOSTHEADERS in order and returns the first (leftmost) X-Forwarded-For entry. Because the deployment fronts Controller with Envoy, which by default appends (rather than replaces) the real client IP to the incoming X-Forwarded-For header, an attacker-supplied leftmost value survives to the application and is chosen as the client IP. That spoofed value is written into the Controller's audit/access logging, including the login audit message ("User <username> logged in from <ip>") and the API 4xx error log's remoteaddr field (API400ERRORLOGFORMAT). An attacker can therefore forge the source IP attributed to API actions in Controller logs and any downstream SIEM keyed on client IP. Unauthenticated failed-login and error log entries can likewise be attributed to arbitrary IPs. A secure mechanism already exists but is not enabled by default: when a valid HTTPXTRUSTEDPROXY shared-secret header is present, getremotehosts prefers the trusted last/rightmost X-Forwarded-For entry (the value Envoy appends) and honors x-envoy-external-address. The flaw affects only audit/forensic integrity and does not grant additional access.

Upstream: https://github.com/ansible/tower (awx) and https://github.com/ansible/django-ansible-base (getremotehost[s]) Affected file: awx/api/generics.py:199-201 (guard), :119 and :246-265 (sinks); awx/settings/productiondefaults.py:23; awx/settings/defaults.py:166; ansiblebase/lib/utils/requests.py:14-16,38-42,65-66

1 / 2
Source: Red Hat
First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

A flaw was found in the Ansible Automation Platform automation-controller webhook receivers. The Bitbucket Data Center webhook receiver at POST /api/controller/v2/{jobtemplates,workflowjobtemplates}/<pk>/bitbucketdc/ is intentionally unauthenticated (permissionclasses=(AllowAny,), authenticationclasses=()) and authorizes requests by verifying an HMAC over the request body keyed by the per-template webhookkey. BitbucketDcWebhookReceiver.mustchecksignature() returns False when the X-Event-Key request header is 'diagnostics:ping', because Bitbucket does not sign ping requests. This carve-out is evaluated only after the receiver has already resolved the target template: getobject() filters templates to those with webhookservice='bitbucketdc' and a non-empty webhookkey and raises PermissionDenied (HTTP 403) when no template matches, whereas a matching template proceeds past the skipped signature check to the ping short-circuit and returns HTTP 200 with the body "Webhook ignored". As a result, an unauthenticated remote attacker who sends a diagnostics:ping request to each template ID observes a 200-vs-403 response discrepancy that reveals exactly which Job Template and Workflow Job Template IDs have Bitbucket DC webhooks configured, with no credentials and no knowledge of the webhookkey. This is unauthenticated reconnaissance: it confirms valid template IDs, reveals which automation is wired to Bitbucket Data Center, and lets an attacker focus subsequent webhookkey brute-force or SCM-side spoofing attempts on the small set of templates that would actually accept signed payloads. The ping path performs no action and no secret values are disclosed.

Upstream: https://github.com/ansible/tower (awx) Affected file: awx/api/views/webhooks.py:306-308 (BitbucketDcWebhookReceiver.mustchecksignature), with webhooks.py:69-78 (getobject) and :144-146 (post ping short-circuit)

1 / 2
Source: Red Hat
First published (updated )
Severity
4

A flaw was found in the Ansible Automation Platform automation-controller webhook receivers. The Bitbucket Data Center webhook receiver at POST /api/controller/v2/{jobtemplates,workflowjobtemplates}/<pk>/bitbucketdc/ is intentionally unauthenticated (permissionclasses=(AllowAny,), authenticationclasses=()) and authorizes requests by verifying an HMAC over the request body keyed by the per-template webhookkey. BitbucketDcWebhookReceiver.mustchecksignature() returns False when the X-Event-Key request header is 'diagnostics:ping', because Bitbucket does not sign ping requests. This carve-out is evaluated only after the receiver has already resolved the target template: getobject() filters templates to those with webhookservice='bitbucketdc' and a non-empty webhookkey and raises PermissionDenied (HTTP 403) when no template matches, whereas a matching template proceeds past the skipped signature check to the ping short-circuit and returns HTTP 200 with the body "Webhook ignored". As a result, an unauthenticated remote attacker who sends a diagnostics:ping request to each template ID observes a 200-vs-403 response discrepancy that reveals exactly which Job Template and Workflow Job Template IDs have Bitbucket DC webhooks configured, with no credentials and no knowledge of the webhookkey. This is unauthenticated reconnaissance: it confirms valid template IDs, reveals which automation is wired to Bitbucket Data Center, and lets an attacker focus subsequent webhookkey brute-force or SCM-side spoofing attempts on the small set of templates that would actually accept signed payloads. The ping path performs no action and no secret values are disclosed.

Upstream: https://github.com/ansible/tower (awx) Affected file: awx/api/views/webhooks.py:306-308 (BitbucketDcWebhookReceiver.mustchecksignature), with webhooks.py:69-78 (getobject) and :144-146 (post ping short-circuit)

First published (updated )

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 --).

First published (updated )

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 --).

First published (updated )
Severity
4

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).

First published (updated )
Severity
4

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).

First published (updated )
Severity
4

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).

First published (updated )
Severity
4

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).

First published (updated )
Severity
7.6
AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:H/A:N

A flaw was found in Ansible Automation Platform's automation-controller. Custom Credential Types let an author define injectors.env (environment variables set in the execution environment when a credential of that type is attached to a job) and injectors.file (files rendered into the execution environment whose path is exposed to other injectors as {{ tower.filename }}). The env-variable names are validated only by a deny-list: CredentialTypeInjectorField.validateenvvarallowed (awx/main/fields.py:689-701) rejects names beginning with "ANSIBLE" and names present in the ENVBLOCKLIST frozenset (awx/main/constants.py:48-70). The stated purpose of this control is to stop injectors from hijacking the runner process (it blocks PATH, PYTHONPATH, VIRTUALENV and all ANSIBLE configuration variables). The deny-list is incomplete: it does not include BASHENV, ENV, LDPRELOAD, LDLIBRARYPATH, LDAUDIT, PYTHONSTARTUP, PYTHONWARNINGS, GITSSHCOMMAND, PERL5OPT, or similar loader/ process-hijacking variables, so those names pass validation. An attacker can therefore define a credential type whose file injector writes a shell script and whose env injector sets BASHENV to {{ tower.filename }}. Every non-interactive bash process spawned during a job (Ansible modules shell out constantly) then sources and executes the attacker's script, yielding arbitrary code execution inside the execution-environment container — independent of the playbook content — for any job that attaches a credential of the malicious type, with access to the secrets of all co-attached credentials in the same job environment and to any inventory host the job can reach. The same weak check is repeated at the runtime injection sink (awxplugins.interfaces temporaryprivateinjectapi.py, which re-checks only ENVBLOCKLIST), so the fix must be applied in both places. Creating a custom credential type requires Controller superuser (CredentialTypeAccess inherits BaseAccess), so this is primarily a defense-in-depth bypass of a control whose explicit purpose is to constrain exactly this behavior; however, an organization-level Credential Admin (not a superuser) can weaponize an already-existing malicious custom type, and in Gateway-managed AAP 2.5+ the platform-admin role is explicitly not host/EE root, so code execution in the execution environment via a configuration API crosses a real trust boundary.

Upstream: https://github.com/ansible/tower (private) / awxplugins.interfaces Affected files: awx/main/fields.py:689-701; awx/main/constants.py:48-70; awxplugins/interfaces/temporaryprivateinjectapi.py:263-288

1 / 2
Source: Red Hat
First published (updated )
Severity
7

A flaw was found in Ansible Automation Platform's automation-controller. Custom Credential Types let an author define injectors.env (environment variables set in the execution environment when a credential of that type is attached to a job) and injectors.file (files rendered into the execution environment whose path is exposed to other injectors as {{ tower.filename }}). The env-variable names are validated only by a deny-list: CredentialTypeInjectorField.validateenvvarallowed (awx/main/fields.py:689-701) rejects names beginning with "ANSIBLE" and names present in the ENVBLOCKLIST frozenset (awx/main/constants.py:48-70). The stated purpose of this control is to stop injectors from hijacking the runner process (it blocks PATH, PYTHONPATH, VIRTUALENV and all ANSIBLE configuration variables). The deny-list is incomplete: it does not include BASHENV, ENV, LDPRELOAD, LDLIBRARYPATH, LDAUDIT, PYTHONSTARTUP, PYTHONWARNINGS, GITSSHCOMMAND, PERL5OPT, or similar loader/ process-hijacking variables, so those names pass validation. An attacker can therefore define a credential type whose file injector writes a shell script and whose env injector sets BASHENV to {{ tower.filename }}. Every non-interactive bash process spawned during a job (Ansible modules shell out constantly) then sources and executes the attacker's script, yielding arbitrary code execution inside the execution-environment container — independent of the playbook content — for any job that attaches a credential of the malicious type, with access to the secrets of all co-attached credentials in the same job environment and to any inventory host the job can reach. The same weak check is repeated at the runtime injection sink (awxplugins.interfaces temporaryprivateinjectapi.py, which re-checks only ENVBLOCKLIST), so the fix must be applied in both places. Creating a custom credential type requires Controller superuser (CredentialTypeAccess inherits BaseAccess), so this is primarily a defense-in-depth bypass of a control whose explicit purpose is to constrain exactly this behavior; however, an organization-level Credential Admin (not a superuser) can weaponize an already-existing malicious custom type, and in Gateway-managed AAP 2.5+ the platform-admin role is explicitly not host/EE root, so code execution in the execution environment via a configuration API crosses a real trust boundary.

Upstream: https://github.com/ansible/tower (private) / awxplugins.interfaces Affected files: awx/main/fields.py:689-701; awx/main/constants.py:48-70; awxplugins/interfaces/temporaryprivateinjectapi.py:263-288

First published (updated )
Severity
4

A flaw was found in automation-controller (AWX). Binding a Credential to a resource is a use-level action because the credential's secrets are decrypted and consumed at run time; the product enforces userole on credential foreign keys throughout (ProjectAccess, CredentialInputSourceAccess, InstanceGroupAccess, job- launch and workflow-node paths). However ExecutionEnvironmentAccess.canadd and canchange (awx/main/access.py:1316-1338) validate only the organization FK (via executionenvironmentadminrole) and never call checkrelated on the credential FK, and ExecutionEnvironmentSerializer.validatecredential (serializers.py: 1327-1330) only enforces kind == 'registry' with no RBAC. Consequently a non- superuser holding Organization ExecutionEnvironment Admin (plus Member) in one organization can POST or PATCH /api/controller/v2/executionenvironments/ with a credential id referencing any Container-Registry credential in any other organization, including one for which direct GET returns 403. When a job runs with that execution environment, awx/main/tasks/jobs.py:320-327 calls cred.getinput('password'), decrypting the foreign credential's plaintext registry password into containerauthdata (podman authfile); for container-group tasks awx/main/tasks/receptor.py builds a Kubernetes imagePullSecret from the same plaintext. This crosses the tenant boundary and discloses another organization's registry credentials. A related instance affects Project.signaturevalidation credential, a second credential foreign key ProjectAccess does not use-check. Discovered internally (vanilla-harness); verified live on AAP 2.7 / automation- controller 4.8.1; still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py: ExecutionEnvironmentAccess.canadd / canchange; contrast ProjectAccess; sink awx/main/tasks/jobs.py containerauthdata).

First published (updated )
Severity
7

A flaw was found in automation-controller (AWX). In WorkflowJobTemplateNodeAccess (awx/main/access.py), canchange() delegates the execute-permission check on a node's unifiedjobtemplate to ujtexecute(), which returns True unconditionally when the node's current obj.unifiedjobtemplate is None, without ever validating the incoming data['unifiedjobtemplate'] against executerole. canadd() does not mark unifiedjobtemplate as mandatory, so a node can be created with unifiedjobtemplate=null (HTTP 201). PATCHing that empty node to any UnifiedJobTemplate id then succeeds (HTTP 200) with no executerole check on the new value. The direct-create path (POST a node with unifiedjobtemplate set) is correctly denied (403) via canadd -> checkrelated(rolefield= 'executerole'); the create-empty-then-PATCH sequence bypasses that guard. Consequently a user with adminrole on any single WorkflowJobTemplate (an organization admin, an Organization Workflow Admin, or an object-level WFJT admin) can bind and execute any Job Template, Project update, Inventory Source sync, System Job Template, or nested Workflow Job Template belonging to any other organization/tenant -- even ones for which direct GET/launch returns 403 -- running the victim template with the victim's attached credentials, inventory and project. The PATCH response summaryfields additionally leaks the victim template's name and description. Discovered internally (pentest-aap); verified live on AAP 2.7 / automation-controller 4.8.x (victim WFJT executed end-to-end, launchedby attacker); still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py: WorkflowJobTemplateNodeAccess.ujtexecute / canadd / canchange; checkrelated both-None handling).

First published (updated )
Severity
7

A flaw was found in automation-controller (AWX). In WorkflowJobTemplateNodeAccess (awx/main/access.py), canchange() delegates the execute-permission check on a node's unifiedjobtemplate to ujtexecute(), which returns True unconditionally when the node's current obj.unifiedjobtemplate is None, without ever validating the incoming data['unifiedjobtemplate'] against executerole. canadd() does not mark unifiedjobtemplate as mandatory, so a node can be created with unifiedjobtemplate=null (HTTP 201). PATCHing that empty node to any UnifiedJobTemplate id then succeeds (HTTP 200) with no executerole check on the new value. The direct-create path (POST a node with unifiedjobtemplate set) is correctly denied (403) via canadd -> checkrelated(rolefield= 'executerole'); the create-empty-then-PATCH sequence bypasses that guard. Consequently a user with adminrole on any single WorkflowJobTemplate (an organization admin, an Organization Workflow Admin, or an object-level WFJT admin) can bind and execute any Job Template, Project update, Inventory Source sync, System Job Template, or nested Workflow Job Template belonging to any other organization/tenant -- even ones for which direct GET/launch returns 403 -- running the victim template with the victim's attached credentials, inventory and project. The PATCH response summaryfields additionally leaks the victim template's name and description. Discovered internally (pentest-aap); verified live on AAP 2.7 / automation-controller 4.8.x (victim WFJT executed end-to-end, launchedby attacker); still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py: WorkflowJobTemplateNodeAccess.ujtexecute / canadd / canchange; checkrelated both-None handling).

First published (updated )
Severity
7

A flaw was found in automation-controller (AWX). Four compounding issues let a holder of object-level NotificationTemplate admin recover the plaintext of notification credentials for fixed-endpoint backends: (1) NotificationTemplate.displaynotificationconfiguration() (awx/main/models/ notifications.py) masks only password subfields of the CURRENT notification type, so after a type change the previous type's password subfield is returned with its full Fernet ciphertext; (2) NotificationTemplateSerializer.validate() (awx/api/ serializers.py) returns early when notificationconfiguration is absent from the request body, allowing a type-only change that keeps the stale ciphertext; (3) NotificationTemplate.save() skips re-encryption of any value beginning with "$encrypted$" and the serializer's forward sentinel tests exact equality with "$encrypted$", so a full ciphertext replanted into a different password subfield is stored verbatim; (4) getencryptionkey() (awx/main/utils/encryption.py) derives the Fernet key from the secret key, primary key, and field name only -- not the subfield -- so a ciphertext produced for one subfield decrypts as any other subfield of the same object. Chaining these, an attacker switches the template type to leak the ciphertext, replants it into a webhook password with an attacker-controlled URL, and triggers /test/; the controller decrypts and sends the plaintext to the attacker in an HTTP Basic auth header. Verified live on AAP 2.7 / automation-controller 4.8.1; still present on devel. Upstream: github.com/ansible/awx (awx/main/models/notifications.py; awx/api/serializers.py; awx/main/utils/encryption.py; awx/main/notifications/webhookbackend.py)

First published (updated )
Severity
7

A flaw was found in automation-controller (AWX). Four compounding issues let a holder of object-level NotificationTemplate admin recover the plaintext of notification credentials for fixed-endpoint backends: (1) NotificationTemplate.displaynotificationconfiguration() (awx/main/models/ notifications.py) masks only password subfields of the CURRENT notification type, so after a type change the previous type's password subfield is returned with its full Fernet ciphertext; (2) NotificationTemplateSerializer.validate() (awx/api/ serializers.py) returns early when notificationconfiguration is absent from the request body, allowing a type-only change that keeps the stale ciphertext; (3) NotificationTemplate.save() skips re-encryption of any value beginning with "$encrypted$" and the serializer's forward sentinel tests exact equality with "$encrypted$", so a full ciphertext replanted into a different password subfield is stored verbatim; (4) getencryptionkey() (awx/main/utils/encryption.py) derives the Fernet key from the secret key, primary key, and field name only -- not the subfield -- so a ciphertext produced for one subfield decrypts as any other subfield of the same object. Chaining these, an attacker switches the template type to leak the ciphertext, replants it into a webhook password with an attacker-controlled URL, and triggers /test/; the controller decrypts and sends the plaintext to the attacker in an HTTP Basic auth header. Verified live on AAP 2.7 / automation-controller 4.8.1; still present on devel. Upstream: github.com/ansible/awx (awx/main/models/notifications.py; awx/api/serializers.py; awx/main/utils/encryption.py; awx/main/notifications/webhookbackend.py)

First published (updated )

A flaw was found in automation-controller (AWX). InventoryInputInventoriesList (awx/api/views/inventory.py) validates only that the attached inventory is not itself constructed, and InventoryAccess.canattach (awx/main/access.py) special- cases only the instancegroups relationship (requiring userole); the inputinventories relationship falls through to BaseAccess.canattach, which requires only canchange on the constructed inventory and read access on the source inventory -- not userole. Consequently a user who administers a constructed inventory in one organization and holds only Inventory Read on an inventory in another organization (for example an organization auditor, or a system auditor with any inventory-admin org) can POST to /api/controller/v2/inventories/{constructedid}/inputinventories/ to attach the foreign inventory; the constructed inventory sync then clones every host and host variable (including secrets) into the attacker's inventory, and the attacker -- as admin of the constructed inventory -- can launch ad hoc shell commands against those hosts or bind the inventory to a job template. Discovered internally; verified live on AAP 2.7 / automation-controller 4.8.1 (a read-only user attached a foreign inventory (204), cloned a secret host variable, and had an ad hoc shell command accepted and dispatched against the foreign host); still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py InventoryAccess. canattach / BaseAccess.canattach; awx/api/views/inventory.py InventoryInputInventoriesList)

First published (updated )

A flaw was found in automation-controller (AWX). InventoryInputInventoriesList (awx/api/views/inventory.py) validates only that the attached inventory is not itself constructed, and InventoryAccess.canattach (awx/main/access.py) special- cases only the instancegroups relationship (requiring userole); the inputinventories relationship falls through to BaseAccess.canattach, which requires only canchange on the constructed inventory and read access on the source inventory -- not userole. Consequently a user who administers a constructed inventory in one organization and holds only Inventory Read on an inventory in another organization (for example an organization auditor, or a system auditor with any inventory-admin org) can POST to /api/controller/v2/inventories/{constructedid}/inputinventories/ to attach the foreign inventory; the constructed inventory sync then clones every host and host variable (including secrets) into the attacker's inventory, and the attacker -- as admin of the constructed inventory -- can launch ad hoc shell commands against those hosts or bind the inventory to a job template. Discovered internally; verified live on AAP 2.7 / automation-controller 4.8.1 (a read-only user attached a foreign inventory (204), cloned a secret host variable, and had an ad hoc shell command accepted and dispatched against the foreign host); still present on devel. Upstream: github.com/ansible/awx (awx/main/access.py InventoryAccess. canattach / BaseAccess.canattach; awx/api/views/inventory.py InventoryInputInventoriesList)

First published (updated )
Severity
4

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)

First published (updated )
Severity
4

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)

First published (updated )
Severity
7

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)

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203