Jinja2 / Lookup RCE
Detects remote code execution and information disclosure via Jinja2 lookups, !unsafe tags, and unsanitised template evaluation in when/set_fact/assert clauses.
10 rules in jinja_lookup_rce.yml
CRITICAL: 2 | HIGH: 6 | MEDIUM: 2
| Rule ID | Severity | Title | Description | Refs |
|---|---|---|---|---|
jinja_ | CRITICAL | Jinja2 {% … %} Statement Block Invoking lookup/pipe/url | A {% %} statement inside a playbook value can call lookup() directly, bypassing the usual {{ }} context-awareness and letting attackers smuggle command execution through what looks like control flow. | |
lookup_ | CRITICAL | lookup(‘pipe’, …) - Arbitrary Command Execution on Controller | The ‘pipe’ lookup runs an arbitrary shell command on the Ansible controller and returns its stdout. Any variable interpolation inside makes this an RCE sink. | |
jinja_ | HIGH | set_fact Assigns Unescaped Template Expression | set_fact with a value like key: ‘{{ user_input }}’ stores the rendered (possibly attacker-controlled) template string as a fact. The next task that interpolates that fact renders it again - classic second-order template injection. | |
jinja_ | HIGH | Jinja2 Expression Wrapped Inside when: Clause | Ansible already treats the body of when: as a Jinja2 expression - wrapping it in {{ }} causes it to be rendered twice, enabling template injection if the inner variable is attacker-controlled. | |
lookup_ | HIGH | lookup(‘file’, …) With Attacker-Input Path - Controller File Disclosure | The file lookup reads files from the Ansible controller filesystem. This rule fires when the lookup path contains an INTEROLATED attacker-controlled variable (user_input, request_body, query_params, raw_input, …) OR a literal .. traversal segment. A compromised inventory variable that flows into a file lookup can disclose /etc/shadow, ~/.ssh/id_rsa, or vaulted secrets from the controller. Role-config path vars like {{ directory_name }}/config.yml are NOT flagged - they’re controller-owned and are the normal shape for parameterised role file-reads; the genuine CWE-22 risk requires attacker-controlled input, which this rule now anchors on. | |
lookup_ | HIGH | lookup(‘password’, …) Writing Plaintext to Shared Path | lookup(‘password’, ‘/tmp/…’ ) or any world-readable path persists the generated password in plaintext on the controller filesystem. | |
lookup_ | HIGH | lookup(‘url’, …) - Remote Content Retrieval on Controller | The ‘url’ lookup fetches content from a URL on the Ansible controller during play compilation. Unpinned URLs and missing validation turn this into a remote-payload sink (SSRF, malicious JSON/YAML ingestion, cache poisoning). | |
unsafe_ | HIGH | YAML !unsafe Tag Bypasses Template Sandbox | Marking a value !unsafe tells Ansible to treat the string as literal and skip templating - but when the value is later concatenated into a shell/command/jinja context, the attacker-controlled content bypasses input validation. | |
jinja_ | MEDIUM | assert: fail_msg Interpolates Unsanitised Variable | fail_msg / success_msg render Jinja2. If they interpolate attacker-controlled data, log exfiltration or second-order SSTI becomes possible. | |
lookup_ | MEDIUM | lookup(’env’, …) Reading Likely-Sensitive Controller Variable | The ’env’ lookup reads environment variables from the Ansible controller process (not the managed host). When the variable name looks like a secret (token, key, password, credentials, AWS_, GH_, etc.) this leaks controller secrets into the play scope. Benign env reads like HOME / USER / PATH / CI flag do not match. |