Skip to content

bug: Resource Injection in APISIX Ingress Controller test\e2e scaffold #2383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
b0b0haha opened this issue Apr 14, 2025 · 0 comments
Open

Comments

@b0b0haha
Copy link

Current Behavior

Target
Apache APISIX ingress controller (https://github.com/apache/apisix-ingress-controller) Reporting channel: security@apache.org (https://security.apache.org/projects/)

Affected Versions
Latest version 019d719
Exploitation Prerequisites
To exploit this vulnerability, an attacker needs:

  1. Control over the ApisixResourceVersion parameter:
  2. The attacker needs to control s.opts.ApisixResourceVersion, which is used in the replacement
  3. This is the critical input that would contain the malicious payload with newlines
  4. Access to the test scaffold environment:
  5. This vulnerability exists in the test/e2e package, suggesting this is test code
  6. An attacker would need to:
    - Access the testing environment
    - Find a way to trigger the tests with controlled parameters
    - Or find similar code patterns in the production code
  7. Kubernetes API access:
  8. For the injected Pod (or other resources) to be created, the test scaffold must have permissions to create such resources in the target Kubernetes cluster
  9. The service account running the tests needs sufficient RBAC permissions
    Exploitation Impact
    By controlling s.opts.ApisixResourceVersion to inject malicious YAML fragments, it's possible to create arbitrary resources that could lead to privilege escalation.

Root Cause Analysis

In test\e2e\scaffold\consumer.go: The call site of s.replaceApiVersion:

func (s *Scaffold) CreateVersionedApisixResource(yml string) error {
    kindValue := s.getKindValue(yml)
    if _, ok := createVersionedApisixResourceMap[kindValue]; ok { // Validation done here
        resource := s.replaceApiVersion(yml, s.opts.ApisixResourceVersion) // Direct replacement to another resource
        return s.CreateResourceFromString(resource)
    }
    return fmt.Errorf("the resource %s does not support", kindValue)
}
func (s *Scaffold) ApisixConsumerBasicAuthCreated(name, username, password string) error {
  ac := fmt.Sprintf(_apisixConsumerBasicAuth, s.opts.ApisixResourceVersion, name, username, password)
  return s.CreateVersionedApisixResource(ac)
}

  _apisixConsumerBasicAuth = `
apiVersion: %s
kind: ApisixConsumer
metadata:
  name: %s
spec:
  authParameter:
    basicAuth:
      value:
        username: %s
        password: %s
`

In test\e2e\scaffold\scaffold.go:

var (
    versionRegex = regexp.MustCompile(
apiVersion: apisix.apache.org/v.*?\n
)
    kindRegex    = regexp.MustCompile(
kind: (.*?)\n
)
)
func (s *Scaffold) replaceApiVersion(yml, ver string) string {
    return versionRegex.ReplaceAllString(yml, "apiVersion: "+ver+"\n")
}

In test\e2e\scaffold\k8s.go:

CreateResourceFromString calls k8s.KubectlApplyFromStringE, the function finally uses kubectl to apply the resouce yaml.
func (s *Scaffold) CreateResourceFromString(yaml string) error {
    err := k8s.KubectlApplyFromStringE(s.t, s.kubectlOptions, yaml)
    // if the error raised, it may be a &shell.ErrWithCmdOutput, which is useless in debug
    if err != nil {
        err = fmt.Errorf(err.Error())
    }
    return err
}
func KubectlApplyFromStringE(t testing.TestingT, options *KubectlOptions, configData string) error {
    tmpfile, err := StoreConfigToTempFileE(t, configData)
    if err != nil {
        return err
    }
    defer os.Remove(tmpfile)
    return KubectlApplyE(t, options, tmpfile)
}
func KubectlApplyE(t testing.TestingT, options *KubectlOptions, configPath string) error {
    return RunKubectlE(t, options, "apply", "-f", configPath)
}

func RunKubectlE(t testing.TestingT, options *KubectlOptions, args ...string) error {
    _, err := RunKubectlAndGetOutputE(t, options, args...)
    return err
}
func RunKubectlAndGetOutputE(t testing.TestingT, options *KubectlOptions, args ...string) (string, error) {
    cmdArgs := []string{}
    if options.ContextName != "" {
        cmdArgs = append(cmdArgs, "--context", options.ContextName)
    }
    if options.ConfigPath != "" {
        cmdArgs = append(cmdArgs, "--kubeconfig", options.ConfigPath)
    }
    if options.Namespace != "" {
        cmdArgs = append(cmdArgs, "--namespace", options.Namespace)
    }
    cmdArgs = append(cmdArgs, args...)
    command := shell.Command{
        Command: "kubectl",
        Args:    cmdArgs,
        Env:     options.Env,
    }
    return shell.RunCommandAndGetOutputE(t, command)
}

The vulnerability has two main causes:

  1. Regular Expression Matching Flaw The original regex apiVersion: apisix.apache.org/v.?\n with .? allows matching any characters (including spaces before the newline), but doesn't filter the ver input:
// Regex allows version numbers to contain newlines
versionRegex.ReplaceAllString(yml, "apiVersion: "+ver+"\n") 
  1. If ver contains \n, it directly breaks the YAML structure. For example, if an attacker sets s.opts.ApisixResourceVersion to:
    "v2\nkind: Pod\nspec:\n containers:\n - name: evil-container\n image: evil/rootkit"
    1The replaced yaml content becomes:
- apiVersion: v2
kind: Pod
spec:
  containers:
name: evil-container
image: evil/rootkit
metadata:
  name: %s
spec:
  authParameter:...
  1. At this point, Kubernetes will prioritize parsing kind: Pod rather than the original ApisixConsumer, resulting in the creation of a malicious Pod.
  2. Dynamic Resource Creation Logic Flaw The CreateVersionedApisixResource method only validates the original kind field without performing secondary validation on the replaced content:
// Original logic only checks the original kind type
if _, ok := createVersionedApisixResourceMap[kindValue]; ok { ... }

Expected Behavior

There should be sanitizer for s.opts.ApisixResourceVersion

Error Logs

No response

Steps to Reproduce

Exploitation Process
The most likely attack path is:

  1. Inject the following payload into s.opts.ApisixResourceVersion: v2\nkind: Pod\nspec:\n containers:\n - name: evil-container\n image: evil/rootkit\n hostNetwork: true\n#
  2. When ApisixConsumerBasicAuthCreated or another function using CreateVersionedApisixResource is called, the malicious YAML will be created
  3. Instead of creating an ApisixConsumer resource, a Pod with elevated privileges will be created, potentially allowing the attacker to compromise the cluster

Environment

  • APISIX Ingress controller version (run apisix-ingress-controller version --long)
  • Kubernetes cluster version (run kubectl version)
  • OS version if running APISIX Ingress controller in a bare-metal environment (run uname -a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant