4 KiB
UNDF: UNDF-2026-000000452
linkerd2-0002: CWE-407 — Quadratic opaque port lookup during pod injection
Severity: MEDIUM
Repository
github.com/linkerd/linkerd2 Commit: (depth-1 clone, branch main)
File
pkg/inject/inject.go
Defective Lines
816: defaultPorts := strings.Split(conf.GetValues().Proxy.OpaquePorts, ",") // []string slice
855: func (conf *ResourceConfig) FilterPodOpaquePorts(defaultPorts []string) []string {
856: var filteredPorts []string
857: for _, c := range append(conf.pod.spec.InitContainers, conf.pod.spec.Containers...) { // O(C)
858: for _, p := range c.Ports { // O(P)
859: port := strconv.Itoa(int(p.ContainerPort))
860: if util.ContainsString(port, defaultPorts) { // O(D)
861: filteredPorts = append(filteredPorts, port)
862: }
863: }
864: }
865: return filteredPorts
866: }
826: for _, p := range service.Spec.Ports { // O(SP) service ports
834: if util.ContainsString(port, defaultPorts) { // O(D) scan
837: } else if util.ContainsString(strconv.Itoa(int(p.TargetPort.IntVal)), defaultPorts) { // O(D)
ContainsString Implementation
// pkg/util/parsing.go:77
func ContainsString(str string, collection []string) bool {
for _, s := range collection { // O(D) linear scan
if s == str { return true }
}
return false
}
Call Chain
Admission webhook per pod/service creation/update →
CreateAnnotationPatch() [inject.go line ~815] →
FilterPodOpaquePorts(defaultPorts) →
for each container × port: ContainsString(port, defaultPorts) // O(C×P×D)
for each service port: ContainsString(port, defaultPorts) // O(SP×D)
Complexity
O(C × P × D) where:
- C = number of containers + init containers per pod
- P = number of exposed ports per container
- D = length of defaultPorts list (comma-separated string, user-configured)
defaultPorts defaults to ~25 well-known opaque ports (3306, 5432, 6379, etc.)
but can be extended by operators. It is parsed as []string and scanned linearly
on every port check. The same defaultPorts slice is scanned multiple times per
pod without pre-indexing.
Impact
Every pod and service injection via the linkerd-proxy-injector webhook calls this path. In clusters with high churn (rolling deployments, HPA scaling), this webhook is hot. Pods with many containers and ports (e.g., sidecar-heavy microservices) cause O(C×P×D) work per admission request.
Fix
Pre-build a map[string]struct{} from defaultPorts once before the loop.
// Before (defective):
func (conf *ResourceConfig) FilterPodOpaquePorts(defaultPorts []string) []string {
var filteredPorts []string
for _, c := range ... {
for _, p := range c.Ports {
port := strconv.Itoa(int(p.ContainerPort))
if util.ContainsString(port, defaultPorts) { // O(D) per port
filteredPorts = append(filteredPorts, port)
}
}
}
return filteredPorts
}
// After (fixed):
func (conf *ResourceConfig) FilterPodOpaquePorts(defaultPorts []string) []string {
defaultSet := make(map[string]struct{}, len(defaultPorts))
for _, p := range defaultPorts { defaultSet[p] = struct{}{} }
var filteredPorts []string
for _, c := range ... {
for _, p := range c.Ports {
port := strconv.Itoa(int(p.ContainerPort))
if _, ok := defaultSet[port]; ok { // O(1)
filteredPorts = append(filteredPorts, port)
}
}
}
return filteredPorts
}
The same fix applies to the service port checks at lines 834 and 837 —
build the set once before the for _, p := range service.Spec.Ports loop.
References
- CWE-407: Inefficient Algorithmic Complexity
pkg/inject/inject.golines 816-865pkg/util/parsing.golines 76-82 (ContainsString)